milestone 17: real deadlines, validator holes, upstream editor, trusted proxies, contract samples, badvers
CI / test (push) Successful in 1m22s
CI / test-aarch64 (push) Successful in 4m55s
CI / frontend (push) Successful in 39s
CI / cross (push) Successful in 7m57s
CI / docker (push) Failing after 1h10m42s

This commit is contained in:
2026-08-07 17:55:59 +02:00
parent 9b12dbaaa0
commit c50c6d285a
57 changed files with 2926 additions and 126 deletions
+44 -2
View File
@@ -78,6 +78,17 @@ total budget. Decision: the total budget becomes real.
file's ceiling-only idiom: `1..1_000_000` with the "must be at most"
message shape. No full memory-fit guarantee — out of reach, not needed.
**Recorded (implementation):** three consequences this ruling did not call
out. (a) `cache.size = 0` was a documented "disables caching" mode; the
`1..` floor removes it — configuration.md now says there is no off value
(use 1). (b) The configuration.md NextDNS-DoT redaction passage lost its
premise (a hostname `tls://` URL is now unconfigurable) and was rewritten.
(c) Three config fixtures used `tls://dot.example:853` and now use an IP
literal with `tls_name`; the back-up-and-restore.md export transcript grew
to `head -10` with real re-captured output. Also: `Pool.init` takes a named
`pool.Timeouts { attempt, total }` struct, not two positional durations —
two same-typed adjacent parameters would be silently swappable.
### 3. The upstream editor is built
Milestone 8 ruling 9 requires upstream CRUD in the UI ("the Settings page
@@ -179,6 +190,16 @@ address's 3-stream SSE cap. No X-Forwarded-For handling exists anywhere
chain → 400); an integration test proving a proxied remote address is
rate-limited while the proxy itself stays exempt.
**Recorded (implementation of ruling 4):** `Request.client_addr` is
`std.Io.net.IpAddress`, not `address.NetAddress``http_util.zig` is a
module root for the fuzz target and cannot import nxdns modules; any future
ruling that wants a domain type on `Request` hits this. `clientAddr` reads
only the final chain entry, never scanning backwards: an earlier entry is
client-controlled, so a fallback would hand the request an attacker-chosen
identity. Known residual, out of this ruling's list: the login log line in
`handlers/auth.zig` still prints the socket peer, which reads as loopback
behind a trusted proxy — carried to milestone 19.
### 5. A field-level contract guard between server and frontend
The REST contract lives in three hand-synced copies. The Zig side is
@@ -217,6 +238,16 @@ Mechanism — captured samples validated by `tsc`, no new dependencies:
checking, so a server field missing from types.ts, a types.ts field
missing from the wire, and an out-of-union literal all fail
`npm run typecheck` — which already runs in CI's frontend job.
**Recorded (implementation):** canonicalization gained two rules beyond
"keys sorted, numbers→0": repeated array elements dedupe by canonical text
(60 identical zeroed timeseries buckets collapse to one; differing rows all
stay), and two build-identity strings (`Version.git_commit`,
`Version.zig_version`) redact to `"<build>"` so the golden is not pinned to
one machine — the list is the named constant `volatile_string_keys`. The
renderer emits prettier's own fixpoint output, because CI runs
`format:check` on the tree. 43 samples: 38 endpoint + 5 error classes. The
regen command is documented in AGENTS.md.
- Embedding: an anonymous-import root must be Zig, not TypeScript, so a
wrapper `web/src/lib/contract_samples.zig` exports
`pub const bytes = @embedFile("contractSamples.gen.ts");` and build.zig
@@ -262,6 +293,13 @@ finding.
`edns.extendedRcode`) and version 0 in the reply OPT; a version-0
query is unaffected.
**Recorded (implementation):** the write-side splitter is
`edns.splitRcode` with `pub const badvers: u12 = 16`; the BADVERS reply
echoes the question when `qdcount == 1`, following the neighboring
FORMERR sites, so a client can bind the reply to its query; a malformed
OPT whose version byte is 1 answers FORMERR, because `parseOpt` fails
before the version check can read the byte.
### 7. The stale phase comments state the as-built contract
- `rate_limiter.zig:5-6` ("Phase 7 decides the locking"): rewrite to the
@@ -404,8 +442,12 @@ Deleted surface: the seven unused `get*` wrappers, `getMetrics`,
types.ts type fails `npm run typecheck` (proven, then reverted).
- [ ] A version-1 EDNS query answers composite RCODE 16 with version 0;
`nxdns_dns_badvers_total` appears in /metrics.
- [ ] Grep: "Phase 7" and "Phase 8" appear in no source doc comment;
the rewritten headers state the mutex and the maintenance task.
- [ ] "Phase 7"/"Phase 8" no longer appear in `rate_limiter.zig`,
`shutdown.zig`, or `pool.zig`; the rewritten headers state the
mutex and the maintenance task. (Narrowed during implementation:
the original repo-wide grep exceeded ruling 7's three files — 24
stale phase references remain in files this milestone's sessions
do not own; the sweep moved to milestone 19.)
- [ ] Grep: `0.1.0-dev` appears in no docs/ page; the new drift test
passes and was proven able to fail (reinsert the literal, watch it
fail, revert).