1 parent
289a8cc2d8
commit
66f36cfac6
19 files changed
+1905
-817
No files matched your search
@@ -275,15 +275,15 @@ file.
|
||||
| `acl.groups` | list | empty | evaluated top to bottom, first match wins |
|
||||
| `acl.groups[].name` / `comment` | string | — | `name` required and unique |
|
||||
| `acl.groups[].match.cidrs` | list of CIDR | empty | matched against the client address (v4 or v6) |
|
||||
| `acl.groups[].match.ptr_suffixes` | list | empty | matched against reverse-DNS names of the client, on a label boundary; a missing trailing dot is added |
|
||||
| `acl.groups[].match.ptr_suffixes` | list | empty | matched against reverse-DNS names of the client, on a label boundary; a missing trailing dot is added. `lan.example.` covers the name and everything under it; `.lan.example.` (leading dot) covers only what is under it |
|
||||
| `acl.groups[].match.tags` | list | empty | labels derived from the reverse name (e.g. `laptop` for `laptop.lan.internal.`) |
|
||||
| `acl.groups[].match.any` | bool | `false` | catch-all; must be the last group |
|
||||
| `acl.groups[].policy` | `allow` \| `deny` \| `block_only` | `allow` | `deny` answers REFUSED and wins over the local zone |
|
||||
| `acl.groups[].allow_domains` / `deny_domains` | list | empty | per-group name policy with the blocklist wildcards; `deny_domains` is checked first |
|
||||
| `acl.groups[].skip_blocklist` | bool | `false` | the group opts out of blocklist filtering |
|
||||
| `acl.groups[].policy` | `allow` \| `deny` \| `block_only` | `allow` | `deny` answers REFUSED and wins over the local zone. **`block_only` behaves exactly like `allow` today**: both resolve normally and both apply the blocklists |
|
||||
| `acl.groups[].allow_domains` / `deny_domains` | list | empty | per-group name policy with the blocklist wildcards (`*.x` and `**.x` both cover `x` itself here). **A non-empty `allow_domains` is allowlist-only mode**: every name not in it is blocked for the group; it does not exempt names from the blocklists (that is `skip_blocklist`). `deny_domains` is checked first and wins |
|
||||
| `acl.groups[].skip_blocklist` | bool | `false` | the group opts out of blocklist filtering (local zones and address overrides are unaffected) |
|
||||
| `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[].max_concurrent` | int | `0` (off) | in-flight cap for this client; the excess get SERVFAIL |
|
||||
| `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 |
|
||||
|
||||
@@ -521,6 +521,7 @@ 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 |
|
||||
| POST | `/api/clients/validate` | — | console only. Check a client group without saving it: `{"group":{…},"test_ip":"…"}` answers `errors` (what blocks a save, with the field), `warnings` (legal but probably unintended: a group buried under a wider one, a rate limit of 0 under a default limit, an allowlist), `notes`, a plain-English `summary`, the group's `position` in the evaluation order, and which group `test_ip` would land in |
|
||||
| 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 |
|
||||
@@ -580,7 +581,7 @@ docker run --rm -p 53:53/udp -p 53:53/tcp -p 8080:8080 \
|
||||
The `Dockerfile` builds the console in a Node stage, then a static
|
||||
`CGO_ENABLED=0` binary with it embedded, and ships it on
|
||||
`gcr.io/distroless/static-debian12:nonroot`. It declares `VOLUME
|
||||
/var/lib/fancy-dns` (cache snapshot, query log, downloaded lists) and copies
|
||||
/var/lib/fancy-dns` (cache snapshot, downloaded lists, the saved cluster snapshot) and copies
|
||||
`deploy/config.example.yaml` plus `deploy/zones/` to `/etc/fancy-dns/`; there
|
||||
is no `config.yaml` in the image, so its `CMD` (`-config
|
||||
/etc/fancy-dns/config.yaml`) requires you to mount one.
|
||||
@@ -626,7 +627,7 @@ Things to know before you apply it:
|
||||
* **Per-pod PVC sizing.** The StatefulSet uses a 4 Gi `ReadWriteOnce`
|
||||
template. `cache.size` is an *entry count*, not bytes: a packed answer is
|
||||
roughly 300–600 bytes on the wire and nearer 1 kB resident, so 500k entries
|
||||
lands around 500 MB, and `qlog.max_entries` is the other consumer. The PVC
|
||||
lands around 500 MB, and the downloaded blocklists are the other consumer. The PVC
|
||||
cannot be shared between replicas, and being a StatefulSet also means an HPA
|
||||
cannot drive the replica count.
|
||||
* Probes all hit `/api/healthz` on the admin port, which is unauthenticated on
|
||||
@@ -771,72 +772,70 @@ Found while writing this file; each is a to-do, not something fixed here.
|
||||
6. **`internal/config/config.go:229` — `cache.prewarm` is dead.**
|
||||
`App.prewarm` exists (`internal/app/app.go:220`) but nothing calls it, while
|
||||
`deploy/config.example.yaml:97` ships `prewarm: []`.
|
||||
7. **`internal/config/config.go:358` — `qlog.log_client_names` is dead.**
|
||||
8. **`internal/config/config.go:373` — `cluster.peer_sync` is dead**, and
|
||||
7. **`internal/config/config.go:373` — `cluster.peer_sync` is dead**, and
|
||||
**`cluster.advertise`** (`:371`, "the address other instances dial for the
|
||||
admin API") is only echoed in heartbeats: no code dials a peer.
|
||||
9. **`internal/config/config.go:411` — `api.allow_blocklist_write` is dead.**
|
||||
8. **`internal/config/config.go:411` — `api.allow_blocklist_write` is dead.**
|
||||
Only `allow_config_write` is enforced (`internal/api/handlers.go:
|
||||
handleSettingsPut`).
|
||||
10. **`internal/config/config.go:418` — `metrics.path` is dead.** The route is
|
||||
9. **`internal/config/config.go:418` — `metrics.path` is dead.** The route is
|
||||
built from `api.base_path` (`internal/api/api.go:routes`:
|
||||
`GET <base>metrics`), so `metrics.path: /metrics` in
|
||||
`deploy/config.example.yaml:196` and in `deploy/k8s/00-config.yaml` has no
|
||||
effect; the endpoint is `/api/metrics`.
|
||||
11. **`internal/config/config.go` — `dns.doh.transport.alt_svc` (`:157`) and
|
||||
10. **`internal/config/config.go` — `dns.doh.transport.alt_svc` (`:157`) and
|
||||
`.mode` (`:156`), `dns.dot.upstream` (`:129`), `dns.edns0.cookie_support`
|
||||
(`:166`), `dns.edns0.client_subnet` (`:162`), `dns.edns0.ipv4_bits` /
|
||||
`ipv6_bits` (`:163-164`) are all dead.** `serveDoH` always speaks h2 with an
|
||||
h2c fallback (no HTTP/3, no Alt-Svc), there is no encrypted-upstream DoT
|
||||
switch (`upstream.servers[].proto: tls` is the way to get that), and ECS is
|
||||
forwarded verbatim whenever `dns.edns0.preserve` is on.
|
||||
12. **`internal/config/config.go:266` — `blocklist.parallelism` is dead**;
|
||||
11. **`internal/config/config.go:266` — `blocklist.parallelism` is dead**;
|
||||
`RefreshAll` compiles single-threaded.
|
||||
13. **`internal/config/config.go` (`ListSource.Timeout`) and
|
||||
12. **`internal/config/config.go` (`ListSource.Timeout`) and
|
||||
`server.timeouts.list_fetch`** — the fetch timeout is never applied:
|
||||
`blocklist.NewManager` hardcodes a 120 s `http.Client` and
|
||||
`internal/blocklist/manager.go:fetch` never consults `l.Timeout`.
|
||||
14. **`internal/config/config.go` (`UpstreamBootstrap.Refresh`)** — bootstrap
|
||||
13. **`internal/config/config.go` (`UpstreamBootstrap.Refresh`)** — bootstrap
|
||||
names are resolved once per pool rebuild; `refresh` never triggers a
|
||||
re-resolution.
|
||||
15. **`internal/config/config.go` (`Valkey.TLS`)** — `cluster.valkey.tls` is
|
||||
14. **`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`.
|
||||
17. **`internal/zonefile/zonefile.go:1-8` and `:238-243` — the package comment
|
||||
16. **`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
|
||||
`Options.MaxIncludeDepth` is never set by any caller. Either implement it or
|
||||
correct the comment and drop the field.
|
||||
18. **`Makefile:113` — `make run` claims it runs
|
||||
17. **`Makefile:113` — `make run` claims it runs
|
||||
`deploy/config.example.yaml` "on unprivileged ports", but that file binds
|
||||
`0.0.0.0:53` and `[::]:53`** (`deploy/config.example.yaml:23-24`).
|
||||
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.
|
||||
20. **`deploy/k8s/20-service.yaml` — the load-balancer comment says to leave
|
||||
19. **`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
|
||||
port is on the separate `fancy-dns-web` ClusterIP Service, which the LB
|
||||
cannot health-check. Pick a provider with UDP health checks or use DNS-level
|
||||
failover.
|
||||
21. **`deploy/k8s/00-config.yaml` enables `dot` and `doh` against
|
||||
20. **`deploy/k8s/00-config.yaml` enables `dot` and `doh` against
|
||||
`/etc/fancy-dns/tls/tls.crt` while the Secret it mounts ships empty
|
||||
`tls.crt` / `tls.key`.** Applying the kustomization unchanged leaves both
|
||||
listeners unable to build a `tls.Config`; a comment says so, but an example
|
||||
that cannot come up as-is is still a footgun.
|
||||
22. **`api.rate_limit.per_minute` defaults to `60` in `config.Default()` but
|
||||
21. **`api.rate_limit.per_minute` defaults to `60` in `config.Default()` but
|
||||
`deploy/config.example.yaml:189` says `600`**, and
|
||||
`deploy/config.example.yaml` sets `qlog.log_cached: false` where the default
|
||||
is `true`. Harmless, but the example is not a dump of the defaults and
|
||||
readers will assume it is.
|
||||
23. **`api.auth.mode: apikey` with an emptied `api_keys` list and
|
||||
`deploy/config.example.yaml` is not a dump of the defaults, and readers will
|
||||
assume it is.
|
||||
22. **`api.auth.mode: apikey` with an emptied `api_keys` list and
|
||||
`trust_loopback: true` is indistinguishable from `none`.** `Validate` only
|
||||
rejects the combination with `trust_loopback: false`; the unsafe case is
|
||||
reported solely as a warning from `POST /api/settings/validate`.
|
||||
24. **`config.Load` does not use `yaml.Decoder.KnownFields`**, so unknown keys
|
||||
23. **`config.Load` does not use `yaml.Decoder.KnownFields`**, so unknown keys
|
||||
are ignored silently. A settled decision (pinning, not changing, the
|
||||
behaviour); `TestLoadIgnoresUnknownKeys` in `internal/config` holds it.
|
||||
25. **RESOLVED — an RPZ record did not cover the name it names.**
|
||||
24. **RESOLVED — an RPZ record did not cover the name it names.**
|
||||
`rpzRule` gave a bare owner name the subdomain form (`*.name`), so
|
||||
`analytics.example.org.` in a policy zone blocked
|
||||
`cdn.analytics.example.org.` and left `analytics.example.org.` resolving
|
||||
@@ -849,7 +848,7 @@ Found while writing this file; each is a to-do, not something fixed here.
|
||||
`TestExampleRPZZoneCoversTheApexItNames`, including the `rpz-passthru.`
|
||||
half — the same gap meant a passthru record did not allowlist its own
|
||||
domain.
|
||||
26. **`internal/api/api.go:288-317` — `api.cors_origins: ["*"]` echoes the
|
||||
25. **`internal/api/api.go:288-317` — `api.cors_origins: ["*"]` echoes the
|
||||
request's `Origin` *and* sets `Access-Control-Allow-Credentials: true`.**
|
||||
That is deliberate (a literal `*` is ignored by browsers when credentials
|
||||
are involved, so echoing is the only way a wildcard can work with a session
|
||||
|
||||
Reference in new issue
Block a user