- 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.
16 KiB
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 P1–P8 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 P1–P8” 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.0 — buildBringUpConfig 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 P1–P8, 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 | PASS — qm snapshot 300 r50pre (retained) |
P2 (3) vmbr9 create |
portless host bridge, non-disruptive | PASS — vmbr9 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 | PASS — pct 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 .176 → level=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 | PASS — agent 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 200 — all 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 18–20); 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 P2–P7 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 3–8 (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 3–8 — 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:229—ListenAddr string"bridge IP:port, e.g.192.168.0.162:8443"; validated ashost:port(:280–284). 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-ipdefault = "vmbr0 IP : 8443" (:114). - Guest dial:
felhom-agent/internal/provision/backhalf.go:75/129writes that sameEndpoint(bridge IP:port) intobootstrap.json'slocal_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:182–210: LANResolverConfig.HostIP is the dnsmasq
listen-address, and WithDefaults(localAPIListen) derives it from the local-API bind addr when
unset (:208–210, 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) onvmbr9. 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 use10.0.0.0/8and 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_ip → LAN 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)
- Phase A (agent + config): teach the install/provision path to create
vmbr9, add guestnet1, setlisten_addr+bootstrap.jsonendpoint to the island IP, and writelan_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. - 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. - 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 3–8) half; the design half above is source-grounded and ready.