docs: unwrap hand-wrapped prose repo-wide
Gates / frontend (push) Successful in 1m2s
Gates / test (push) Successful in 1m38s
Gates / package (push) Successful in 5m5s
Gates / test-aarch64 (push) Successful in 6m30s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 13m30s
Gates / frontend (push) Successful in 1m2s
Gates / test (push) Successful in 1m38s
Gates / package (push) Successful in 5m5s
Gates / test-aarch64 (push) Successful in 6m30s
Gates / container (push) Successful in 15s
CI / gates (push) Successful in 13m30s
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
# nxdns — Implementation Plan v3.0 (Zig 0.16.0 Stable)
|
||||
|
||||
Source of truth for **nxdns**, a self-hosted DNS sinkhole written in Zig 0.16.0 stable.
|
||||
All stdlib claims in this document are verified against the `0.16.0` tag of the Zig repo (`../zig`).
|
||||
Source of truth for **nxdns**, a self-hosted DNS sinkhole written in Zig 0.16.0 stable. All stdlib claims in this document are verified against the `0.16.0` tag of the Zig repo (`../zig`).
|
||||
|
||||
There is no v1/v2 versioning. Scope is binary: a feature is in scope (and gets built) or out of scope (and does not). "Done" = everything in scope implemented, tested, documented.
|
||||
|
||||
@@ -121,16 +120,9 @@ Rule kinds: `exact`, parent-walk (implicit via candidate chain), `wildcard` (`*`
|
||||
|
||||
Tie-break at same specificity: **allow wins**.
|
||||
|
||||
Each level is checked against the whole candidate chain before the next level is
|
||||
checked against any, which is what makes an allow rule on a parent beat a block
|
||||
rule on the child.
|
||||
Each level is checked against the whole candidate chain before the next level is checked against any, which is what makes an allow rule on a parent beat a block rule on the child.
|
||||
|
||||
Two positions carry an argument rather than a preference. The regex levels come
|
||||
last among the operator rules because they are the only ones that are not a set
|
||||
lookup or a label walk: a regex runs only once every cheaper level has missed.
|
||||
Blocklist exceptions come below **every** operator level because a downloaded
|
||||
list may cancel what another list blocked and must never cancel what the
|
||||
operator decided — no list can open an allow hole the operator did not open.
|
||||
Two positions carry an argument rather than a preference. The regex levels come last among the operator rules because they are the only ones that are not a set lookup or a label walk: a regex runs only once every cheaper level has missed. Blocklist exceptions come below **every** operator level because a downloaded list may cancel what another list blocked and must never cancel what the operator decided — no list can open an allow hole the operator did not open.
|
||||
|
||||
### 3.11 Network Posture
|
||||
|
||||
@@ -290,8 +282,7 @@ Walk chain to depth 8; any target hitting block logic → synthesize blocked res
|
||||
|
||||
### 7.1 Evaluation
|
||||
|
||||
For `{domain, group_id}` (the qtype travels with the query for logging and response synthesis, not
|
||||
for matching):
|
||||
For `{domain, group_id}` (the qtype travels with the query for logging and response synthesis, not for matching):
|
||||
1. Normalize: lowercase, trim trailing dot.
|
||||
2. Build candidate chain (full, parent1, parent2, …).
|
||||
3. Explicit rules per §3.10 precedence, allow before block at each level: exact rules against every candidate in the chain, then wildcard patterns and then regex patterns against the whole name (both kinds express their own reach, so neither walks the chain).
|
||||
@@ -300,10 +291,7 @@ for matching):
|
||||
6. Group's blocklist wildcards, matched against every proper parent of the query name.
|
||||
7. No match → allow.
|
||||
|
||||
Blocklist *domain* entries do not parent-walk: they are matched against the query name alone. Wildcard
|
||||
entries match every proper parent, and exception entries walk the candidate chain the way rules do
|
||||
(§3.9), so `@@||good.ads.example^` also lifts `y.good.ads.example`. ABP `||x.y^` emits both a domain
|
||||
entry `x.y` and a wildcard entry `x.y`, which together give domain-and-subdomains semantics.
|
||||
Blocklist *domain* entries do not parent-walk: they are matched against the query name alone. Wildcard entries match every proper parent, and exception entries walk the candidate chain the way rules do (§3.9), so `@@||good.ads.example^` also lifts `y.good.ads.example`. ABP `||x.y^` emits both a domain entry `x.y` and a wildcard entry `x.y`, which together give domain-and-subdomains semantics.
|
||||
|
||||
### 7.2 Group Assignment
|
||||
|
||||
@@ -314,12 +302,7 @@ entry `x.y` and a wildcard entry `x.y`, which together give domain-and-subdomain
|
||||
|
||||
### 7.3 Reload
|
||||
|
||||
New immutable matcher built from DB + compiled list files → swap under an `std.Io.RwLock` with a
|
||||
generation counter. Readers take the shared lock for the microseconds of one evaluate; the writer
|
||||
takes the exclusive lock only for the swap, and the source status table is installed in the same
|
||||
critical section, so a failed reload publishes neither. (Deliberate deviation from "readers
|
||||
lock-free": freeing the old snapshot without a lock needs epoch-based reclamation, unjustifiable at
|
||||
household scale — see specs/milestone-5.md S8.3.)
|
||||
New immutable matcher built from DB + compiled list files → swap under an `std.Io.RwLock` with a generation counter. Readers take the shared lock for the microseconds of one evaluate; the writer takes the exclusive lock only for the swap, and the source status table is installed in the same critical section, so a failed reload publishes neither. (Deliberate deviation from "readers lock-free": freeing the old snapshot without a lock needs epoch-based reclamation, unjustifiable at household scale — see specs/milestone-5.md S8.3.)
|
||||
|
||||
### 7.4 Safe-Search
|
||||
|
||||
@@ -365,14 +348,9 @@ Per-group boolean. Rewrites known engine domains to their safe-search CNAME targ
|
||||
|
||||
### 11.2 config.db Schema (v1 baseline)
|
||||
|
||||
The DDL below is the live schema, kept byte-identical to
|
||||
`src/storage/config_schema.zig`. `src/storage/migrations.zig` carries it as its
|
||||
one and only step, so a database is at **version 1** or it does not exist.
|
||||
The DDL below is the live schema, kept byte-identical to `src/storage/config_schema.zig`. `src/storage/migrations.zig` carries it as its one and only step, so a database is at **version 1** or it does not exist.
|
||||
|
||||
Until nxdns reaches v0.1 this baseline is **editable**: a schema change edits
|
||||
this section and `config_schema.zig` together and adds no migration step. nxdns
|
||||
has no installs, so there is no database for a step to reconcile. At v0.1 the
|
||||
baseline freezes and every later change becomes an append-only step.
|
||||
Until nxdns reaches v0.1 this baseline is **editable**: a schema change edits this section and `config_schema.zig` together and adds no migration step. nxdns has no installs, so there is no database for a step to reconcile. At v0.1 the baseline freezes and every later change becomes an append-only step.
|
||||
|
||||
```sql
|
||||
CREATE TABLE schema_version (version INTEGER NOT NULL);
|
||||
@@ -509,14 +487,7 @@ Periodic delete of rows older than `retention_days`; scheduled checkpoint/VACUUM
|
||||
|
||||
### 12.1 Config ZON Shape
|
||||
|
||||
The canonical shape is not duplicated here. It lives in
|
||||
[docs/reference/configuration.md](docs/reference/configuration.md), which is
|
||||
handwritten against `config/model.zig` and only partly guarded (the drift test
|
||||
covers settings-key rows, not the whole shape, so a new collection can go
|
||||
undocumented while the guard stays green), and `nxdns export` emits it. A copy in
|
||||
this document is how §12.1 came to describe an `.upstream.servers` field that
|
||||
never existed and to omit the required `.groups` and `.upstreams` — a sample
|
||||
nobody could load. The skeleton, for orientation only:
|
||||
The canonical shape is not duplicated here. It lives in [docs/reference/configuration.md](docs/reference/configuration.md), which is handwritten against `config/model.zig` and only partly guarded (the drift test covers settings-key rows, not the whole shape, so a new collection can go undocumented while the guard stays green), and `nxdns export` emits it. A copy in this document is how §12.1 came to describe an `.upstream.servers` field that never existed and to omit the required `.groups` and `.upstreams` — a sample nobody could load. The skeleton, for orientation only:
|
||||
|
||||
```zon
|
||||
.{
|
||||
@@ -584,48 +555,37 @@ Requirements: responsive desktop/mobile; route loaders for initial fetch; TanSta
|
||||
## 16. Implementation Order
|
||||
|
||||
### Phase 0 — Build Baseline
|
||||
Scaffold tree; `build.zig` with 0.16 assertion, musl targets, vendored sqlite3 + mbedtls compiling; version plumbing; Gitea Actions workflows (test, integration, fuzz smoke, OpenAPI lint, frontend build).
|
||||
Exit: cross-compiled hello-world linking both C deps on both targets; CI green.
|
||||
Scaffold tree; `build.zig` with 0.16 assertion, musl targets, vendored sqlite3 + mbedtls compiling; version plumbing; Gitea Actions workflows (test, integration, fuzz smoke, OpenAPI lint, frontend build). Exit: cross-compiled hello-world linking both C deps on both targets; CI green.
|
||||
|
||||
### Phase 1 — Platform Layer
|
||||
`platform/address.zig` (v4/v6 parity + canonical keys); `platform/tls_client.zig` (deadlines, error classes); `platform/tls_server.zig` (mbedTLS handshake → `std.Io.Reader`/`Writer`).
|
||||
Exit: UDP echo over `std.Io`; TLS client handshake against a real host; mbedTLS server terminating a loopback TLS connection.
|
||||
`platform/address.zig` (v4/v6 parity + canonical keys); `platform/tls_client.zig` (deadlines, error classes); `platform/tls_server.zig` (mbedTLS handshake → `std.Io.Reader`/`Writer`). Exit: UDP echo over `std.Io`; TLS client handshake against a real host; mbedTLS server terminating a loopback TLS connection.
|
||||
|
||||
### Phase 2 — DNS Core
|
||||
Types/header/name/question/record/packet; parser + encoder tests + fuzz target; EDNS + DO passthrough.
|
||||
Exit: unit + fuzz smoke pass.
|
||||
Types/header/name/question/record/packet; parser + encoder tests + fuzz target; EDNS + DO passthrough. Exit: unit + fuzz smoke pass.
|
||||
|
||||
### Phase 3 — Resolver Transport
|
||||
UDP server, TCP server, DoH + DoT upstream clients, pool + failover/backoff + health.
|
||||
Exit: A/AAAA forwarding over UDP + TCP; health populated.
|
||||
UDP server, TCP server, DoH + DoT upstream clients, pool + failover/backoff + health. Exit: A/AAAA forwarding over UDP + TCP; health populated.
|
||||
|
||||
### Phase 4 — Storage + Config
|
||||
SQLite wrapper; config.db schema + migration runner; querylog.db schema + recreate-on-mismatch; repositories; ZON loading + import/export; `nxdns check`.
|
||||
Exit: export → import round-trips byte-stable. (The ZON bootstrap this phase shipped was replaced in m20 by the two authority modes above.)
|
||||
SQLite wrapper; config.db schema + migration runner; querylog.db schema + recreate-on-mismatch; repositories; ZON loading + import/export; `nxdns check`. Exit: export → import round-trips byte-stable. (The ZON bootstrap this phase shipped was replaced in m20 by the two authority modes above.)
|
||||
|
||||
### Phase 5 — Filtering + Local DNS
|
||||
Rule matcher (exact/parent/wildcard; `regex` added in m21); blocklist parsers → compiled file format; fetcher + scheduled update; RCU swap; per-group safe-search; local records; forward zones.
|
||||
Exit: precedence table validated by tests; local zone answers + conditional forwards work.
|
||||
Rule matcher (exact/parent/wildcard; `regex` added in m21); blocklist parsers → compiled file format; fetcher + scheduled update; RCU swap; per-group safe-search; local records; forward zones. Exit: precedence table validated by tests; local zone answers + conditional forwards work.
|
||||
|
||||
### Phase 6 — Cache + Rate Limit + Logging + Disk Monitor
|
||||
TTL cache + reconstruction; v4/v6 rate limiter; async query logger + backpressure + retention; DiskMonitor + rotation + error-log dedup.
|
||||
Exit: disk thresholds trigger degradation + drop counters in integration test.
|
||||
TTL cache + reconstruction; v4/v6 rate limiter; async query logger + backpressure + retention; DiskMonitor + rotation + error-log dedup. Exit: disk thresholds trigger degradation + drop counters in integration test.
|
||||
|
||||
### Phase 7 — Handler Integration
|
||||
Full pipeline composition; CNAME uncloaking; pause/resume.
|
||||
Exit: end-to-end DNS flow with blocking, local records, cache, failover.
|
||||
Full pipeline composition; CNAME uncloaking; pause/resume. Exit: end-to-end DNS flow with blocking, local records, cache, failover.
|
||||
|
||||
### Phase 8 — Web / API / SSE / Auth / Metrics
|
||||
HTTP server + router; handlers; SSE; optional auth; API rate limiting; `/metrics`; OpenAPI served + contract tests; embedded frontend + dev-mode disk serving.
|
||||
Exit: frontend fully drives config and operations; contract tests green.
|
||||
HTTP server + router; handlers; SSE; optional auth; API rate limiting; `/metrics`; OpenAPI served + contract tests; embedded frontend + dev-mode disk serving. Exit: frontend fully drives config and operations; contract tests green.
|
||||
|
||||
### Phase 9 — Local DoH/DoT Endpoints
|
||||
DoH server + DoT server on `platform/tls_server.zig`; cert watcher + reload.
|
||||
Exit: LAN client resolves via DoH and DoT against local certs.
|
||||
DoH server + DoT server on `platform/tls_server.zig`; cert watcher + reload. Exit: LAN client resolves via DoH and DoT against local certs.
|
||||
|
||||
### Phase 10 — Packaging + Ops + Docs
|
||||
systemd unit (`AmbientCapabilities=CAP_NET_BIND_SERVICE`, hardened, writable `/var/lib/nxdns` + optional `/var/log/nxdns`); Dockerfile + compose (53/udp+tcp, 8080; mounts `/etc/nxdns`, `/var/lib/nxdns`); operator/architecture/config-reference/API docs.
|
||||
Exit: documented deployment works end-to-end on the Pi 5.
|
||||
systemd unit (`AmbientCapabilities=CAP_NET_BIND_SERVICE`, hardened, writable `/var/lib/nxdns` + optional `/var/log/nxdns`); Dockerfile + compose (53/udp+tcp, 8080; mounts `/etc/nxdns`, `/var/lib/nxdns`); operator/architecture/config-reference/API docs. Exit: documented deployment works end-to-end on the Pi 5.
|
||||
|
||||
---
|
||||
|
||||
@@ -660,9 +620,7 @@ Exit: documented deployment works end-to-end on the Pi 5.
|
||||
|
||||
## 20. Publication
|
||||
|
||||
The project publishes released binaries and container images from its own Gitea
|
||||
instance. Building from source stays fully supported and documented; it is no
|
||||
longer the only path.
|
||||
The project publishes released binaries and container images from its own Gitea instance. Building from source stays fully supported and documented; it is no longer the only path.
|
||||
|
||||
- **Trigger.** Pushing an annotated, GPG-signed tag `vX.Y.Z` to `git.mial.net/mokhtar/nxdns`. Nothing else publishes. Pre-release tags are rejected.
|
||||
- **Version.** The tag is authoritative. `build.zig.zon`'s `.version` must equal the tag, and the packaging gate asserts it. Nowhere else stores a version.
|
||||
|
||||
Reference in New Issue
Block a user