v0.119.0 — the host report carries the box's addresses
A managed box's IP was invisible in every operator surface because nothing reported one: HostMetrics carried node/cpu/mem/disk/load/uptime/temp/wrapper-sha and no address of any kind. The hub could not show a host's LAN IP anywhere. Two things that looked like the answer are traps, both checked before writing code: lan_resolver.host_ip is an OPTIONAL config value absent unless that feature is configured, and DeriveHostIP(local_api.listen_addr) returns 169.254.253.1 — since R-50 the local API binds a link-local address identical on every box. Both would have produced a confident wrong answer. New wire field addresses[], one entry per (interface, address). Deliberately iface+cidr rather than a single lan_ip: a Proxmox host legitimately holds several (management bridge, tailnet, WG tunnel) and picking one to call "the" LAN IP is a guess the agent is not entitled to make — silently wrong on a box whose bridge is not vmbr0. The agent reports what exists; the hub does the labelling. The filter is one predicate, chosen by MEASURING both demo hosts rather than by reasoning about interface names. IsGlobalUnicast() alone drops loopback, IPv6 link-local (one per bridge, pure noise) and IPv4 link-local (169.254/16 — exactly the island address above). It needs no veth/fwbr/tap denylist: that per-guest plumbing carries no IP at all and self-excludes, verified on both boxes. No new privilege and no block I/O — net.Interfaces() is a netlink/procfs read, so the sudoers fence is untouched and the health-check rule is honoured. The seam DEFAULTS to the real enumerator, inverting the nil-reporter-means-off convention: this stanza has no config gate, so a forgotten wiring call would have shipped it silently empty — the inert-seam failure recorded four times here. Cross-repo: the golden is duplicated byte-identically in felhom.eu and the contract test fails on top-level key drift, so both goldens moved together and addresses[0]'s key set is asserted bidirectionally. The field marshals as [], never null — the repo's own no-nulls invariant caught that on the first run. Tests +9; three red-proofs (global-unicast filter, down-interface guard, inert collectAddresses) each run, observed failing, and reverted.
This commit is contained in:
@@ -1,3 +1,53 @@
|
||||
## v0.119.0 — the host report carries the box's addresses (2026-07-31)
|
||||
|
||||
**Pairs with hub v0.85.0.** The agent half is useless without it — the hub is what renders these.
|
||||
|
||||
**A managed box's IP was invisible in every operator surface, because nothing reported one.**
|
||||
`HostMetrics` carried node, cpu, memory, disk, loadavg, uptime, temperature and the wrapper sha —
|
||||
and no address of any kind. The hub therefore could not show a host's LAN IP anywhere; the only IP
|
||||
reachable from the UI at all was the WireGuard one, and only on `/offsite`'s peer table keyed by
|
||||
pubkey, so you could go peer→host and never host→peer.
|
||||
|
||||
**Two things that looked like the answer are traps, and both were checked before writing code.**
|
||||
`lan_resolver.host_ip` is an OPTIONAL config value, absent unless that feature is configured. And
|
||||
`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 **identical on every box**. Either would have produced a
|
||||
confident wrong answer, which is worse than the blank it replaces.
|
||||
|
||||
**New wire field `addresses[]`, one entry per (interface, address).** Deliberately iface+cidr rather
|
||||
than a single `lan_ip`: a Proxmox host legitimately holds several — a management bridge, a tailnet,
|
||||
the WG tunnel — and picking one to call "the" LAN IP is a guess the agent is not entitled to make. On
|
||||
a box whose management bridge is not `vmbr0` that guess is silently wrong. The agent reports what
|
||||
exists; the hub does the labelling.
|
||||
|
||||
**The filter is one predicate, and it was chosen by MEASURING both demo hosts, not by reasoning.**
|
||||
`IsGlobalUnicast()` alone drops loopback, IPv6 link-local (`fe80::/10`, one per bridge, pure noise)
|
||||
and IPv4 link-local (`169.254/16` — exactly the island address above). It needs **no veth/fwbr/tap
|
||||
denylist**, because on a Proxmox host that per-guest plumbing carries no IP at all and self-excludes:
|
||||
`veth9201i0/i1` and the unused NICs appear in `ip link` and in no `ip addr` output on either box. What
|
||||
survives is `vmbr0`'s LAN address, `wg-felhom`'s tunnel address and `tailscale0`'s tailnet addresses —
|
||||
all true, all useful, none labelled here.
|
||||
|
||||
**No new privilege and no block I/O.** `net.Interfaces()` is a netlink/procfs read: it needs no sudo
|
||||
grant, touches the sudoers fence not at all, and honours the health-check rule (R-117 spike §6.3) that
|
||||
liveness is decided from `/proc` and kernel state, never by reading a filesystem.
|
||||
|
||||
**The seam defaults to the REAL enumerator.** `Collector.addrEnum == nil` uses `systemInterfaces`,
|
||||
inverting the nil-reporter-means-off convention the optional stanzas use. Those gate on a config
|
||||
feature; this one has no dependency and no flag, so a forgotten wiring call in `main.go` would have
|
||||
shipped it silently empty — the inert-seam failure this repo has now recorded four times.
|
||||
|
||||
**Cross-repo, and the contract test enforces it:** `testdata/host-report.golden.json` is duplicated
|
||||
byte-identically in `felhom.eu/hub/internal/api/testdata/`, and `TestHostReport_ContractMatchesGolden`
|
||||
fails on any top-level key drift. Both goldens moved in this arc, and `addresses[0]`'s key set is
|
||||
asserted bidirectionally alongside the existing sections. The field marshals as `[]`, never `null` —
|
||||
the repo's own no-nulls invariant test caught that on the first run.
|
||||
|
||||
Files: `internal/hub/hostaddr.go` (new), `internal/hub/report.go`, `internal/hub/collect.go`,
|
||||
`internal/hub/contract_test.go`, `internal/hub/report_test.go`, both goldens, `REUSE.md`.
|
||||
Tests +9; three red-proofs (the global-unicast filter, the down-interface guard, and an inert
|
||||
`collectAddresses`) each run, observed failing, and reverted.
|
||||
|
||||
## v0.118.1 — R-106 follow-up: the namespace was still being lost in the merge (2026-07-30)
|
||||
|
||||
**v0.118.0's R-106 fix was incomplete and live validation caught it.** Deployed to demo-felhom, the
|
||||
|
||||
Reference in New Issue
Block a user