sync milestone-1 spec and api notes with as-built findings

This commit is contained in:
2026-07-31 23:59:40 +02:00
parent b960413ea9
commit bf02f83adc
2 changed files with 46 additions and 10 deletions
+16 -8
View File
@@ -20,8 +20,9 @@ S1 ──> { S2, S3, S4, S5 } ──> orchestrator integration
- `.name = .nxdns`, `.version = "0.1.0"`, `.minimum_zig_version = "0.16.0"`, `.paths = .{""}`, `.fingerprint` (compiler suggests the value on first build — accept it).
- Dependencies added with `zig fetch --save=<name> <url>` so the content hash is pinned:
- `sqlite`: the newest stable SQLite **amalgamation zip** from sqlite.org (check https://sqlite.org/download.html for the current one; record the version in a comment).
- `mbedtls`: the newest **mbedTLS 3.6.x LTS** release tarball from the Mbed-TLS GitHub releases (3.6 line only — not 4.x).
- `sqlite`: SQLite 3.53.4 amalgamation zip (https://sqlite.org/2026/sqlite-amalgamation-3530400.zip).
- `mbedtls`: mbedTLS 3.6.7 LTS via the GitHub **source tag** tarball (refs/tags/mbedtls-3.6.7.tar.gz). The release asset is .tar.bz2, which `zig fetch` cannot decompress; the source tarball was diffed against it — identical library + 3rdparty content (3rdparty is in-tree in the 3.6 line).
- As built: `.gitignore` also covers `zig-pkg/` (0.16 unpacks deps there); build.zig adds `-Dgit-commit` (default "unknown") and a `run` step; mbedTLS compiles the 103 `library/*.c` files plus the five 3rdparty objects named by the everest/p256-m Makefile.inc fragments; `installHeadersDirectory` exposes mbedtls/psa headers to any module linking the lib (S4's shim needs no extra include wiring).
### S1.2 build.zig
@@ -38,7 +39,7 @@ S1 ──> { S2, S3, S4, S5 } ──> orchestrator integration
### S1.3 src/main.zig, src/version.zig, src/tests.zig
- `src/version.zig`: `pub const string` / `pub const zig_version_string` pulled from `build_options`.
- `src/main.zig`: parse first CLI arg. `version` → print version + zig version, exit 0. `run`, `check`, `export`, `import` → print `not implemented`, exit 2. No arg / unknown → usage on stderr, exit 64. No allocator gymnastics`std.process.args` is enough here.
- `src/main.zig`: parse first CLI arg. `version` → print version + zig version, exit 0. `run`, `check`, `export`, `import` → print `not implemented`, exit 2. No arg / unknown → usage on stderr, exit 64. Args come from `std.process.Init` (`main(init: std.process.Init)`)`std.process.args` does not exist in 0.16; see the API notes.
- `src/tests.zig`: `comptime { _ = @import("main.zig"); _ = @import("version.zig"); }` plus `test` block asserting the sqlite3 and mbedTLS C headers link: call `sqlite3_libversion()` and `mbedtls_version_get_string_full()` via `@cImport`-free extern declarations (declare the two extern fns manually) and check non-empty results.
### S1.4 Acceptance Criteria
@@ -115,7 +116,9 @@ pub const TlsStream = struct {
// (tls.Client holds its reader/writer by value)
pub const Options = struct {
host: []const u8, // SNI + verification name
host: []const u8, // SNI + verification name (always sent as SNI)
// insecure_skip_verify skips CA/expiry verification but keeps SNI and leaf
// hostname matching (stdlib couples SNI to .host = .explicit)
ca: enum { system, insecure_skip_verify },
// buffers supplied by caller; read_buffer.len >= std.crypto.tls.Client.min_buffer_len
read_buffer: []u8,
@@ -148,7 +151,8 @@ pub const TlsStream = struct {
### S3.2 Tests
- Unit: `classify` mapping table (pick 6+ representative errors across the four classes).
- Integration (compiled only when `build_options.integration`): connect to a live host (`cloudflare-dns.com:853` — DoT port, TLS without ALPN), complete the handshake with `.ca = .system`, close cleanly. This test is NOT part of the default `zig build test` run and NOT part of blocking CI.
- Integration: lives in `src/platform/tls_client_integration_test.zig` (separate file — conditional imports are impossible in 0.16; see API notes). Runtime guard `if (!build_options.integration) return error.SkipZigTest;` — the body stays type-checked in every build, runs only under `-Dintegration`. Connects to `1.1.1.1:853` (IPv4 literal — this host's IPv6 egress is dead and the anycast address is documented) with `.host = "cloudflare-dns.com"` so SNI + hostname verification run against the real name; `.ca = .system`; clean close. NOT part of default `zig build test`, NOT part of blocking CI.
- As built: `InitError = tls.Client.InitError || error{CertificateBundleLoadFailure}` (bundle rescan folded in, std.http.Client style). `classify` reaches exhaustiveness via `inline for` over `@typeInfo(tls.Client.InitError).error_set.?` — one mapping site, new stdlib errors break the build there.
### S3.3 Acceptance Criteria
@@ -196,8 +200,9 @@ pub const ServerStream = struct {
};
```
- Error mapping: negative mbedTLS return codes → Zig error set with named errors for the common cases (`CertParse`, `KeyParse`, `HandshakeFailed`, `PeerClosed`, `WantReadWrite` handled internally); include the raw code in a log via `mbedtls_strerror`.
- `MBEDTLS_ERR_SSL_WANT_READ/WANT_WRITE` loop inside read/write — never surfaces to callers.
- Error mapping: negative mbedTLS return codes → Zig error set with named errors for the common cases (`CertParse`, `KeyParse`, `KeyMismatch`, `HandshakeFailed`); include the raw code in a log via `mbedtls_strerror`.
- `MBEDTLS_ERR_SSL_WANT_READ/WANT_WRITE` loop inside read/write — never surfaces to callers. ONLY those two codes retry; the `*_IN_PROGRESS` codes are misconfiguration (no async crypto configured) and surface as errors.
- As built (review round 1): only an authenticated close_notify reads as clean `error.EndOfStream`; transport EOF without close_notify → `error.ReadFailed` with `read_err = .TlsConnectionTruncated` (no opt-out). `ServerContext.init` calls `mbedtls_pk_check_pair` (via the `nx_x509_crt_pk` shim accessor) → `error.KeyMismatch` on a mismatched pair, covered by the `tests/fixtures/mismatched_key.pem` fixture. mbedTLS compiles with `MBEDTLS_THREADING_C` + `MBEDTLS_THREADING_PTHREAD` (build.zig `addMbedtlsThreadingMacros`, applied to every module that sees mbedTLS headers — context sizes change with threading).
### S4.3 Test fixture + loopback test
@@ -254,6 +259,8 @@ src/platform/tls_server.zig S4 mbedTLS server wrapper
src/platform/mbedtls_shim.c S4 sizeof shims
tests/fixtures/self_signed_cert.pem S4
tests/fixtures/self_signed_key.pem S4
tests/fixtures/mismatched_key.pem S4 (review round 1: KeyMismatch fixture)
tests/fixtures/fixtures.zig S4 (@embedFile module root for fixtures)
tests/fixtures/README.md S4
.gitea/workflows/ci.yml S5
.gitea/workflows/live-tls.yml S5
@@ -274,7 +281,8 @@ S2S5 MUST NOT edit build.zig, build.zig.zon, or src/tests.zig. If a session n
## Acceptance Criteria (Milestone 1 Complete)
- [ ] `zig build test` exits 0 (aggregator includes address, tls_client unit, tls_server unit tests).
- [ ] `zig build test -Dintegration` exits 0 locally (loopback TLS echo; live DoT test may be skipped on network failure with a visible skip message).
- [ ] `zig build test -Dintegration` exits 0 locally (hermetic loopback TLS echo; deterministic, PR-blocking in CI).
- [ ] `zig build test -Dintegration -Dlive` exits 0 locally (adds the live DoT handshake; manual workflow only — a live-network failure is an environment finding, not a gate).
- [ ] `zig build cross` produces two statically linked executables that print `nxdns version` output under qemu-user or on-target (checked manually for aarch64 if qemu absent).
- [ ] CI workflows valid YAML; `test` + `cross` jobs green on the Gitea runner.
- [ ] All files committed with GPG-signed, lowercase-message commits.
+30 -2
View File
@@ -34,6 +34,30 @@ changed most of these APIs.
(compiler suggests the value on first run), plus `.version`, `.paths`, `.dependencies`
(`path`/`url`+`hash`/`lazy`), `.minimum_zig_version`.
## std.process + stdio (found during S1)
- `std.process.args` / `std.process.ArgIterator` **do not exist** in 0.16. Arguments arrive
through main's parameter: `pub fn main(init: std.process.Init) u8 { var args =
init.minimal.args.iterate(); ... }`. `std.process.Init` carries `.io`, `.gpa`, `.arena`,
`.environ_map`, `.preopens`, `.minimal` (`.args`, `.environ`).
See lib/std/process.zig:30 and lib/std/process/Args.zig.
- Console output: `std.Io.File.stdout().writer(io, &buffer)` then use `.interface`
(lib/std/Io/File.zig:91,600). Same shape for stderr.
- Package deps unpack into `zig-pkg/` inside the project root (gitignore it).
- Entropy: `io.random(buf)` (Io.zig:2468). Realtime timestamp: `std.Io.Clock.real.now(io)`
→ `Io.Timestamp` (Io.zig:778). There is NO `io.now(.real)`.
- `std.mem.indexOfScalar` is gone; use `std.mem.findScalar` (mem.zig:1219).
- Conditional imports are impossible: no `@hasImport`, and `@import("root")` in a
`zig test` build resolves to the compiler's test runner, never your tests root.
Integration tests gated on `build_options` therefore live in a separate file with a
runtime `if (!build_options.integration) return error.SkipZigTest;` guard (body stays
semantically analyzed either way — code cannot rot).
- ENVIRONMENT (this dev machine): IPv6 egress is broken — hostname-resolving tests hit
AAAA-first timeouts. Use documented IPv4 literals in live tests, keep SNI on the name.
- `zig fetch` accepts tar.gz/zip etc. but **not .tar.bz2** (no bzip2 decompressor —
src/Package/Fetch.zig ~line 1337). Pin GitHub *source tag* tarballs when a release
asset is bz2-only.
## std.Io (Threaded backend, concurrency)
- `var t = std.Io.Threaded.init(gpa, .{});` — returned by value, must live at a **stable
@@ -59,8 +83,12 @@ changed most of these APIs.
- TCP server: `addr.listen(io, .{ .reuse_address = true })` → `net.Server`;
`srv.accept(io) !Stream`; `srv.deinit(io)`. `Stream.close(io)`, `.shutdown(io, how)`.
Shutdown of the listener makes a blocked accept fail `error.SocketNotListening`.
- TCP client: `addr.connect(io, .{ .timeout = ... })` → `Stream`.
`Io.Timeout = union(enum){ none, duration, deadline }` — **connect only**.
- TCP client: `addr.connect(io, .{ .mode = .stream })` → `Stream`
`ConnectOptions.mode` is REQUIRED (no default, net.zig:332).
`ConnectOptions.timeout` is a LANDMINE in 0.16.0: the Threaded backend panics
"TODO implement netConnectIpPosix with timeout" (Threaded.zig:12077). Never set it.
Bound connects the same way as reads: race the task against a `Clock.Duration.sleep`
via `std.Io.Select` and cancel the loser (see tls_client_integration_test.zig).
- **Stream reads/writes accept no timeout** in 0.16.0 (VTable netRead/netWrite have none;
Operation lacks net stream variants). Bound a TCP/TLS read by running it under
`io.concurrent` and cancelling the future (or shutdown the socket).