# REPORT — agent v0.21.0: agent-managed split-horizon LAN resolver (2026-06-11) **Repo:** `felhom-agent` · **Version:** 0.21.0 · **Pushed commit:** `a43e981` · paired with `felhom-controller` v0.42.1 (real wildcard cert — the GATE this depends on). ## What shipped — `internal/lanresolver` LAN clients 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. - **Renderer** — a base drop-in (`/etc/dnsmasq.d/felhom-resolver-base.conf`: `bind-interfaces`, `listen-address=` + `127.0.0.1`, `no-resolv`, `server=`) + a per-customer drop-in (`felhom-.conf`: `local=//` + `address=//`). The proven two-line shape: `local=` makes dnsmasq authoritative for the zone so **AAAA → NODATA** (no Cloudflare-AAAA split-brain — the guest has only link-local v6); `address=` is the wildcard A; everything else (and its AAAA) forwards upstream unchanged. - **`Manager`** — `EnsureDnsmasq` (apt-install if absent, base config, `systemctl enable --now`), `ReconcileGuest` (discover live IPv4 via `pct exec -- ip -4 -o addr show dev eth0`; discover domain from the guest controller's pulled `controller.yaml` — the v2 bootstrap omits it; write-if- changed; `systemctl reload`), `Remove` (decommission). Tolerates the early-boot pre-lease window (empty IP → skip+retry, never a blank record); logs IP transitions. Never touches `/etc/resolv.conf`. - **`Loop`** — a 7th daemon goroutine; every interval (default 300s) enumerates provisioned guests (`/var/lib/felhom-agent/guests//`) and reconciles each, so the resolver follows DHCP IP moves. - Config `lan_resolver.{enable,host_ip,upstreams,interval_seconds,state_dir}` (host_ip defaults to the local-API bridge IP). `--selftest=lanresolver -vmid N`. New `FELHOM_DNSMASQ` sudoers alias. ## 2A spike findings (host environment — all green, no blockers) - **`:53` is FREE** on the host (no systemd-resolved/dnsmasq/named). - **Host IP is STATIC** — `vmbr0 inet static 192.168.0.162/24` (the stable anchor the router points at). - **Host DNS intact** — `/etc/resolv.conf` (→ Pi-hole `192.168.0.250`) is untouched; the host resolves upstream for itself independent of the dnsmasq we add for LAN clients. - **Domain sourcing** — `pct exec -- docker exec felhom-controller cat …/controller.yaml` → `customer.domain` (no new credential). ## Live validation (felhom-pve, guest 9201 = demo-felhom) - `--selftest=lanresolver -vmid 9201`: installed dnsmasq, wrote base + per-customer config, discovered IP `192.168.0.151` + domain `demo-felhom.eu`, dnsmasq listening on `192.168.0.162:53`. - **Full loop from dooplex (real LAN client):** `felhom.demo-felhom.eu A → 192.168.0.151`, **AAAA → NODATA**, `example.com → forwards`, real cert `200 ssl_verify=0`. - **IP-change tracking:** poisoned the drop-in with a stale IP → reconcile detected the live IP differs, re-rendered + reloaded → corrected. - **Daemon loop** runs (`lanresolver: enabled host_ip=192.168.0.162 interval_s=300`); gracefully skips a not-yet-leased / absent guest. - **Box-down:** with dnsmasq stopped, the primary (`192.168.0.162`) refuses; a secondary (`1.1.1.1`) still answers normal names — so a client configured `[host-IP, secondary]` fails over to the Cloudflare path; recovery confirmed on restart (dnsmasq is `enable`d → starts on boot). ## Install step (the one irreducible manual action) + robustness - **One-time:** set the customer router's DNS servers to **[host-IP (192.168.0.x) primary, an upstream secondary (e.g. 1.1.1.1)]**. After that, split-horizon is automatic forever — new apps auto-resolve locally (the wildcard `address=` covers every subdomain) and serve the real wildcard cert. - **Box-down:** the secondary means a box reboot degrades to the public/Cloudflare (hairpin) path, not total DNS loss. Client DNS-failover is imperfect but standard; this is a required install config, not agent code. - **Locked ISP routers** (can't repoint DNS): LAN-direct isn't achievable; the Cloudflare tunnel remains the graceful fallback. Documented, not forced. - New host dependency the agent ensures: **dnsmasq** (installed/enabled by `EnsureDnsmasq`). ## Notes / follow-ups - Stale guest state (`/var/lib/felhom-agent/guests//` for a destroyed guest) makes the loop log a harmless per-tick skip; a decommission lifecycle should remove that dir + call `Manager.Remove`. Left for the decommission slice. (Cleaned the one stale `9200` dir on the demo by hand.)