7.7 KiB
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 runsacl.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_ipsays 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.anyalone 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 saidblock_onlymeans "no recursion"; in fact it is identical toallow. It suggested.lanas 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'sacltable is corrected to match. matchPTRrejected 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 inTestMatchPTRLabelBoundary. 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 deadqlogandmanaged_zonesreferences from README, the Compose config, and a comment ininternal/api/zones.go.
Decisions waiting for you
Decided: removed from the UI. It did exactly whatblock_only.allowdoes. 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.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.- 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_proxiesand 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.sentis 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
- Resolve the decisions above.
- Stop hand-writing the console's types.
web/src/lib/types.tsis hand-written andapiFetchends inJSON.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. - Test the console package beyond validation.
internal/consolehas one test file (the validator). The cluster-consistent merge inhandleClientsandputACLis the part that has caused lost updates before. - Decide whether the shutdown grace belongs in config.
mainhard-codes 15s forStop, of which 250ms is the UDP drain, and the KubernetesterminationGracePeriodSecondsis a second copy of the same number. - 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.
- Re-audit README "Known inconsistencies" against the code.
Commands
make check-go: the inner loop (~1s): gofmt, vet, compile (no npm).make check:check-goplusgo 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.