Remove the ConfigMap, RPZ and managed-directory editors the cluster objects replace
Zones, address redirects, inline rules, lists and groups are cluster objects now (see the previous commits and deploy/MIGRATION.md), so the mechanisms that held them are gone: * internal/kubezones and the resolver's service-account access to a ConfigMap, the address-overrides read/write path and its poller, the zone-file poller and the Local zones editor with its /api/local-zones routes. * localzone.Managed, the directory of console-written master files, and the blocklist.managed_zones key. The resolver now writes no zone data at all; its zone hooks only list. The shared half of that file (the record validator, the renderer and the round-trip check) stays as zonestate.go, because DNSZone rendering is built on it. Its tests are ported to RenderZone, which keeps the coverage of every editable record type, wildcards, relative owners, the SOA/NS rule and the delegation rule. * Tests of the removed write paths: the contract test now builds its zone from a DNSZone, and a new test pins that every zone write route on the resolver is refused and changes nothing. Standalone installs keep editing master files on disk and SIGHUP, and the YAML-backed domain, client, upstream and settings editors, which have not moved. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2f25463347
commit
467733a7f9
21 files changed
+505
-2312
No files matched your search
@@ -1,7 +1,8 @@
|
||||
# fancy-dns
|
||||
|
||||
A recursive DNS server with blocklists, per-client ACLs, a query log, an
|
||||
embedded admin console and optional clustering over Valkey/Redis.
|
||||
A recursive DNS server with blocklists, per-client ACLs, Prometheus metrics, a
|
||||
JSON access log, a separate admin console and optional clustering over
|
||||
Valkey/Redis.
|
||||
|
||||
* **What it is** — a single Go binary that answers DNS over UDP, TCP,
|
||||
DNS-over-TLS (RFC 7858) and DNS-over-HTTPS (RFC 8484), resolves everything
|
||||
@@ -24,7 +25,8 @@ Each line points at the package that implements it.
|
||||
| Blocklists from hosts, Adblock Plus, plain and RPZ sources, compiled into one immutable set that is swapped atomically | `internal/blocklist` |
|
||||
| Authoritative local data and RPZ policy zones from RFC 1035 master files | `internal/zonefile`, `internal/localzone` |
|
||||
| Per-client ACL groups matched on CIDR, PTR suffix or reverse-name tag, with per-group domain policy, rate limit, concurrency cap, cache and blocklist toggles | `internal/acl` |
|
||||
| Query log in a bbolt ring plus an in-memory live ring, sampling, per-domain counters and an SSE stream | `internal/qlog` |
|
||||
| JSON access log on stdout (blocked and failed queries by default) through a buffer that drops rather than blocks | `internal/accesslog` |
|
||||
| Cluster objects (`DNSZone`, `ClientGroup`, `BlockList`, `AddressOverride`, `DomainRule`) read with a watch, and written by the console | `internal/kubesource`, `internal/kubeedit`, `internal/console` |
|
||||
| Prometheus metrics (`fancydns_*`) | `internal/metrics` |
|
||||
| Admin REST API with API-key / basic / no auth, read-only keys, CORS, per-IP rate limiting and login lockout | `internal/api` |
|
||||
| Admin console embedded in the binary with `go:embed` | `internal/web`, `web/` |
|
||||
@@ -85,9 +87,8 @@ blocklist:
|
||||
enabled: true # lists are OFF unless you say enabled: true
|
||||
refresh: 0s # 0 disables the scheduled refresh
|
||||
|
||||
qlog:
|
||||
enabled: true
|
||||
path: data/queries.db
|
||||
access_log:
|
||||
enabled: true # one JSON line on stdout per blocked or failed query
|
||||
|
||||
api:
|
||||
enabled: true
|
||||
@@ -249,7 +250,6 @@ file.
|
||||
| `blocklist.response_ttl` | duration | `60s` | TTL of synthesised answers |
|
||||
| `blocklist.redirect_name` | name | — | required when `default_action: redirect` |
|
||||
| `blocklist.local_zones` | list of globs | empty | RPZ policy zones and/or local authoritative data, read from master files |
|
||||
| `blocklist.managed_zones` | path | empty | the only directory the admin console may **write** zone data into, one `<apex>.zone` per zone; empty keeps the console's Zones page read-only |
|
||||
| `blocklist.block_entries` / `allow_entries` | list of lines | empty | inline rules, compiled as `inline:block` / `inline:allow` and applied before every list |
|
||||
| `blocklist.parallelism` | int | `4` | **unused** — compilation is single-threaded |
|
||||
| `blocklist.lists[].name` | string | — | required, unique |
|
||||
@@ -284,25 +284,25 @@ file.
|
||||
| `acl.groups[].skip_cache` | bool | `false` | the group only gets fresh lookups |
|
||||
| `acl.groups[].rate_limit` | int | inherit | queries/second for this group |
|
||||
| `acl.groups[].max_concurrent` | int | `0` (off) | in-flight cap for this client |
|
||||
| `acl.groups[].log_queries` | bool | `false` | log every query from this group regardless of `qlog.sample_rate` |
|
||||
| `acl.groups[].log_queries` | bool | `false` | log every query from this group regardless of `access_log` selection |
|
||||
| `acl.groups[].upstreams` | list of addrs | empty | restrict this group to named upstreams; a list that matches nothing falls back to the whole pool |
|
||||
|
||||
### qlog
|
||||
### access_log
|
||||
|
||||
One JSON line on stdout per query whose outcome is selected. The resolver keeps
|
||||
no query log of its own: a log collector indexes these lines, and the Prometheus
|
||||
metrics count everything. Lines go through a bounded buffer and are dropped, and
|
||||
counted in `fancydns_access_log_dropped_total`, if the stream falls behind, so
|
||||
logging can never stall a query.
|
||||
|
||||
| Key | Type | Default | What it does |
|
||||
|---|---|---|---|
|
||||
| `qlog.enabled` | bool | `true` | when off the bbolt file is not opened (the live ring and counters stay) |
|
||||
| `qlog.path` | path | `data/queries.db` | bbolt ring file |
|
||||
| `qlog.max_entries` | int | `200000` | ring bound; the oldest entries are dropped |
|
||||
| `qlog.flush_interval` | duration | `1s` | batches writes onto a background goroutine |
|
||||
| `qlog.sample_rate` | 0–1 | `1.0` | sampling for ordinary queries; blocked/error/cached logging is governed by the flags below |
|
||||
| `qlog.log_blocked` | bool | `true` | always log blocks |
|
||||
| `qlog.log_cached` | bool | `true` | log cache hits (set `false` on a busy instance) |
|
||||
| `qlog.log_errors` | bool | `true` | always log SERVFAIL and friends |
|
||||
| `qlog.log_client_names` | bool | `false` | **unused** |
|
||||
| `qlog.live_buffer` | int | `2000` | in-memory ring that feeds the UI and the SSE stream |
|
||||
| `qlog.top_domains` | int | `5000` | number of per-domain counters kept |
|
||||
| `qlog.top_ttl` | duration | `5m` | how long those counters are aggregated |
|
||||
| `access_log.enabled` | bool | `true` | |
|
||||
| `access_log.log_blocked` | bool | `true` | blocklist hits and redirects |
|
||||
| `access_log.log_errors` | bool | `true` | SERVFAIL, drops and ACL refusals |
|
||||
| `access_log.log_cached` | bool | `false` | also cache hits |
|
||||
| `access_log.sample_rate` | 0–1 | `0` | fraction of ordinary answers to log as well; for a debugging session |
|
||||
| `access_log.buffer` | int | `4096` | lines that may wait for the stream before dropping |
|
||||
|
||||
### api
|
||||
|
||||
@@ -432,51 +432,27 @@ records mean and where they are matched:
|
||||
one-label and deeper wildcards, SRV, TXT). `deploy/zones/local.rpz.zone.example`
|
||||
is a policy zone.
|
||||
|
||||
**Two zone pages, two ownership models.** **Local zones** edits the RFC 1035
|
||||
master files themselves (in Kubernetes, one file per zone in the
|
||||
`fancy-dns-zones` ConfigMap; standalone, the files named by
|
||||
`blocklist.local_zones`). **Zones** is the record-level view and the only one
|
||||
that writes a directory of its own, `blocklist.managed_zones`. They are
|
||||
separate on purpose: the first edits what you wrote, the second writes what it
|
||||
owns. Neither rewrites the other's files.
|
||||
|
||||
**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.
|
||||
**Zones the console writes.** `blocklist.managed_zones` names a directory the
|
||||
admin console owns: one `<apex>.zone` per zone, holding the SOA, the apex NS
|
||||
set and whatever records were added there. It is a zone source like any other —
|
||||
`localzone.Load` compiles it alongside `local_zones` — so a record added from
|
||||
the console is served on the next query, with no reload and no restart. The
|
||||
master files in `local_zones` are never rewritten: they are listed in the
|
||||
console's Zones page and shown as read-only. Without `managed_zones` the page
|
||||
is a browser and nothing else.
|
||||
|
||||
Four rules the writer holds to, all of them because the alternative is a zone
|
||||
**Zones in Kubernetes are `DNSZone` objects.** With `-kube` the resolver watches
|
||||
`DNSZone` objects and serves them next to the master files in `local_zones`; the
|
||||
console (`fancy-dns-console`) is what writes them. The resolver itself writes no
|
||||
zone data. A `DNSZone` is rendered into a master file and re-parsed before it is
|
||||
served, and one that does not render is rejected without touching the zones that
|
||||
already work. Rules the renderer holds to, all because the alternative is a zone
|
||||
that silently stops answering:
|
||||
|
||||
* A record line is validated by the project's own master-file parser, so an RDATA
|
||||
name written without a trailing dot means the same thing here as in a zone
|
||||
file, and the value the API echoes is the value the file holds.
|
||||
* What the writer renders is re-parsed and compared with what it meant to write
|
||||
before anything touches the disk. A round-trip that loses a record is a
|
||||
writer bug, caught there rather than by an operator noticing an empty zone.
|
||||
* A record is validated by the project's own master-file parser, so an RDATA name
|
||||
written without a trailing dot means the same thing here as in a zone file.
|
||||
* What is rendered is re-parsed and compared with what it meant to write. A
|
||||
round-trip that loses a record is a renderer bug, caught there.
|
||||
* A zone needs an SOA (or it cannot answer NXDOMAIN/NODATA) and an NS record at
|
||||
the apex (or it delegates nothing and has no server to name), so a brand new
|
||||
zone is started with one of the two. The SOA's primary follows the apex NS
|
||||
set, and the serial moves on every write.
|
||||
* A record at or below a delegation is refused: the store answers those names as
|
||||
delegated and never looks at the zone file, so it would be data that looks
|
||||
alive and never answers.
|
||||
the apex. The SOA's primary follows the apex NS set, and its serial is the
|
||||
object's generation, so every replica serves identical bytes.
|
||||
* A record at or below a delegation is refused: those names are answered as
|
||||
delegated and the record would look alive and never answer.
|
||||
|
||||
A record is written by rewriting the whole file, so a file under
|
||||
`managed_zones` is the console's to own; hand-maintained data belongs in
|
||||
`local_zones`. Each file says so in its header.
|
||||
To move an existing deployment onto these objects, see
|
||||
[deploy/MIGRATION.md](deploy/MIGRATION.md). A standalone install keeps its zones
|
||||
as master files on disk and reloads with SIGHUP.
|
||||
|
||||
## Blocklists
|
||||
|
||||
@@ -536,7 +512,6 @@ is on.
|
||||
| POST | `/api/auth/login` | — | exchange an API key or user/password for a bearer token (public) |
|
||||
| POST | `/api/auth/logout` | yes | revoke the caller's bearer token |
|
||||
| GET | `/api/whoami` | — | caller's name, mode, `read_only`, `can_write` |
|
||||
| GET | `/api/dashboard?range=…` | — | aggregated counters and the rolling series |
|
||||
| GET | `/api/status` | — | version, uptime, config generation/last error, per-component status, listeners |
|
||||
| GET | `/api/blocklists` | — | compiled state, per-source metadata, errors |
|
||||
| POST | `/api/blocklists/refresh` | yes | recompile now; body `{"names":[…]}` or empty for everything |
|
||||
@@ -546,22 +521,19 @@ is on.
|
||||
| POST | `/api/domains` | yes | add an inline rule |
|
||||
| POST | `/api/dns/test` | — | run one query through the live pipeline (`{"name","type"}`) and report the rcode, action, answers, upstream and latency — the console's "is my DNS answering" test, and a failed lookup is a 200 with a result, not a failed request |
|
||||
| DELETE | `/api/domains/{name…}?action=&list=` | yes | remove a rule |
|
||||
| GET | `/api/zones` | — | every local zone with its records, delegations, source files, the managed directory, whether it is writable, the writable record types, and why the store is stale (`error`) |
|
||||
| GET | `/api/zones` | — | every local zone with its records, delegations, source files, whether each is an editable cluster object, the record types the console writes, and why the store is stale (`error`) |
|
||||
| POST | `/api/zones/records` | yes | add a record (`{"zone","name","type","ttl","value"}`; a name without a trailing dot is relative to the zone) |
|
||||
| PUT | `/api/zones/records` | yes | replace the whole RRset of one name and type |
|
||||
| DELETE | `/api/zones/records?zone=&name=&type=&value=` | yes | remove one record, or the RRset when `value` is empty |
|
||||
| POST | `/api/zones/delegations` | yes | delegate a name to other servers (`{"zone","name","targets":[…],"ttl"}`); the delegation's targets become the apex NS set of a zone that has none |
|
||||
| DELETE | `/api/zones/delegations?zone=&name=` | yes | remove a cut |
|
||||
| DELETE | `/api/zones?zone=` | yes | delete a zone the console owns, so its names go back to whatever answered for them before; refused for a zone with records outside the managed directory |
|
||||
| DELETE | `/api/zones?zone=` | yes | delete a `DNSZone`, so its names go back to whatever answered for them before (console only) |
|
||||
| GET | `/api/clients` | — | ACL groups and the default policy |
|
||||
| PUT | `/api/clients` | yes | replace the ACL (`{"groups":[…],"default_policy":…}`); validates, writes the file, swaps, broadcasts |
|
||||
| GET | `/api/upstreams` | — | pool state, health, latency, breaker |
|
||||
| PUT | `/api/upstreams` | yes | replace policy/servers/bootstraps/attempts/threshold (cooldown is preserved) |
|
||||
| GET | `/api/logs?q=&action=&client=&rcode=&blocked=&since=&limit=&offset=` | — | page the query log |
|
||||
| GET | `/api/logs/stream` | — | Server-Sent Events: `ready`, `entry`, `stats` and `: ping` keep-alives |
|
||||
| POST | `/api/logs/clear` | yes | empty the query log |
|
||||
| GET | `/api/cache` | — | cache statistics |
|
||||
| GET | `/api/cache/top?limit=` | — | hottest cached names |
|
||||
| GET | `/api/links` | — | the configured `api.links` (Grafana, log view, metrics) |
|
||||
| POST | `/api/cache/flush` | yes | drop the cache (also done after a rule change) |
|
||||
| GET | `/api/settings` | — | file YAML, effective YAML, defaults YAML, writability |
|
||||
| PUT | `/api/settings` | yes | validate, write (atomic, mode 0600), bump `version`, swap, broadcast; needs `api.allow_config_write` |
|
||||
|
||||
Reference in new issue
Block a user