Files
felhom.eu/documentation/audits/host-addresses-visible-2026-07-31.md
T

6.3 KiB

AUDIT — a managed host's addresses are visible in the hub (2026-07-31)

Artifacts: felhom-agent v0.119.0 + hub v0.85.0. Method: endpoint-level on live hardware (no browser on DooPlex), plus direct ip addr reads on both boxes as the ground truth the rendered page is checked against.

1. The finding that started it

The operator asked to see the LAN and WireGuard IPs on the hosts page, "because it looks like it is not visible anywhere". That was very nearly exact:

  • LAN IP — not visible, and not reportable. HostReport/HostMetrics carried node, cpu, memory, disk, loadavg, uptime, cpu temp and the wrapper sha — no address of any kind. The hub could not render what it was never sent.
  • WireGuard IP — visible in one place, in the wrong direction. /offsite renders a peer table with an Assigned IP column and a Host link, so an operator could go peer→host but never host→peer, which is the direction the question is actually asked in. GetWGPeerForHost already existed with no UI consumer.

2. Two plausible LAN-IP sources were rejected BEFORE any code, on measurement

Candidate Why it fails
lan_resolver.host_ip (agent config) OPTIONAL — set only when the split-horizon DNS feature is configured. Absent on a box that never enabled it.
DeriveHostIP(local_api.listen_addr) Returns 169.254.253.1 — since the R-50 island migration the local API binds a link-local address that is byte-identical on every box.

Both would have produced a confident wrong answer, which is worse than the blank they replace. The agent now reads the kernel's own interface table instead.

3. Ground truth — what the boxes actually hold

ip -o addr show, 2026-07-31:

demo-felhom            demo-hp
lo         127.0.0.1/8 + ::1/128            (both)
tailscale0 100.70.170.35/32 + ULA + fe80    100.76.96.79/32 + ULA + fe80
vmbr0      192.168.0.162/24 + fe80          192.168.0.87/24 + fe80
vmbr9      169.254.253.1/30 + fe80          169.254.253.1/30 + fe80   <- IDENTICAL on both
wg-felhom  10.77.0.2/32                     10.77.0.3/32
(no address at all: enp*/wlp*/wlo1, veth9201i0, veth9201i1)

The measurement decided the design. IsGlobalUnicast() alone drops loopback, fe80::/10 and 169.254/16 — that last one being the island literal above. And no veth/fwbr/tap denylist is needed, because on a Proxmox host that per-guest plumbing carries no IP at all and self-excludes. The unit-test fixtures are these tables transcribed verbatim, including the address-less rows, because the no-denylist claim rests on those rows really being empty.

4. The wire, from the agent that will ship

felhom-agent --selftest=hub on demo-felhom running 0.119.0 — "the report it would send":

"addresses": [
  { "iface": "tailscale0", "cidr": "100.70.170.35/32" },
  { "iface": "tailscale0", "cidr": "fd7a:115c:a1e0::5236:aa24/128" },
  { "iface": "vmbr0",      "cidr": "192.168.0.162/24" },
  { "iface": "wg-felhom",  "cidr": "10.77.0.2/32" }
]

Correctly present: the LAN address. Correctly absent: lo, vmbr9's island address, every fe80::.

5. Rendered, live, on every enrolled host

Read off GET /hosts/<id> on the deployed hub 0.85.0:

Host Agent Network card
demo-felhom-8363b5 0.119.0 WireGuard 10.77.0.2 confirmed; vmbr0 192.168.0.162/24, tailscale0 100.70.170.35/32 + ULA
demo-hp-bb76ea 0.119.0 WireGuard 10.77.0.3 confirmed; vmbr0 192.168.0.87/24, tailscale0 100.76.96.79/32 + ULA
drill-r50-0a4f9a 0.113.0 WireGuard 10.77.0.4 allocated; "This host's agent (0.113.0) does not report its addresses — they are unknown, not absent. Needs agent 0.119.0 or newer."
sess-f-2670b5 old same UNKNOWN rendering

Both 0.119.0 rows match the ip addr ground truth in §3 exactly.

6. The two design points that carry the risk

WireGuard is rendered as TWO facts. WGAssignedIP is the hub's allocation (wg_peers, desired state); WGConfirmed is whether the box reports holding it. Rendering the allocation alone would make a peer that was never applied look healthy — the presence-is-not-result trap, in the shape this project has already been bitten by twice (F-CRIT-2, R-100). A mismatch renders not confirmed by the box, and a red-proof pins it: hard-wiring WGConfirmed = true turns that test red.

The split is keyed on the allocation, never on the interface name wg-felhom — that is a systemd unit name, and a UI keyed on it would silently mis-render the day it changes.

An old agent renders UNKNOWN, never "no addresses." Below 0.119.0 the field is absent from the wire. An absent signal and a negative result are different facts, and conflating them would have the page state something false about a live host. Proven live on drill-r50-0a4f9a (§5) and red-proofed: deleting the branch makes the page claim the host has no routable address.

7. Release hygiene (R-115's leg, walked deliberately)

Deployed and installable, in the same session — the two states R-115 says drift silently:

  • built v0.119.0, deployed to both demo boxes (systemctl is-activeactive, --version → 0.119.0)
  • published to Gitea (scripts/publish-agent.sh), sha e37aca824d0f57bb…, round-trip GET verified
  • vouched in the Day-0 manifest and read back: agent=0.119.0, with golden 0.188.0, min_agent 0.113.0 and wrapper_sha256 preserved verbatim
  • min_agent deliberately NOT raised: the field has no controller coupling, so holding the controller floor for it would be an unnecessary fleet-wide write

A trap hit on the way, worth recording: the Gitea package list sorted lexicographically put 0.96.0 above 0.118.1 — the exact shape R-120 recorded for SQL MAX(). Re-sorted by semver before choosing the version.

8. Not covered

  • A box with two LAN bridges. The design handles it (one row per interface) and a unit test covers it, but no such box exists to observe.
  • IPv4-only or IPv6-only hosts — every box here is dual-stacked via tailscale's ULA.
  • A real WireGuard drift — the not confirmed branch is unit-tested and red-proofed, never seen in production, because no live peer is currently unapplied.
  • The card is operator-facing only; no customer surface changed.