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
+3 -1
View File
@@ -26,7 +26,9 @@ missing tutorial, and prove every instruction by running it.
alone: the first real blocklist download aborted the process (commit 35f2324)
and a stale SPA bundle crashed the settings page — both would have surfaced if
the documented paths had been run. No session may report a page complete on the
strength of having read the code.
strength of having read the code. This ruling constrains **command lines**;
a pasted **output line** may carry a placeholder where the literal text
would go stale, as milestone-17 ruling 8 does with `nxdns <version>`.
4. **Content is verified against `src/`, not copied from the old pages.** Defaults,
flags, exit codes, paths and route names come from the code at HEAD. Report any
+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).
+23
View File
@@ -313,6 +313,29 @@ pinned by two tests. Restructure: `try { await onSubmit(...) } catch (error)
{ if (error instanceof ApiError) return; throw error; }`, resets after the
try. The page's `<InlineError>` keeps rendering the mutation error as today.
### Carried in from milestone 17: the stale phase-comment sweep
Milestone 17 ruling 7 rewrote the "Phase 7/8" headers in three files; its
repo-wide acceptance grep then found 24 more stale phase references in
files its sessions did not own, recorded here for this milestone:
safesearch.zig:58, forward_client.zig:7, dns_cache.zig:10/:42/:435,
cli.zig:332, filter_integration_test.zig:1391, logger.zig:178/:303,
retention.zig:7/:137/:151, disk_monitor.zig:7, querylog_schema.zig:71,
db.zig:272, manager.zig:292/:613/:1145, and the six repository files
(groups_repo.zig:7, rules_repo.zig:15, sources_repo.zig:9,
upstreams_repo.zig:7, local_repo.zig:3, clients_repo.zig:12-13). Each
comment is rewritten to state the as-built truth it gestures at (not
deleted, unless it says nothing beyond the phase number). Line numbers
are hints from m17-time HEAD; re-grep before acting. Acceptance: "Phase
7" and "Phase 8" appear in no source doc comment repo-wide; spec files
and TECH_DEBT.md are exempt. The session owning each file in the plan
below picks up its share; files owned by no session fall to S4.
Also carried from milestone 17 (ruling 4 residual): the login log line in
`src/web/handlers/auth.zig` prints the socket peer, which reads as
loopback for every login behind a trusted proxy; it should print
`request.client_addr`.
## Sessions
S1-S4 run in parallel; no two sessions write the same file.
+8
View File
@@ -54,6 +54,14 @@ anything else against /home/mokhtar/app/zig tag 0.16.0.
re-materializes). Client prefixes: `GET/PUT /api/client-prefixes` as a whole-list
resource (tiny table, atomic replace). No POST for clients — creation is by DNS activity
or import (PLAN:540 gives clients no POST deliberately).
> **Placement deviation (milestone 17 ruling 3).** "The Settings page must
> edit them" above names the wrong page. Milestone 9 dropped the obligation
> entirely; milestone 17 restored it as a dedicated **Upstreams** page with
> its own nav entry (`web/src/features/upstreams/`, route `/upstreams`),
> matching the house resource-page pattern that every other collection
> follows. The Settings page keeps the scalar settings keys only. The API
> contract in this ruling is unchanged.
10. **Repo layer**: every list row the API serves carries its row id; each mutated resource
gains `getX(db, id)`, `updateX(db, id, item)`, `deleteX(db, id)` (strict: 0 rows touched
→ error.NotFound), written in the house repo idiom with prepared statements. Existing