docs(v0.22.0): REPORT for durable_id exposure

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 20:01:02 +02:00
parent e57fbc9b80
commit 9e3513557f
+23 -56
View File
@@ -1,63 +1,30 @@
# REPORT — agent v0.21.0: agent-managed split-horizon LAN resolver (2026-06-11)
# REPORT — agent v0.22.0: expose durable_id in GET /disks (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).
**Repo:** `felhom-agent` · **Version:** 0.22.0 · **Pushed commit:** `4734d4a` · paired with
`felhom-controller` v0.43.0 (the controller-side storage-management rebuild that needs this).
## What shipped — `internal/lanresolver`
## What shipped (one read-only field)
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.
`internal/localapi/DiskInfo` gains `durable_id` (mapped directly from `StorageTarget.DurableID` — e.g.
`"uuid:<fs-uuid>"` for usb/local-dir, `"path:…"`/`"store:…"` otherwise). That's the entire change.
- **Renderer** — a base drop-in (`/etc/dnsmasq.d/felhom-resolver-base.conf`: `bind-interfaces`,
`listen-address=<host-LAN-IP>` + `127.0.0.1`, `no-resolv`, `server=<upstreams>`) + a per-customer
drop-in (`felhom-<customer-id>.conf`: `local=/<domain>/` + `address=/<domain>/<guest-ip>`). 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 <vmid> -- 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/<vmid>/`) 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.
**Why it's necessary:** the de-privileged controller cannot read a device's filesystem UUID itself, yet
`POST /disks/assign` mounts **strictly by fs UUID** (`EnsureMount``/dev/disk/by-uuid/<UUID>`). Without
the UUID surfaced somewhere the controller can reach, the guided init/attach flows could not complete the
mount. `GET /disks` already carried everything else (name/type/state/device/mount/class/data_bearing) but
dropped the durable id. The controller strips the `uuid:` prefix to get the assign key.
## 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 <vmid> -- docker exec felhom-controller cat …/controller.yaml`
`customer.domain` (no new credential).
**Security:** no new privilege, no subprocess, no behaviour change to `format`/`assign`/`eject` or the
data-bearing signature gate. The agent still inspects the device itself at format time and refuses a
data-bearing wipe with a `pending_op` (the controller surfaces the `felhom-opsign` command).
## 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).
## Validation (live, guest 9201)
`GET /disks` (via the controller's `/api/disks` proxy) now returns `durable_id` per disk, e.g.
`felhom-usb` `/dev/sdb1`, `data_bearing:true`, `durable_id:uuid:277a2179-a764-4758-b840-9ea741517914`.
The controller's guided init on `sdb` correctly drove the agent to a **data-bearing refusal** (HTTP 403 →
`pending_op{op:storage_wipe, host_scope:demo-felhom-01, durable_id:byid:wwn-0x5000039ddb108568-part1}`),
which the controller surfaced as the operator `felhom-opsign` command — the gate held end-to-end.
## 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/<vmid>/` 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.)
## Notes
This pairs with the controller's storage-management rebuild (build-golden default bumped to controller
0.43.0; golden rebaked). No other agent change.