diff --git a/specs/milestone-1.md b/specs/milestone-1.md index 8765317..87cd153 100644 --- a/specs/milestone-1.md +++ b/specs/milestone-1.md @@ -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= ` 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 @@ S2–S5 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. diff --git a/specs/research/zig-0.16-api-notes.md b/specs/research/zig-0.16-api-notes.md index 9fb5deb..d07cf35 100644 --- a/specs/research/zig-0.16-api-notes.md +++ b/specs/research/zig-0.16-api-notes.md @@ -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).