# REPORT — The removal that works once, and a status page that says what it is asking for (2026-08-13) **Shipped:** `installer-v1.28.0`, published and verified against the live URL. **Register:** R-305 CLOSED (by R-316), **R-316 / R-317 / R-318 opened**; ceiling R-315 → **R-318**. **Venue:** `drill-r50` only. Neither demo box reinstalled. `peti-felhom` not contacted. Nothing deleted. --- ## 1. Cycle 3, before and after **Before — on the PUBLISHED v1.27.0, from `virgin`, exit 1:** ``` [ERROR] a resolver is already bound to :53 on this host: udp UNCONN 0 0 0.0.0.0:53 … users:(("dnsmasq",pid=7076,fd=4)) … [ERROR] a resolver is already bound to :53 on this host — Felhom needs the guest reachable by name on your LAN. Stop or reconfigure that resolver, OR point your LAN DNS at the guest's address, then re-run. (Felhom does NOT touch DNS services on a host it does not own — this is a refusal, not a change.) THIS LOOKS LIKE OURS. A previous Felhom install leaves the dnsmasq PACKAGE installed and its unit enabled (only our config snippet is removed), and unconstrained it binds 0.0.0.0:53 — which is what this gate is seeing. If this host had no dnsmasq before Felhom, clear it with: systemctl disable --now dnsmasq Then re-run this installer. If dnsmasq is YOURS, leave it and use one of the two routes above. [ERROR] PRE-FLIGHT FAIL (exit 1) — fix the finding above and re-run ``` **After — v1.28.0, three fresh cycles from `virgin`, exit 0:** ``` :53 now: 0 CYCLE 3 rc=0 [INFO] host DNS (:53): free [OK] pre-flight passed [OK] PRE-FLIGHT PASS (mode=byo) — no state written, no install step executed ``` The three cycles were run on bytes **sha256-identical** to what the live URL now serves (`cca1dedd9b7c152c…`, compared three ways: live URL, tested file, repo main). ## 2. The mechanism, at `file:line` | | | |---|---| | ownership **recorded** | `felhom-host-install.sh:1718` — preflight, `dpkg-query -W -f='${Status}' dnsmasq \| grep -q "install ok installed"` | | ownership **read** | `:1097` (`_state_get dnsmasq_preexisting`) at uninstall | | state file **deleted** | `:1268` — *after* the read. **The order was already correct.** | | Felhom's dnsmasq snippets removed | `:1162`, in the loop just above the ownership decision | **Why cycle 2 concludes "pre-existing": package presence alone.** The preflight asks dpkg one question and nothing else — it does **not** consult the absence of a record, and it cannot, because the record was deleted with the state file. v1.27.0 stopped the unit and left the package, so the answer stayed `yes` and our own package became "the household's" one cycle later. **What the first uninstall did and did not remove:** snippets — removed. Unit — stopped + disabled. State file (and with it the record) — removed. **Package — left.** That last one is the whole defect. ## 3. What v1.28.0 changes When the record says we installed it, the uninstall **removes the package** as well as stopping the unit. Order unchanged: read the record → act → delete the state. **Two packages are recorded, not one.** `dnsmasq` ships the systemd unit; **`dnsmasq-base` ships `/usr/sbin/dnsmasq`** (`dpkg -S`, measured on the box). Separately installable, so each is recorded at preflight and taken back only if we added it. **Guard rails:** ownership **read, never inferred**; the dependency check is an **`apt-get -s purge` simulation** that proceeds only if the removal set is a subset of ours, else stop+disable **naming the blocking package**; never interactive; **never fatal** — a wedged apt is recorded and restated in the closing NOTE; and the success is **re-queried** rather than read off apt's exit code. ## 4. Scenarios and red-proofs | Scenario | Result | |---|---| | **A** three cycles, fixed | install 3 **PASSES**, `:53` free at every uninstall | | **B** resolver pre-dates Felhom | **untouched** — `install ok installed`, unit active, both packages recorded `yes` | | **C** something depends on it | **not purged**; log named `household-dns-thing`; stop+disable; `:53` free; dependent survived | | **D** no ownership record | **untouched**, reason logged, exact command named | **Red-proofs — mutation asserted applied before each run:** | Mutation | Outcome | |---|---| | remove the purge call | cycle 2 records `yes`; **cycle 3 refuses, exit 1, in those exact words** | | remove the ownership check | **the household's resolver is PURGED** (`unknown ok not-installed`) | | infer ownership when no record | **the guess is taken** — a field box loses its own DNS | **One honest note on red-proof B.** Its first run appeared to pass *for the wrong reason*: apt failed with `dpkg was interrupted` (a broken state my own earlier manual package juggling left), so the resolver survived by accident rather than by the guard. I repaired dpkg and re-ran, and it then failed as required. Worth stating twice over: the mutation looked like a passing guard, and **my own fix's apt-failure path was incidentally observed doing exactly what it should** — logging, continuing, naming the command. ## 5. The machines already in the field **Measured, not reasoned.** A field box is one with the package installed and **no record** — scenario D. Its uninstall leaves the resolver running with the reason logged, and its next install refuses with the message quoted in §1. **Is there an honest durable marker? No, and none can be invented.** The Felhom `/etc/dnsmasq.d/ felhom-*.conf` snippets are deleted by the uninstall's own loop *before* the ownership decision; the state file carrying the record is deleted at `:1268`; nothing under `/etc/felhom*` survives. `/var/log/dpkg.log` does record the install and is a **timestamp** — refused by the standing rule as a heuristic dressed as a fact. → **R-318** **The preflight message, judged as a customer would:** it states the finding, keeps its two routes and its written promise not to touch DNS on a host we do not own, and adds the one thing that gets a person moving — *"THIS LOOKS LIKE OURS"* plus one exact command. It hedges correctly (*looks like*). Its weakness is that it asks them *"did this host have dnsmasq before Felhom?"* — precisely the question we can no longer answer for them. **That is a mechanism, not a rule: the command is on the screen at the moment it is needed.** ## 6. The defect I nearly shipped The agent decides whether to install dnsmasq with `os.Stat("/usr/sbin/dnsmasq")` (`felhom-agent/internal/lanresolver/lanresolver.go:105`) — but that path belongs to **`dnsmasq-base`**, while the unit comes from **`dnsmasq`**. Purging only `dnsmasq` would leave the binary, so the next install would skip the apt step and then fail to enable a unit that is gone — **a silent resolver where today there is at least a visible refusal.** That is why ownership is recorded per package. It remains reachable where `dnsmasq-base` pre-dated Felhom (we correctly keep it). **Pre-existing, not introduced here**, filed as **R-317**, and the uninstall now says so out loud rather than leaving it to be found from a resolver that never came up. Fixing it is a one-line agent change, deliberately **not** made here to keep this session to one repo. ## 7. Publication `installer-v1.28.0` tagged; **both** `--ref`s in `manifests/webpage.yaml` moved (sidecar 327, init 372); ArgoCD synced to `823cd29`; live deployment carries the new ref. ``` live URL serves: SCRIPT_VERSION="1.28.0" (was 1.27.0) _dnsmasq_purge_owned present in the served file: 3 ``` **Pushing publishes nothing here** — `/scripts/` follows the tag. The runbook still says otherwise; **R-309 remains open and is not forgotten**, deliberately out of scope. ## 8. Teardown — four layers | Layer | State | |---|---| | **machine** | `drill-r50`: 0 guests, no `/var/lib/felhom-install`, dnsmasq `not-installed` at the end | | **host** | `drill-r50` **reverted to snapshot `virgin`, powered off**; `qemu.pid` removed. DooPlex was drill host only | | **hub** | `drill-r50-0a4f9a` **RETAINED** — pre-existing since 2026-07-25, re-used not duplicated. **No new host or customer row was created this session.** Nothing deleted | | **off-site** | **Not contacted at all** — no restic, sftp or endpoint call was made. `demo-felhom`: `last_status=ok`, `escrow=escrowed`, **no abandon countdown** | Both demo boxes untouched and healthy: controller **0.214.0**, agent **0.129.0**, `Up … (healthy)`. ## 9. Evidence, and a repeated miss Logs at `documentation/audits/evidence-r316-2026-08-13/`: the fixed cycles (`F1`, `F2`, `F3`), all four scenarios (`B`, `C`, `D`), and every red-proof (`RPA*`, `RPB*`, `RPD*`). **The Part 1 logs did not survive.** They were on the drill VM's disk and were destroyed by the revert to `virgin` between Part 1 and Part 2 — **the same mistake as Tuesday, in the same place.** The §1 quotation is verbatim from the live run as read at the time, and `RPA1/RPA2/RPA3.log` are an independent reproduction of the identical three-cycle failure, retained. Recorded rather than glossed; the fix is procedural and I have now got it wrong twice. ## 10. Observations — noticed, not acted on - **`dnsmasq-base` is flagged "automatically installed and no longer required"** after `dnsmasq` goes. We deliberately do not `autoremove` — that would be a blast radius nobody asked for. - **`apt-get -s purge` exits 0 even when it prints `E: dpkg was interrupted`.** The simulation output is the signal, never the exit code — which is why the guard parses `Remv`/`Purg` lines rather than trusting `$?`. Another entry for the exit-codes-that-lie class. - The drill VM's `pveam` index is stale on `virgin` and needs `pveam update` before listing templates — already recorded yesterday, hit again today in passing.