# 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":

```json
"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-active` → `active`, `--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.
