# Inbound-firewall verify/repair — JIT plan (releases#43)

Lane `fix/install-firewall-verify-repair`, worktree `.worktrees/lane-43`, based on fetched
`origin/main` @d7411f9. Spec = releases#43 body + doyle's L3 ruling (2026-07-31, in-thread) +
the lane's scope comment on #43.

## The defect

`REQ-INSTALL-7` shipped both halves of the M8 story — the elevated installer registers a
program-scoped inbound-UDP rule, and `inbound_block_hint()` renders a warning when it is
absent. Four gap classes let that pair read green while the node is unreachable inbound:

1. **Unelevated install warns and continues** — the rule never lands and nothing re-checks.
2. **The rule binds ONE exe path** — a moved binary or a dev-build daemon binds the socket
   from outside the rule. (Checked on HFENDULEAM 2026-07-31: the rule names
   `%LOCALAPPDATA%\spt-core\bin\spt.exe` and the resident daemon runs that same installed
   binary, so this host MATCHES. An earlier draft of this plan claimed a live mismatch here;
   measurement refuted it. The class holds by construction — a program-scoped rule admits
   exactly one path — and is pinned by unit fixture, not by a field specimen.)
3. **No Linux host-firewall leg at all** (ufw / nftables / firewalld untouched).
4. **Provider firewalls** (DO cloud) are unreachable by install by design.

Class 2 is what makes this a hazard-7.61 instance, not a gap list: the probe asks *does a
rule NAMED `spt-core inbound UDP` exist*, and a rule pointing at the wrong binary satisfies
that question while being exactly the fault. **Green must assert the rule covers the binary
that BINDS the socket** — the resident daemon's exe, never the invoking CLI's `argv[0]`
(doyle's condition: a repair keyed to the caller would rewrite the rule onto a dev path and
mint hazard-7.61 instance 3 in the same diff that cites it).

## Ruled scope (L3)

| leg | ships here |
|-----|-----------|
| (a) verify-and-repair | **Windows: both.** Verify = rule program path == resident daemon exe. Repair (elevated) rewrites the rule to THAT path. Unelevated = loud, no write. No resident daemon = "cannot say", never a guessed path. |
| (b) Linux | **Verify only.** Detect the active host firewall (ufw / nft / firewalld), verify inbound UDP against the CURRENTLY bound port, render the exact command. Zero writes. |
| (c) self-detect + render | Folded into (a): the render reads the binder-pinned verdict instead of probing by name. DEFERRED.md's M7-acceptance row is promoted out of deferred. |
| (d) provider firewalls | Docs leg — inbound UDP for QUIC as a stated hosting requirement. |
| L1 Linux auto-repair | **NOT here** — needs a pinned UDP port (prod binds ephemeral; `BindScope::Port` is test-scope only). Relocated to releases#54, its own wave and gate. |

## Shape

- **`spt-store/src/inbound.rs`** — the verdict RECORD. Written by the binder, read by any
  renderer. Carries the binder's pid AND image path, and a reader re-derives validity from
  the process table: the pid must still resolve to an image and that image must be the
  recorded one, or the record **is discarded**. Staleness must not be able to produce green
  (7.61 again: a daemon that restarted on a new port and failed to re-verify must read
  "unknown", not "fine"), and a pid alone is recyclable.

  The pin does NOT key on `<spt_home>/daemon.pid`. That breadcrumb is best-effort by contract
  and is written BEFORE the socket bind that arbitrates single-daemon-per-home, so a losing
  starter overwrites the winner's value on its way out. Measured on HFENDULEAM 2026-07-31:
  the file named pid 40332, dead since startup, while the daemon actually serving was 23416,
  started one second earlier. A first draft of this lane gated the verify on breadcrumb
  equality, which on this host would have disabled the feature outright.
- **`spt-daemon/src/firewall.rs`** — probes + pure decision seams:
  - Windows: bounded `netsh advfirewall firewall show rule name=… verbose`, parse the
    `Program:` line, decide `Ok | Missing | PathMismatch{rule_path} | ProbeFailed`.
  - Linux: bounded `ufw status` / `nft list ruleset` / `firewall-cmd --state`, decide
    `NoFirewall | Allows | Blocks{cmd} | ProbeFailed` against the bound port.
  - Repair (Windows, elevated only): delete + add keyed to the daemon exe.
  - Every subprocess bounded — KNOWN-HAZARDS 5.3 / `REQ-HAZARD-SUBPROCESS-TIMEOUT`.
- **`spt-daemon/src/nethost.rs`** — expose `bound_port()` off the live endpoint.
- **`spt-daemon/src/daemon.rs`** — fire the verify at `try_start_net`'s successful bind, NOT
  inside `NetHost::start`: the CLI starts short-lived net hosts too (rc, wansend), and a
  repair keyed to one of those would point the rule at whichever build ran a command. This
  call site is unambiguously the daemon binding the endpoint it will serve on, and it re-runs
  on the REQ-DAEMON-9 self-heal retry, whose port is new.
- **`spt/src/cli.rs`** — `inbound_block_hint()` reads the record; renders path-mismatch and
  the Linux command. Keeps the "silent on a broken probe" discipline.

## Requirements minted (all in `[[groups]] install-bootstrap`, rule-7 scope-at-mint)

- `REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH` — doc/impl/unit
- `REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY` — doc/impl/unit  (N4: platform axis named in the
  same position as its Windows sibling)
- `REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED` — impl/unit
- `REQ-INBOUND-PROVIDER-FIREWALL-DOCS` — doc
- `REQ-NET-PINNED-UDP-PORT` — `required_stages = []`, the parked L1 seed (releases#54)

## Verification

Workspace clippy (CI form), the unit suites, `traceable-reqs check`, `xtask check`. Both new
rigs are mutation-proven rather than merely green: restoring name-presence semantics fails the
mismatch and unknown assertions, and disabling the binder pin fails the staleness assertion.

**Full-suite status, stated rather than implied:** the workspace `nextest` run on this host
did NOT complete — it failed to link with `rustc-LLVM ERROR: IO failure on output stream: no
space on device` (15.9 GB free against a 19.4 GB lane target). The unit suites of the touched
crates ran green. The full suite is owed from a box with disk, or from doyle's gate.
