Session close: REPORT-visitors-2026-10-01, STATUS (go/no-go: the family gate; SparkyFitness licence), CONTEXT, capability map rows, R-783..R-787 (register 422)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-01 22:19:50 +02:00
parent 09ae93db05
commit 27f4e7d976
5 changed files with 197 additions and 47 deletions
+139
View File
@@ -0,0 +1,139 @@
# 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.