Add UI editing for local DNS zones
build / build-push (push) Successful in 29s

Local zones were only editable as mounted files, so operators could not manage DNS entries from the console. Add a Local zones editor backed by a shared Kubernetes ConfigMap, validate master files before saving, scope the pod service account to that ConfigMap, and reload changed zones on every replica while flushing stale cache entries.
This commit is contained in:
mats committed 2026-09-28 08:50:50 +02:00
1 parent 2a7ac824f7
commit 32f940b763
18 files changed
+727 -36

No files matched your search

+20 -20
View File
@@ -397,13 +397,12 @@ local traffic counted correctly — see STATUS.md for the change note.
but there is no zone transfer, so nothing calls it.
**Zone name from a path.** `localzone.Load` derives each file's origin with
`zonefile.OriginFromPath`, which takes the base name and truncates it at the
first dot: `zones/lan.zone` and `lan.zone` both become the origin `lan.`
(the doc comment on that function says `ads.zone.`, which is wrong — see
[Known inconsistencies](#known-inconsistencies)). An explicit `$ORIGIN` inside
the file wins, because `Parse` starts from that derived origin and the
directive replaces it. Records that fall outside their own zone are dropped
(`localzone.add` returns false).
`zonefile.OriginFromPath`. Legacy names truncate at the first dot, so
`zones/lan.zone` becomes `lan.`. The console marks fully-qualified zone names
with `zone--`, so `zone--example.com.zone` becomes `example.com.`. An explicit
`$ORIGIN` inside the file wins, because `Parse` starts from that derived origin
and the directive replaces it. Records that fall outside their own zone are
dropped (`localzone.add` returns false).
The practical consequence: a *relative* name is qualified with the file's base
name, not with the zone you had in mind. In `local.example.zone` (origin
@@ -431,6 +430,15 @@ records mean and where they are matched:
one-label and deeper wildcards, SRV, TXT). `deploy/zones/local.rpz.zone.example`
is a policy zone.
**Edit zones in the console.** Open **Local zones** to create a zone or edit its
RFC 1035 master-file records. In Kubernetes, the editor stores each file in the
`fancy-dns-zones` ConfigMap; both replicas watch the projected files and reload
them automatically. The API validates every file and its record owners before
saving. The pod's service account can read and patch only this ConfigMap. The
ConfigMap's `data` is intentionally left out of the GitOps manifest so console
edits persist across reconciliation. For a standalone install, manage zone
files on disk and reload the configuration.
## Blocklists
A source is compiled into rules, and every source is merged into one immutable
@@ -662,11 +670,12 @@ Things to know before you apply it:
* **No zone transfer.** AXFR/IXFR are not implemented: the master-file parser
knows the record types, but nothing serves or accepts a transfer, and
`zonefile.SerialNext` is unused.
* **No dynamic update.** An UPDATE message passes the engine's shape check
* **No RFC 2136 dynamic update.** An UPDATE message passes the engine's shape check
(`validate` tolerates `OpcodeUpdate`) and is then handled as an ordinary
query. There is no RFC 2136 handling, no journal and no NOTIFY. A local zone
only changes when the file changes and the config is reloaded (SIGHUP,
`POST /api/settings/reload`, or a restart).
query. There is no wire-protocol update handling, journal or NOTIFY. Local
zones are editable from the console's **Local zones** page. Kubernetes pods
reload changed zone files automatically; standalone file edits need a config
reload or restart.
* **No HTTP/3** for DoH despite `dns.doh.transport.mode` / `alt_svc`; the
listener is h2 with an h2c fallback.
* **No certificate automation**: certificates are files on disk, one per
@@ -774,11 +783,6 @@ Found while writing this file; each is a to-do, not something fixed here.
15. **`internal/config/config.go` (`Valkey.TLS`)** — `cluster.valkey.tls` is
never applied; `internal/cluster/cluster.go:dial` builds a plain
`redis.Client` with no `TLSConfig`.
16. **`internal/zonefile/zonefile.go:525-526` — the `OriginFromPath` doc
comment is wrong.** It claims `"rpz/ads.zone"` and `"ads.zone"` both become
`"ads.zone."`; the code truncates at the first dot, so both become `"ads."`.
The behaviour is what `localzone.Load` and `blocklist.parseRPZFiles` rely
on — fix the comment.
17. **`internal/zonefile/zonefile.go:1-8` and `:238-243` — the package comment
advertises `$INCLUDE`, the parser rejects it** ("`$INCLUDE … is not
supported; use blocklist.local_zones or a list source instead`"), and
@@ -790,10 +794,6 @@ Found while writing this file; each is a to-do, not something fixed here.
Unprivileged use needs the same `sed` rewrite the CI smoke test does
(`.github/workflows/ci.yml`, "Smoke test the real binary"), or a port you
can bind.
19. **`deploy/k8s/10-deployment.yaml` — the comment tells the reader to manage
zones "through the admin API (`/api/zones`)", which does not exist.** There
is no zone endpoint in `internal/api/api.go:routes`; local zones are edited
on disk.
20. **`deploy/k8s/20-service.yaml` — the load-balancer comment says to leave
the LB health check on the HTTP admin port, but the `fancy-dns-dns`
LoadBalancer Service publishes only 53/UDP, 53/TCP and 853/TCP.** The admin