Files
fancy-dns/STATUS.md
T
mats 66f36cfac6
build / build-push (push) Successful in 1m11s
more stuff
2026-10-16 14:49:50 +02:00

139 lines
7.7 KiB
Markdown

# fancy-dns — status and continuation plan
`AGENTS.md` holds the decisions, invariants and working rules. This file holds
state and the plan. History is in `git log`; the old per-round fix lists were
removed because each fix is explained in its commit and pinned by a test.
Last updated after the round that added client-group validation. The cluster-object
architecture, the separate console and the web UI are deployed.
## What exists
Two binaries, one repository:
| Binary | Does | Talks to |
|---|---|---|
| `fancy-dns` | The resolver: UDP/TCP/DoT/DoH, cache, blocklists, ACL groups, local zones, access log, metrics, a read-only admin API. Reads `DNSZone`, `ClientGroup`, `BlockList`, `AddressOverride` and `DomainRule` objects with a watch (`-kube`) and keeps a snapshot so it can start without the cluster. | Upstreams; optionally the Kubernetes API and Valkey. |
| `fancy-dns-console` | The web UI and the edit API. The only writer of the cluster objects; authenticates (API key, basic, OIDC with PKCE); proxies every other `/api` route to the resolver. Not on the DNS path. | The resolver's admin API and the Kubernetes API. |
Operational docs: `README.md` (reference), `deploy/MIGRATION.md` (ConfigMap
deployment to cluster objects), `deploy/OUTSIDE-CLUSTER.md` (a backup resolver
and console on a node outside the cluster), `deploy/compose/README.md`
(file-configured Docker backup).
## Verified
`make check` (gofmt, vet, compile, `go test -race ./...`) passes on every
package. The web console typechecks and `oxlint` reports no errors; the
remaining warnings are `set-state-in-effect` in `AppShell`, `Login`, `Domains`
and `Zones` and an unused parameter in `ui/Select.tsx`. The console has no test
runner, so a UI change is verified by `tsc` and by looking at it.
## This round
- **Client-group validation.** `POST /api/clients/validate` (console only)
checks a group without saving it. It applies the same checks as a save, now
reported per field and all at once (`kubesource.CheckGroupAll`,
`kubesource.FieldError`), then runs `acl.Analyze`, which explains the group in
plain English and warns about legal-but-unintended settings: a group buried
under a wider one (and the reverse: a save that buries a later group), CIDRs
with host bits or a huge range, an allowlist, `block_only`, a rate limit of 0
under a default limit, unknown upstreams, domain patterns that never match,
tags with a dot. `test_ip` says which group an address would land in.
Tests: `internal/acl/explain_test.go`, `internal/console/validate_test.go`.
- **Clients page.** Rebuilt around that endpoint: templates for the common
cases, sections ("Which devices?", "What happens to their queries?",
advanced), live validation with errors next to the field, a "What this group
will do" panel, and a try-an-address box. The "Match any client" switch is
gone: the console cannot store a catch-all group (`match.any` alone is
refused for cluster objects), so it could only produce an error.
- **The old help text was wrong, in ways that mattered.** It said
`allow_domains` "skip the blocklists"; in fact a non-empty list is
allowlist-only mode (everything else is blocked). It said `block_only` means
"no recursion"; in fact it is identical to `allow`. It suggested `.lan` as a
reverse-DNS suffix, which never matched anything (see the next item). It said
tags match "ECS/DNS tag metadata"; they are labels of the client's reverse
name. README's `acl` table is corrected to match.
- **`matchPTR` rejected a suffix with a leading dot.** `.lan.` was compared at a
label boundary that the leading dot had already consumed, so it matched
nothing. A leading dot now means "anything under". Pinned in
`TestMatchPTRLabelBoundary`. This changes which clients a group with such a
suffix matches (from nobody to the intended ones).
- **Docs.** This file, `deploy/OUTSIDE-CLUSTER.md`, and removal of the dead
`qlog` and `managed_zones` references from README, the Compose config, and a
comment in `internal/api/zones.go`.
## Decisions waiting for you
1. ~~`block_only`.~~ **Decided: removed from the UI.** It did exactly what `allow`
does. The API and CRD still accept it so stored objects keep working; the
console shows it only on a group that already has it, and the validator warns.
2. ~~Catch-all group.~~ **Decided: no change.** "Everyone else" is the default policy in
the resolver's YAML (`acl.default_policy`). The console does not set it and
cluster objects are never catch-alls.
3. The pinned behaviours below, one at a time.
## Pinned, not fixed
Each has a test that fails when it changes and says what to do then.
- **Unknown config keys are silently ignored** (`TestLoadIgnoresUnknownKeys`).
- **Auth config is frozen at startup.** `api.base_path`, `trusted_proxies` and
the authenticator are read once; a reload re-reads CORS only
(`TestPinnedWrittenAuthConfigIsNotAppliedUntilRestart`,
`TestCORSOriginsAreReadAtHandlerBuild`).
- **DoH clients behind an Ingress all look like the proxy**, because DoH
deliberately ignores `X-Forwarded-For`
(`TestDoHClientIdentityIsTheProxyNotTheForwardedForHeader`).
- **`api.cors_origins: ["*"]` echoes the request's Origin with credentials**
(`TestCORSWildcardEchoesTheOrigin`, `TestCORSPreflight`).
- **A partial bind failure is invisible** to the admin API
(`TestStartReportsAPartialFailureButServes`).
- **A question-less message is answered on UDP and drops the connection on
TCP/DoT** (`TestUDPQuestionlessMessageReachesTheEngine`).
- **`doh.sent` is never bumped** (`TestCountersFollowTheEngineOutcome`).
The README's "Known inconsistencies" section lists more dead config keys and
stale line references; its line numbers have drifted and have not been
re-audited.
## Plan, in order
1. Resolve the decisions above.
2. **Stop hand-writing the console's types.** `web/src/lib/types.ts` is
hand-written and `apiFetch` ends in `JSON.parse(text) as T`; the Go contract
tests make the server authoritative today, but only because someone compared
them once. Generate the types from the Go structs (or emit an OpenAPI
document). `internal/app/dump_shapes_test.go` (`DUMP_SHAPES=1`) is the
stopgap.
3. **Test the console package beyond validation.** `internal/console` has one
test file (the validator). The cluster-consistent merge in `handleClients`
and `putACL` is the part that has caused lost updates before.
4. **Decide whether the shutdown grace belongs in config.** `main` hard-codes
15s for `Stop`, of which 250ms is the UDP drain, and the Kubernetes
`terminationGracePeriodSeconds` is a second copy of the same number.
5. **Look at the Zones and Clients pages in a browser.** Neither the Zones page
nor this round's Clients page has been seen rendered by the person who wrote
it; both are typechecked and built only.
6. Re-audit README "Known inconsistencies" against the code.
## Commands
- `make check-go`: the inner loop (~1s): gofmt, vet, compile (no npm).
- `make check`: `check-go` plus `go test -race ./...`; what CI runs.
- `make smoke`: boots the real binary with the console embedded and checks the
API, metrics and auth.
- `make web-check`: console typecheck and production build.
- `make build`: both binaries with the console embedded (needs npm).
- `make bench`: cache and blocklist benchmarks.
## Test-hygiene traps
These are in `AGENTS.md` (TXT strings over 255 bytes, `EDNS0_NSID` is hex,
`dns.NewRR` needs a TTL, closing a stop channel after `wg.Wait()`, counting
goroutines by stack, `tsc -b` replaying cached diagnostics, `vite build`
deleting `internal/web/dist/.gitkeep`). One more from this round: a Go test that
needs a Kubernetes client should use `dynamic/fake` with
`NewSimpleDynamicClientWithCustomListKinds` and list every GVR in
`kubesource.allGVRs`, or a `List` panics.