From 790adb07a9503da1e8ffe06a86e5ba78691199b1 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 11 Jun 2026 18:30:44 +0200 Subject: [PATCH] docs(v0.21.0): REPORT for the split-horizon LAN resolver + install step Co-Authored-By: Claude Opus 4.8 (1M context) --- REPORT.md | 97 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/REPORT.md b/REPORT.md index 85cfcaa..f4e20b2 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,46 +1,63 @@ -# REPORT — agent v0.20.0: golden stacks-dir bind + per-guest hostname/CT + bake base-infra images (2026-06-11) +# REPORT — agent v0.21.0: agent-managed split-horizon LAN resolver (2026-06-11) -**Repo:** `felhom-agent` · **Version:** 0.20.0 · **Date:** 2026-06-11 -**Pushed commit:** `1799fcd` · paired with `felhom-controller` v0.41.0 (`abbd948`) + golden rebake. +**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 (all in `configs/build-golden.sh` + the provision path; no change to proxmox/authz/token fences) +## What shipped — `internal/lanresolver` -- **Section-G mount fix (load-bearing):** the in-guest controller writes app/infra compose stacks under - `/opt/docker/stacks` *inside its container*, but the baked controller-bootstrap `docker run` never - bind-mounted that path — so `docker compose up` (run by the GUEST daemon over the shared socket) - resolved every relative bind source on the guest filesystem, silently creating empty dirs and breaking - **every** bind-mounted stack (base infra AND customer apps). The bootstrap unit now `mkdir -p - /opt/docker/stacks` and adds a **same-path host bind** `-v /opt/docker/stacks:/opt/docker/stacks` (a - named volume would NOT fix this). Empirically confirmed on guest 9201 before writing the fix. -- **Per-guest container hostname (3A):** the bootstrap unit parses `customer.id` from - `/etc/felhom-bootstrap/bootstrap.json` with a portable `sed` (NO jq in the golden) and passes - `--hostname ` to `docker run`. **Security-hardened** (flagged by the commit security - review): the id is validated to a DNS-safe label (reject spaces/slashes/leading-dash) and passed via a - quoted array, so a malformed/hostile id can't smuggle docker flags (e.g. `--privileged`) into the - de-privileged run. Empty/invalid → no `--hostname` (fail-safe). -- **Per-guest CT/LXC name (3B):** `--selftest=provision` defaults `-hostname` to the DNS-safe-sanitized - `-customer-id` when not given, so the bring-up's existing `SetConfig hostname` step names the CT - meaningfully instead of inheriting the golden's `felhom-golden`. New `sanitizeHostname` (lowercase, - collapse invalid → `-`, trim, ≤63). -- **Bake base-infra images:** the golden pulls the three PINNED, PUBLIC base-infra images - (`traefik:v3.6.7`, `cloudflare/cloudflared:2026.6.0`, `gtstef/filebrowser:1.3.3-stable`) into its Docker - storage so the controller's first-boot bring-up is OFFLINE-capable. A `docker manifest inspect` **hard - gate** fails the bake early on a bad pin. Tags MUST match the controller's `internal/infra` constants. +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. -## Live validation (demo host felhom-pve / PVE 9.2.2) +- **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. -- Agent **v0.20.0** built (go1.26, ldflags `-X main.version=0.20.0`) and deployed to - `/usr/local/bin/felhom-agent`; service active. -- **Golden rebaked** → `local:backup/vzdump-lxc-9100-2026_06_11-15_06_05.tar.zst` (877 MB, up from ~599 MB - ≈ the three baked infra images). Controller `:0.41.0` pulled (digest `0a9d456…`) + all 3 infra pins - resolved past the hard gate. `/etc/felhom-controller-image` reads `:0.41.0`. -- **Guest 9201 destroyed + re-provisioned** from the new golden: provision header `hostname=demo-felhom`; - CT config `hostname: demo-felhom`; runtime hostname `demo-felhom`; controller `os.Hostname()` = - `demo-felhom`. Back-half minted the per-guest token + attached the `mp9` bootstrap mount. -- After `pct reboot` + `systemctl restart felhom-agent`: the baked controller-bootstrap deployed the - controller, which pulled its config and stood up the base stack — **4 containers running**, Health = OK, - cloudflared tunnel registered, the `/opt/docker/stacks` bind resolving end-to-end. +## 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). -## Notes -- The registry credential used for the bake was staged transiently as a 0600 file on the host and removed - by the rebake; it is **stored out-of-band** and never committed. +## 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.)