# REPORT — the box tells visitors apart; the permanent-gate spike; SparkyFitness; R-772/R-773; release + golden (2026-10-01 night) ## The Part table | Part | Result | Notes | |---|---|---| | **A** — visitors apart (build) | **DONE, with one change to the brief's sketch and one leg unmeasured** | The sketch plus 2 costs paid in the same rollout: a header clean-up at traefik, and 19 catalog router resets for the apps that read the LEFTMOST address. The brief named neither. ep0's one request was refused by Cloudflare before the box (R-779). | | **B** — permanent family gate (spike) | **DONE — PASS** on exit items 1–5; costs measured | Exit test committed before the run. Build plan: 2 sessions. Waits for the operator (R-780). | | **C** — SparkyFitness record | **DONE, with 7 rows open** | Bench (9401 rebuilt and destroyed) and 9202 measured. **Its licence forbids commercial use** (R-784, operator). | | **D** — R-772, R-773 | **DONE** | Both red-proven and live on 9202. R-772 needed a second fix found live (0.286.0 → 0.286.1). | | **E** — release, floor, golden | **DONE** | Controller 0.286.1 floored (min_agent 0.131.0); both demo boxes took it and reconciled traefik/cloudflared by themselves. Golden 0.286.1 baked, round-tripped and vouched; the currency gate is OK, not waived. | Baselines (re-verified): controller `c1b123c64955`, agent `d766666ff8cf`, felhom.eu `bc9c7b49346b`, catalog `94477cba435a`. Ends at: controller `a4a753b` (v0.286.1), catalog `de0a9bd`, felhom.eu (this commit). Agent untouched. Architecture read: `01-topology-and-trust.md` §5 and §7 (both corrected), `09` §3 decisions 45–49, 57–62; decision 63 added first (operator ruling). ## Claims in the brief that turned out wrong (or right), named - *`clientIP` takes the leftmost hop.* **Right** (`claim.go:232`, pre-0.286). - *A stranger can lock the household out of the dashboard.* **Right, through the tunnel only.** Every tunnel visitor shared cloudflared's address as the key; a LAN visitor always had its own key. - *Cloudflare appends to a client-sent `X-Forwarded-For`.* **Right, measured:** `6.6.6.6,37.191.56.193` (M2). Also measured, not in the brief: - Cloudflare passes a client's `X-Forwarded-Host` and `X-Forwarded-Port` unchanged. - It strips a client's `X-Real-IP`. - It refuses a client-sent `CF-Connecting-IP` at the edge (403). - *cloudflared's address is docker-assigned.* **Right** (`172.18.0.5`, no `ipv4_address`). - *The setup gate's token is minted only from a dashboard session.* **Right** (`ServeGateStart`, one caller). But **"the setup gate uses [the visitor address]" was wrong:** the gate read no client address at all, so there was nothing to switch over. It now logs the visitor. - *"Any reader takes the address at a fixed distance from the RIGHT."* **Half wrong.** The distance differs by path: second from the right through the tunnel, first on the LAN. So a fixed count is wrong for one of the two paths, and readers must skip trusted proxies from the right. Count readers (calibre-web, tandoor, wger) were left alone for that reason. - *The sketch (traefik trusts cloudflared) is enough.* **Not on its own.** Trusting cloudflared makes the leftmost entry — written by a stranger — reach every app. 19 apps read exactly that one (several for rate limits: audiobookshelf and docmost by address only, papra skips its limit on a junk value). It also lets a stranger's `X-Forwarded-Host` through. Both were closed in the same rollout. - *calibre-web `TRUSTED_PROXY_COUNT`, wger `AXES_*` proxy count.* **Left as they are.** Both are count readers, and both lock per NAME (decisions 58, 61). A count of 2 would break the LAN path, and for calibre-web also its proto/host. - *Grimmory improves.* **Right as read in source; not measured in this session** (R-775 updated). ## Part A — the box tells visitors apart - **Paths**: `audits/visitors-2026-10-01/A/` — M1–M4 before, M5 after, DESIGN.md (the path table, the options, the choice, the docs quoted). - **Built** (controller v0.286.0/.1): - A `felhom-tunnel` network `172.16.253.0/29` with docker's allocation confined to `.4/30`. The hand prototype found traefik grabbing `.2`, so cloudflared now sits at a fixed `.2` and traefik at a fixed `.3`. - traefik trusts `172.16.253.2/32` only, and runs `felhom-forwarded@file`. - `EnsureBaseStack` reconciles a running traefik or cloudflared. - `clientaddr.go`, plus IPv6 counted per /64. - The dashboard login messages are informal, in both languages. - **Red-proofs**: RP-A1, RP-A2 and RP-A3 each fail on their mutant. - **Live**: - **9202, simulated tunnel:** a stranger rotating a forged leftmost address was locked after 5 tries; the household from another address got in at once; LAN and impostor forgeries were counted as themselves (L1). - **demo-hp, real tunnel:** an app sees `6.6.6.6,37.191.56.193, 172.16.253.2`. The dashboard counter was keyed on `37.191.56.193` and locked after 5. Every app answered (L2, M5, E/). - **ep0's one request:** refused 403 by Cloudflare's edge, with no line in the box's log, so the second outside address is unmeasured on the real tunnel → **R-779**. - **Apps** (sweep of all 56 apps plus Grimmory and MeTube, read in source: `A/sweep/`): - **19 router resets**, one commit each, shipped before the release. docmost on the real tunnel still throttles a rotating forger. - **BookStack `APP_PROXIES`**, with 3.6 re-measured before and after: the household is no longer throttled by the stranger. - **Gains with no change:** Home Assistant (it treated every internet visitor as "local"), actualbudget, immich, dawarich, claper, termix, Grimmory. - **Open:** kimai, zipline, vikunja, nextcloud (R-776); Emby and Jellyfin LAN rights (R-777). - **Checklist 3.10** added. ## Part B — the permanent gate (spike) `audits/permanent-gate-2026-10-01/VERDICT.md`. **PASS** on items 1–5. - 36 stranger requests, 0 reached an app. - Family members got their own 30-day logins, websockets worked, and logout worked. - A stranger's guesses locked only him. - OPDS, Kobo and KOReader worked through anchored exceptions, with the app's own login still in force. - The family login never opened the dashboard. Costs: +0.4 ms per request; with the gate down, apps answer 500 (closed); the build is 2 sessions. One build requirement: anchor every exception (`/api/v1/opdsx` slipped past an unanchored prefix; Grimmory's own login still refused it). Fully torn down. ## Part C — SparkyFitness `app-catalog-felhom.eu/onboarding/sparkyfitness.md`. - **Bench:** 5.1 server 33 %; 5.2 server 38 % and db 53 % under a heavy burst, 0 kills; 1.4 v0.17.2 → v0.17.3 proven; 2.1 CLEAN; 9.1 green. - **9202:** - A stranger got nothing during install. - Sign-up was blocked in 3 spellings, and blocked again after remove + restore. - Sign-in limit (3.6): ~10 s for everyone (R-783). - 4.4: the state read degraded within 10 s. - Backup → remove → restore read the data back. - **Open:** 0.1 licence (**R-784 — non-commercial only**), 0.5, 0.7, 1.6, 1.7, 5.4, 8.3 (R-786). The pin is 11 releases and a major behind (R-785). A licence sweep of the whole catalog is owed (R-787). ## Part D — R-772, R-773 - **R-772:** not-run → `healthy:false, not_checked:true`, plus a line on the app page. 0.286.0 still waited for the last healthy record's 5-minute interval; 0.286.1 sees it on the next tick. **Live:** not_checked 14 s after the stop, healthy 42 s after the start. Red-proofs RP-D1 and RP-D1b. Readers of `health_probe`: the state override and the API/page only; the hub never sees it. - **R-773:** a restore of a removed app writes the lock record (`opened_by: restore`) and the block before the start. **Live** on Karakeep and SparkyFitness: a stranger got 403 after remove + restore. Red-proof RP-D2. ## Part E — release, floor, golden - **Release:** controller 0.286.1. Full suite rc 0, gates green. 0.286.0 ran on 9202 only. - **Floor:** `0.286.1` + `min_agent 0.131.0` → demo-hp and demo-felhom on 0.286.1 within ~60 s. Each created the network, recreated traefik and moved cloudflared by itself (`E/rollout-*.txt`). - **Golden 0.286.1** (`documentation/tests/golden-0.286.1-2026-10-01/`): - sha `1dec051d…a89`; the round trip is equal; token leaks 0, with the positive control at 1. - Vouched: golden 0.286.1 / agent 0.138.0 / min_agent 0.131.0. The hub then refused golden 0.285.0. - `golden_currency_gate`: OK (not waived). The waiver was not renewed — the bake ran. ## Rows - **Closed:** R-753, R-754, R-772, R-773. - **Narrowed:** R-775. - **Opened:** R-776..R-787 (12). - **Register:** 410 → 422. - **Capability map:** two rows added (PARTIAL: visitors apart; MISSING/spiked: the family gate). - **`unproven.py`:** unchanged — 35 of 55 not walked. ## Teardown (three layers) - **Machine:** - 9202: karakeep, bookstack and sparkyfitness removed with their data and backups through the product; the echo containers, the spike (gate, Grimmory, MariaDB, MeTube, volumes, network, dynamic file), test images and helper files removed. 9202 keeps controller 0.286.1, its new traefik config and `felhom-tunnel` — the product's own. - demo-hp 9201: `a1-echo` and its image removed; its traefik config was restored after the M2 minute, and is now the release's. - **Host:** the bench 9401 was rebuilt and destroyed (`pvesm` before/after in `C/bench/C0`, `C9`). The drill VM is powered off and reverted to `virgin`; CT 9100 destroyed. - **Hub:** the floor and the artifact manifest changed on purpose (above); no customer or appliance records created. - **ep0:** one HTTP request, nothing else.