feat(dns): PiHole answers an explicit LAN name list, not a zone wildcard
Public names are proxied CNAMEs to the Cloudflare Tunnel since PR #156 and work from the LAN, so they should resolve publicly there too. Replace the address=/${BASE_DOMAIN}/ wildcard with explicit entries: the LAN-only names (derived from every controller:none HTTPRoute), gitea/nextcloud (off Cloudflare's 100 MB / 100 s limits), keycloak/auth, and the L4 VIPs. Add L0 check pihole-lan-names, mutation-tested. Correct docs that still described public names as CNAME to wan. and the cloudflared entrypoint comment. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
020e736541
commit
8fe8d5df8a
16 files changed
+513
-60
No files matched your search
@@ -0,0 +1,97 @@
|
||||
---
|
||||
date: 2026-11-09
|
||||
branch: feat/pihole-explicit-lan-names
|
||||
title: PiHole answers an explicit LAN name list instead of a zone wildcard (public names resolve to Cloudflare from the LAN)
|
||||
---
|
||||
|
||||
## What changed
|
||||
|
||||
**`catalog/pihole.yaml` → `apps/wave-0/pihole.yaml`.** `FTLCONF_misc_dnsmasq_lines` no longer carries
|
||||
`address=/${BASE_DOMAIN}/${GATEWAY_VIP}`. Every name not listed is now forwarded to 8.8.8.8/8.8.4.4 and
|
||||
gets the public answer. The final list (`local-ttl=60` kept):
|
||||
|
||||
| Name (`.${BASE_DOMAIN}`) | Answer | Why |
|
||||
|---|---|---|
|
||||
| jackett, pihole, plex, qbittorrent, radarr, semaphore, sonarr, headlamp | `${GATEWAY_VIP}` | LAN-only: every HTTPRoute with `external-dns.alpha.kubernetes.io/controller: none`, derived from the tree. No public record, tunnel 404s them; this entry is their only path |
|
||||
| gitea, nextcloud | `${GATEWAY_VIP}` | public, but Cloudflare's proxy caps bodies at 100 MB / responses at 100 s (git push, LFS, uploads) |
|
||||
| keycloak, auth | `${GATEWAY_VIP}` | keep the OIDC flow of the LAN-path apps on one path |
|
||||
| unifi | `${UNIFI_L4_VIP}` | L4 VIP, not behind the Gateway |
|
||||
| crafty-mc | `${CRAFTY_L4_VIP}` | L4 VIP, not behind the Gateway |
|
||||
| pihole-dns | `${PIHOLE_DNS_VIP}` | the resolver's own address |
|
||||
| plex-vip | `${PLEX_L4_VIP}` | kept, see below |
|
||||
|
||||
Not listed, so they resolve to Cloudflare from the LAN: the apex, grafana, crafty, seerr.
|
||||
|
||||
**The derived LAN-only set matches the brief's list exactly** (8 names; the tree was walked, not the
|
||||
list trusted). The `runbooks/serving` route has no `hostnames:` but is not disowned (it inherits the
|
||||
apex, public).
|
||||
|
||||
**plex-vip is kept.** Nothing advertises it any more (`ADVERTISE_IP` is a `plex.direct` name since
|
||||
2026-10-05), but it is not unused: it is Plex's `customCertificateDomain` (`CERTDOMAIN`, Secret
|
||||
`plex-vip-tls`), the name of the tcp/32400 VIP, and the 404'd name in the tunnel config. It has no
|
||||
public record, so this entry is its only resolution.
|
||||
|
||||
**New L0 check `conformance/tests/l0-static/pihole-lan-names`** (`scope: whole-repo`, so picked up by
|
||||
the runner's scope selection with no other wiring). Asserts: (a) no zone-wide entry (zone, `#`, or any
|
||||
parent of the zone); (b) every `controller: none` hostname → `${GATEWAY_VIP}`, and a disowned route
|
||||
with no `hostnames:` fails; (c) every tunnel-404 hostname has an entry, with the L4 names (derived
|
||||
from Services carrying both the external-dns hostname and an lbipam VIP) on their own VIP; (d) gitea
|
||||
and nextcloud → `${GATEWAY_VIP}`; plus catalog == rendered value and a built-in mutation self-test.
|
||||
|
||||
**Stale docs corrected:** `platform/pm-agent/README.md`, `runbooks/pm-agent.md`,
|
||||
`platform/externaldns/README.md`, `platform/cloudflared/README.md`, `platform/gateway/gateway.yaml`
|
||||
(comment), `platform/headlamp/httproute.yaml` (comment), `apps/vms/unifi/README.md`, `README.md`,
|
||||
`schemas/renderer-contract.md`; `runbooks/cloudflare-tunnel.md` gains the two consequences below.
|
||||
`platform/cloudflared/deployment.yaml`: the image entrypoint comment was wrong (comment-only).
|
||||
|
||||
## What I verified
|
||||
|
||||
- **Render:** the baseline `render.py --no-image-automation catalog/pihole.yaml` was byte-identical to
|
||||
the committed `apps/wave-0/pihole.yaml` before the change, and the file was replaced by that
|
||||
command's output after it; `--emit-waves` was not run.
|
||||
- **Mutation, against the real files (10 mutants, each rc 1 with the right assertion, each restored):**
|
||||
(a) zone wildcard restored; (b) headlamp and sonarr entries removed; (b) a disowned route with no
|
||||
`hostnames:`; (c) a new tunnel-404 host with no entry; (c) pihole-dns removed; (c) unifi pointed at
|
||||
the Gateway; (d) gitea removed; (d) nextcloud repointed; (0) rendered value drifted from the catalog.
|
||||
- **Rollout mechanics, measured live (read-only):** StatefulSet `updateStrategy` RollingUpdate
|
||||
partition 0, `podManagementPolicy: Parallel`, no `minReadySeconds`, 2 replicas, PDB minAvailable 1.
|
||||
The last rollout (2026-11-08 06:47–06:48Z) replaced pihole-1 then pihole-0: FTL logged "started" ~1 s
|
||||
after the container, the pod went Ready ~16 s after the container, and pihole-0 started ~9 s later.
|
||||
- **Expected LAN DNS blip:** no total outage; one replica answers throughout. Each of the two pod
|
||||
replacements can lose a few UDP queries for ~1–2 s while the terminating pod leaves the Service
|
||||
(clients retry). For roughly 30–60 s the two pods answer differently for the apex, grafana, crafty
|
||||
and seerr; both answers work. Cached answers age out in 60 s (`local-ttl`).
|
||||
- **Live DNS before the change:** @8.8.8.8 returns Cloudflare (104.21.51.91 / 172.67.178.27) for the
|
||||
apex, grafana, seerr, crafty and gitea, and nothing for jackett; @192.168.2.23 returns 192.168.2.20
|
||||
for all of them (the wildcard).
|
||||
- **Cloudflared image:** `skopeo inspect --config` on the pinned digest (amd64): Entrypoint
|
||||
`["cloudflared"]`, Cmd `["version"]`, User `65532:65532`.
|
||||
|
||||
## What I did NOT verify
|
||||
|
||||
- **The post-merge behaviour.** Nothing here ran against a live PiHole with the new list; the
|
||||
expected answers are from dnsmasq semantics and the tree. After reconcile, check
|
||||
`dig @192.168.2.23` for one name per row, and grafana returning Cloudflare.
|
||||
- **Whether every LAN client path works via Cloudflare.** Measured 2xx/3xx through Cloudflare by the
|
||||
brief, not re-measured from a LAN client here.
|
||||
- **The FTL DNS-listening moment.** "FTL started" is a log line, not a successful query; the rollout
|
||||
timing above assumes DNS is up shortly after it.
|
||||
|
||||
**Accepted consequences, stated plainly:**
|
||||
- The apex, grafana, crafty and seerr now depend on the internet and the tunnel from the LAN too. An
|
||||
ISP or Cloudflare outage, or stopping the connectors, makes them unreachable from the LAN where they
|
||||
previously kept working. If grafana in particular should survive an internet outage, add it to the
|
||||
list (one entry; the check permits it).
|
||||
- The tunnel rollback (revert to `wan.`) now also requires restoring the wildcard; recorded in
|
||||
`runbooks/cloudflare-tunnel.md`.
|
||||
|
||||
**Flagged, not changed:**
|
||||
- The `pihole` container has no readiness probe; only the `oauth2-proxy` sidecar does, so "Ready" says
|
||||
nothing about DNS. The measured margin (FTL ~1 s, Ready ~16 s) makes this benign in a normal rollout
|
||||
but a slow FTL start would not hold the rollout back. A DNS readiness probe needs a schema/renderer
|
||||
decision.
|
||||
|
||||
**Stale statements left alone:** `platform/coredns/coredns-custom.yaml` header ("both resolve the whole
|
||||
zone ... by wildcard", brief said not to touch CoreDNS); `clusters/base/cluster-vars.yaml` comments
|
||||
(sensitive path); `schemas/renderer-contract.md` §`exposure` first bullets (manual edge port-forward
|
||||
wording); historical `infra/PROD-PROVISIONING.md`, `platform/externaldns/README.md` TODO list.
|
||||
Reference in new issue
Block a user