Files
felhom.eu/documentation/audits/SPIKE-island-bridge-2026-07-25.md
T
admin a2500532c3 R-50: A4 fresh-provision validated (agent auto-net1 live); spike caveat closed
- A4: scratch guest provisioned from golden on the island-configured drill came
  up with net1 automatically (zero manual edits) — the v0.96.0 provisioning path
  proven live. Spike method caveat CLOSED.
- REPORT: Phase A done + vouched 0.96.0; B0/B1/A4 done; B2/B3 await operator go.
2026-07-25 14:36:56 +02:00

16 KiB
Raw Blame History

SPIKE — R-50 island-bridge control plane, 2026-07-25

Question: the agent binds a LAN literal (local_api.listen_addr) and the guest dials that literal from bootstrap.json. A DHCP/site change that moves the host's LAN address makes the agent fail to start (bind: cannot assign requested address) — storage/PBS/quiesce/restore-test/DR all go down silently (the 2026-07-20 vacation incident, F1). Target: a host-internal bridge (no physical port) with fixed private addresses, so the control plane survives any router/lease/site move. This spike was to validate the whole chain end-to-end on the DRILL environment (qm300 / demo-vm-felhom-2f4b00) before any production spec exists.

VERDICT UP FRONT: GO — validated end-to-end on a live drill (2026-07-25, evening).

A nested-PVE drill appliance was provisioned on the t740 (demo-hp) through the REAL day-0 pipeline, and probes P1P8 ALL PASS. The island-bridge control plane works end-to-end, survives the exact F1 failure that motivated the row, the pin is address-independent as predicted, the dnsmasq trap is confirmed live AND its fix proven, and the whole topology survives a cold host reboot untouched. Recommendation: GO — write the production implementation spec (Phase A/B/C skeleton at the bottom). See “## Empirical validation (2026-07-25 PM) — probes P1P8” below. The two earlier BLOCKED attempts (no drill existed) are retained further down for provenance.

Method caveat (honest scope) — CLOSED 2026-07-25 by the implementation.

The probes drove the mechanism via manual, surgical config edits on the drill. That caveat is now resolved: R-50 shipped (agent v0.96.0buildBringUpConfig attaches the island net1 from local_api.island_bridge/island_guest_addr; host-install v1.19.0 creates vmbr9, writes the island bind + island_* + the lan_resolver.host_ip LAN pin). The provisioning path is now proven live, not just the runtime: (a) the migration runbook ran verbatim on the drill (RUNBOOK-island-migration.md, B1); and (b) a scratch guest provisioned by the agent from the golden came up with net1 on the island automatically, zero manual edits (A4). Fleet migration of the two demo boxes remains (Phase B, operator go/no-go). See felhom-agent/CHANGELOG.md v0.96.0 + scripts/CHANGELOG.md v1.19.0.

Empirical validation (2026-07-25 PM) — probes P1P8, ALL PASS

Drill: nested-PVE appliance drill-r50 (QEMU VM 300 on demo-hp/t740), installed from the current release ISO felhom-pve-9.2-1-v1.25.0-nested-vm-generic-mkimage.iso through the real day-0 (self-register → operator bind → deliver → guest provision). Reached the exact R-50 starting condition: agent local_api.listen_addr = 192.168.0.176:8443 (the LAN literal) and the guest's bootstrap.json local_api.endpoint = 192.168.0.176:8443 — the F1 literal baked in both places. Nested guest 9201 runs the controller (agent 0.93.0, golden 0.161.0). Access to the drill-PVE: break-glass root via hub host_recovery/drill-r50-0a4f9a, reached through demo-hp. (Day-0 tail: claim left PENDING — customer-email-gated, no inbox on a scratch customer; escrow N/A — no DR/PBS tier on the minimal drill. Neither gates the control-plane probes.)

Probe What it validates Result
P1 snapshot rollback point before mutation PASSqm snapshot 300 r50pre (retained)
P2 (3) vmbr9 create portless host bridge, non-disruptive PASSvmbr9 169.254.253.1/30 up via ifreload -a; vmbr0 untouched (.176/24), LAN gw still reachable, guest kept running
P3 (4) guest NIC hot-add live hotplug, LAN leg undisturbed PASSpct set 9201 -net1 …ip=169.254.253.2/30; eth1 up on the island, eth0/LAN undisturbed (192.168.0.15/24), host↔guest island ping both ways
P4 (7) dnsmasq trap + fix the Finding-1 coupling, live PASS (both) — with host_ip unset, moving listen_addr to the island rebound dnsmasq to 169.254.253.1:53 and LAN DNS 192.168.0.176:53 died; setting lan_resolver.host_ip=192.168.0.176 put dnsmasq back on 192.168.0.176:53 while the API bind stayed on the island
P5 (6) pin pin survives the address move PASS — served leaf SHA-256 over the island identical (4ef1d953fe…f219bab); authenticated GET https://169.254.253.1:8443/storage from the guest → HTTP 200. No cert re-issue. Finding 6 confirmed live
P6 (5) F1 replay control plane survives a LAN move PASS — the money shot. LAN moved 192.168.0.176 → .200, listen_addr left on the island → agent stays active, still bound 169.254.253.1:8443, control plane HTTP 200. Contrast (original bug reproduced): set listen_addr back to the now-absent .176level=ERROR "daemon: exited with error" err="localapi: bind 192.168.0.176:8443: listen…", systemd status=1/FAILURE, nothing bound — the 2026-07-20 incident, verbatim
P7 (8) survival matrix topology is reboot-durable PASSagent restart: island bind + 200; guest reboot: eth1 island NIC + controller + 200 returned; host (qm300) COLD reboot: vmbr9, agent island bind, dnsmasq on the LAN IP, guest autostart (onboot=1) with eth1, controller healthy, control plane HTTP 200all with zero intervention

All source-grounded findings are now empirically confirmed: the F1 double-bake (agent bind + guest dial) moves atomically and works; the pin is DER-based and address-transparent (Finding 6); the dnsmasq trap is real and its lan_resolver.host_ip = LAN IP fix works (Finding 1); link-local 169.254.253.x/30 is a clean host-internal fabric that no LAN move perturbs. The drill was left in the working island configuration (not rolled back); snapshot r50pre preserves the clean LAN-literal day-0 if a re-run is wanted.

GO/NO-GO: GO. Proceed to the implementation spec (Phase A/B/C below). No blocker remains; the one

residual is that the provisioning path (host-install writing the island config + golden-bake carrying the new bootstrap template) is unbuilt — that IS the impl task, now de-risked.


Empirical attempt 2 (2026-07-25, PM) — the t740 was checked; there is no drill VM there. — SUPERSEDED by the validation above (a drill was then provisioned and probed).

The operator ruled (2026-07-25) that drill + build VMs are hosted on the HP t740 (demo-hp) from now on, so this half was re-attempted there. Discovery (read-only, no LAN scan): the t740 = demo-hp (Tailscale 100.76.96.79 / LAN 192.168.0.87), accessed via the hub-vaulted G1 break-glass root credential (host_recovery/demo-hp-bb76ea; secret redacted). Findings:

t740 (demo-hp, PVE 9.2.2): qm list → (empty); /etc/pve/qemu-server/ → empty; standalone (no cluster);
                           only LXC guest 9201 (its OWN live demo controller). NO nested drill PVE VM.
hub registry:              only demo-hp-bb76ea (t740) + demo-felhom-8363b5 (N100) — NO drill appliance.
DooPlex:                   drill.qcow2 EXISTS (/mnt/5_hdd/felhom.eu/drill/, 18G) but is POWERED OFF —
                           a GOLDEN-BAKE appliance (bake logs to 0.153.0, last Jul 1820); its nested
                           guest (9100) is purged after each bake, so it has no island-bridge topology
                           (agent + nested guest + bootstrap.json) to probe. DooPlex is the production
                           k3s node and is loaded (~17G of 62G free).

So the operator's ruling is forward-looking and not yet realized — no probeable drill appliance exists on the t740, and the only artifact is a stale bake VM on the production node. Per the hard rule ("do not improvise on production"), probes P2P7 were NOT run; felhom-pve, the t740 host networking, and guest 9201 were left UNTOUCHED. Operator decision (2026-07-25): they will provision a nested-PVE drill VM on the t740 (agent + nested guest); this task re-runs then. GO/NO-GO stays PENDING.

Empirical attempt 1 (2026-07-25, AM) — drill env was GONE on felhom-pve.

Probe 1 (drill env health check) — FAIL/ABSENT. On felhom-pve (2026-07-25):

# qm list        → (empty)  — no QEMU VMs at all; the nested drill PVE VM qm300 does not exist
# pct list       → VMID 9201 "demo-felhom" running  — the LIVE demo controller only
# grep -rl '2f4b00|drill' /etc/pve/{qemu-server,lxc}/  → (no matches)

The drill appliance (a throwaway nested PVE-in-a-VM) has been torn down since the last drill. Per the task's hard environment rule — "if the drill env is down or unreachable: STOP, report, done; do not improvise on production" — probes 38 (bridge create, guest NIC hot-add, island bind, the F1 replay, survival matrix) were NOT run. felhom-pve's bridges/NICs, the live agent's listen_addr, and guest 9201 were left UNTOUCHED. No GO/NO-GO can be issued on live evidence; a rebuilt drill VM is required to validate the chain.

What follows is the source-only analysis (read-only; no host mutation) — the address plan, the three named findings, and the provisioning inventory — so the eventual implementation spec (and the next drill run) start from a real base rather than zero.


Probes 38 — NOT RUN (drill env absent) — SUPERSEDED: all now PASS, see “Empirical validation (2026-07-25 PM)” above.

Probe Would validate Status
3 Bridge create (vmbr9, no ports, ifreload -a) w/ vmbr0 untouched non-disruptive host bridge add BLOCKED — needs drill host
4 Guest NIC hot-add (pct set -net1 …ip=169.254.253.2/30) live hotplug, LAN leg undisturbed BLOCKED
5 Agent island bind + F1 replay (renumber LAN → agent still starts/serves) the money shot BLOCKED
6 Pin confirmation (bootstrap→island, controller round-trip, unchanged fingerprint) pin survives address move BLOCKED (but see source finding below)
7 dnsmasq trap probe LAN DNS on the LAN bridge, not the island BLOCKED (source-confirmed below)
8 Survival matrix (agent restart / guest reboot / host reboot) control plane returns on the island BLOCKED

Source-confirmed findings (read-only; no probe needed)

F1 root cause — the LAN literal is baked in TWO places, together

  • Agent bind: felhom-agent/internal/config/config.go:229ListenAddr string "bridge IP:port, e.g. 192.168.0.162:8443"; validated as host:port (:280284). The install script sets it from the vmbr0 (LAN) IP: felhom.eu/scripts/felhom-host-install.sh:2162 (ip -4 addr show vmbr0) → :2226 (base['local_api']['listen_addr'] = BRIDGE_ADDR), --bridge-ip default = "vmbr0 IP : 8443" (:114).
  • Guest dial: felhom-agent/internal/provision/backhalf.go:75/129 writes that same Endpoint (bridge IP:port) into bootstrap.json's local_api.endpoint (internal/provision/doc.go:44) — the address the in-guest controller dials. → Both must move to the island address atomically (agent bind + the guest's bootstrap endpoint), or the controller can't reach the agent.

Finding 1 — the dnsmasq trap (CONFIRMED in source; a MUST for the spec)

felhom-agent/internal/config/config.go:182210: LANResolverConfig.HostIP is the dnsmasq listen-address, and WithDefaults(localAPIListen) derives it from the local-API bind addr when unset (:208210, l.HostIP = host(localAPIListen)). So the instant listen_addr moves to the island (169.254.253.1), the split-horizon LAN resolver would bind its DNS to the island bridge and LAN DNS dies — a silent, nasty coupling. Spec requirement: set LANResolverConfig.HostIP explicitly to the LAN (vmbr0) bridge IP; never let it default off listen_addr once the two diverge. (The install script must write lan_resolver.host_ip alongside the new listen_addr.)

Finding 6 (pin) — moving the bind should NOT need a cert re-issue

The controller pins the agent's leaf-cert DER SHA-256, not its SAN/address: felhom-agent/internal/localapi/cert_test.go:37 ("the reported fingerprint must equal the SHA-256 of the served leaf DER"); the pinning verifier keys on the cert bytes, not the connect address. So an address move is transparent to the pin as long as the same leaf cert is served — no re-issue expected. (Unproven live — that was probe 6; asserted from source.) Caveat: if a future cert is minted with the island IP in its SAN and something ever DOES check SAN, that changes — but today's pin does not.

Address-plan recommendation

  • Recommend link-local 169.254.253.1/30 (host) ↔ 169.254.253.2/30 (guest) on vmbr9. Rationale: RFC-3927 link-local (169.254.0.0/16) is, by standard, neither routed nor DHCP-assigned on a LAN — a home router won't hand it out or forward it, so a host-internal /30 cannot collide with any customer LAN, ever. A /30 is exactly two usable hosts (host + guest) — no waste, no ambiguity.
  • Alternative (obscure RFC-1918 /30, e.g. 10.201.253.0/30): also works, but a customer LAN could legitimately use 10.0.0.0/8 and route it, so it carries a small non-zero collision risk that link-local structurally does not. Prefer link-local.

Provisioning inventory (what an implementation must touch — file:line)

Concern Location
Create vmbr9 (portless) + host island IP felhom.eu/scripts/felhom-host-install.sh (new: /etc/network/interfaces stanza + ifreload -a)
Guest net1 on vmbr9 (static island IP) felhom-host-install.sh (new pct set … -net1 …) — R-54 interplay: guest eth0 stays DHCP (the guestnet healer keeps owning the LAN leg); the island NIC is static; the control plane simply stops depending on the LAN leg
listen_addr → island IP felhom-host-install.sh:2226 (+ default logic :2162/:114) → agent config.go:229
lan_resolver.host_ipLAN vmbr0 IP (Finding 1) new write in felhom-host-install.sh; consumed by config.go:198 WithDefaults
bootstrap.json endpoint → island IP felhom-agent/internal/provision/backhalf.go:129 (Endpoint) + the golden/bootstrap template
Leaf cert no change expected (pin is DER-based; Finding 6)

Cluster-parity finding (Peti's two-node cluster)

A one-host demo needs vmbr9 on that host only. A cluster needs bridge parity on BOTH nodes — either per-node identical /etc/network/interfaces stanzas (simplest, drift-prone) or a Proxmox SDN zone/vnet defined cluster-wide (one definition, auto-applied per node; preferred at ≥2 nodes). The guest's island IP is per-guest and node-independent; the agent-follows-guest rule (each host's agent binds its own vmbr9 island IP) holds on both nodes. Migration order for the eventual rollout: drill-proven → demo (one host) → Peti (SDN on the 2-node cluster).


Implementation-spec skeleton (for the future task — NOT this one)

  1. Phase A (agent + config): teach the install/provision path to create vmbr9, add guest net1, set listen_addr + bootstrap.json endpoint to the island IP, and write lan_resolver.host_ip = LAN IP (Finding 1). Repos: felhom.eu (host-install), felhom-agent (provision/bootstrap template). MinAgent/floor: additive to provisioning; no controller coupling → MinAgent unchanged; a golden re-bake carries the new bootstrap template.
  2. Phase B (migration): per-box runbook (bridge add → net1 → agent+bootstrap renumber → restart → F1-replay check → rollback = revert listen_addr+endpoint to the LAN IP), drill-first.
  3. Phase C (cluster): SDN vnet on Peti's cluster; parity check on both nodes.

STOP — the implementation is its own future task. This spike is BLOCKED on a rebuilt drill VM for the empirical (probes 38) half; the design half above is source-grounded and ready.