v0.21.0: agent-managed split-horizon LAN resolver (internal/lanresolver)

Host-side dnsmasq the agent manages so LAN clients reach their guest directly
(same hostname + real wildcard cert, no Cloudflare hairpin). Renders local=/
+address=/ per customer (AAAA->NODATA via authoritative zone, wildcard A ->
live guest IP), forwards everything else. Manager ensures dnsmasq+base config,
discovers guest IP (pct exec ip) + domain (controller.yaml), write-if-changed +
reload. Loop (7th daemon goroutine) tracks DHCP IP changes per provisioned
guest. --selftest=lanresolver. FELHOM_DNSMASQ sudoers. Spiked live on felhom-pve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 18:24:58 +02:00
parent 621a09a1c5
commit a43e9813ad
7 changed files with 642 additions and 10 deletions
+28
View File
@@ -3,6 +3,34 @@
All notable changes to **felhom-agent** are recorded here. Update on every code
change that gets pushed.
## v0.21.0 — agent-managed split-horizon LAN resolver (internal/lanresolver) (2026-06-11)
LAN clients can now reach their guest **directly** at the same public hostname with the same real
wildcard cert (no Cloudflare hairpin), via a host-side dnsmasq the agent manages. The host is the
stable anchor (static LAN IP); the guest stays DHCP/ephemeral and the agent tracks its live IP.
- **`internal/lanresolver`** — renders a dnsmasq base drop-in (bind to the host LAN IP, no-resolv,
upstreams) + a per-customer drop-in `local=/<domain>/` + `address=/<domain>/<guest-ip>`. The proven
two-line shape: `local=` makes dnsmasq authoritative for the zone so **AAAA returns NODATA** (no
Cloudflare-AAAA split-brain — the guest has only link-local v6), `address=` is the wildcard A; all
other names (and their AAAA) forward upstream unchanged.
- **`Manager`** ensures dnsmasq present (apt) + the base config + enabled, discovers the guest's live
IPv4 (`pct exec <vmid> -- ip -4 -o addr show dev eth0`) and domain (read from the guest controller's
pulled `controller.yaml` — the v2 bootstrap omits it), writes drop-ins **write-if-changed**, and
**reloads** (not restarts) dnsmasq. Tolerates the early-boot pre-lease window (empty IP → skip+retry,
never a blank record). Logs IP transitions.
- **`Loop`** — a 7th daemon goroutine: every interval (default 300s) it enumerates provisioned guests
(`/var/lib/felhom-agent/guests/<vmid>/`) and reconciles each, so the resolver follows DHCP IP changes.
Config `lan_resolver.{enable,host_ip,upstreams,interval_seconds}` (host_ip defaults to the local-API
bridge IP). `--selftest=lanresolver -vmid N`.
- **`configs/felhom-agent.sudoers`** — new `FELHOM_DNSMASQ` alias (apt install dnsmasq; install
felhom-*.conf drop-ins; systemctl enable/reload dnsmasq; rm felhom-*.conf; the two FIXED `pct exec`
reads). The agent never touches `/etc/resolv.conf` (host's own resolution unaffected).
- **Box-down robustness** is a documented **router config** (DNS = [host-IP primary, upstream
secondary]) so a box reboot degrades to the Cloudflare path, not total DNS loss — see REPORT install step.
- Spiked live on felhom-pve first (`:53` free, host IP static `192.168.0.162`, host DNS intact, full
loop from a real LAN client returned the guest IP + AAAA NODATA + the real wildcard cert `200 0`).
## v0.20.0 — golden: stacks-dir bind + per-guest hostname/CT name + bake base-infra images (2026-06-11)
Lockstep with `felhom-controller` v0.41.0 + a golden rebake. Changes in `configs/build-golden.sh` and