README's setup-host section documents the subnet input and the refusal;
'box help setup-host' names BOX_SUBNET and the nested-box trap; 'box help
doctor' names the #80 signature it now judges; the changelog carries the
full story, including that suggested fix 4 (the agent-context guard note
for the templates) lands in heavy-duty/rig#31's bootstrap roles per the
thin-templates split (#81).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two pure functions are extracted and driven, the same seam as box_tier
and load_template: a shim ip serves canned route tables (the poisoned
guest verbatim from #80's capture, the remapped-bridge workaround, a
healthy stack host), and each signature line is proven to fire alone and
to stay silent on the clean states.
Then the WHOLE setup-host is driven end to end under shims (fake incus and
sudo log every call; fake id keeps it unprivileged): the three refusals
(gateway-in-subnet, foreign interface, garbage/wrong-shape BOX_SUBNET)
exit 1 having made NO incus and NO sudo call — refuse-before-mutation is
asserted on the logs' absence, not assumed — the existing-bridge mismatch
refuses, the legitimate converge (boxnet's own prior claim) still reaches
'Host ready', and a fresh BOX_SUBNET=10.89.0.0/24 build lands
ipv4.address=10.89.0.1/24 on the bridge and destination: 10.89.0.1/32 in
the ACL, proving every derived value follows the one input.
Line-order guards pin the wiring: the subnet guard precedes the first
mutation in setup-host, the machine-local signature precedes the doctor's
daemon checks, probe_sig runs on both tiers, box-firewall reads the
gateway off the live bridge, and the drill/multiuser/migrate probes derive
their prefix from the network.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The doctor had already CAUGHT #80 without knowing it: 'DIRTY cannot reach
1.1.1.1 by address' beside 'OK resolves deb.debian.org' — and attributed
it to the host admin, whose every check was green. That split IS the
fingerprint: the impostor dnsmasq on the captured gateway address keeps
answering names (it forwards upstream via the default route) while direct
IP egress dies.
Three additions:
· gw_squat_signature — pure text (ip -4 route show / ip -4 -o addr
show) in, findings out: a default gateway held as a LOCAL address
(the kernel's local table then eats packets meant for the real
gateway — DNS, unicast DHCP renewals), and duplicate connected
routes for the uplink subnet (whichever link last gains carrier
wins, and a nested bridge with carrier blackholes egress). Pure so
test/cli.sh drives it against synthetic tables, including the
issue's exact captures.
· judged on THIS machine first, both tiers, before any daemon call —
inside a poisoned guest the daemon answering is the nested impostor,
happily judging its own stack clean — and probed INSIDE every box
the doctor examines (probe_sig), where the signature actually lives.
· the admin ACL section now compares the gateway carve-out to boxnet's
actual gateway: the #80 escape hatch moves the bridge, and the stale
/32 strands box DNS inside the 10.0.0.0/8 drop. Measured live on a
workaround host: doctor names the stale carve-out and the
BOX_SUBNET re-run that converges it.
The egress-broken-but-DNS-fine hint now names itself on both tiers and
points at the signature probes for the verdict.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run inside a box, setup-host built a nested boxnet claiming the exact
subnet and gateway of the guest's own uplink: the guest then held
10.88.0.1 as a LOCAL address while its default route pointed at 10.88.0.1
as its GATEWAY, carried two connected routes for the subnet, and suffered
intermittent, self-recovering egress blackouts (~24-36 s, roughly hourly)
that no host-side check could attribute. The flagship use case funnels
agents toward exactly this — working on box, in a box — so the guard, not
the operator, has to catch it.
setup-host now scans the target subnet BEFORE any mutation: the default
route's gateway inside it (the smoking gun — that is this machine's own
uplink), or any non-boxnet interface holding an address in it, refuses
with the way out named (BOX_SUBNET). A prior boxnet owning the subnet is
the one claimant that is NOT a collision — the legitimate re-run — and an
existing bridge on a DIFFERENT subnet refuses too: setup-host converges a
bridge, it never re-addresses one under live leases.
BOX_SUBNET=<a.b.c.0/24> (validated at the gate, alongside BOX_DNS) moves
the whole stack from one place:
· the bridge address derives (ipv4.address=<gw>/24)
· the ACL's gateway carve-out derives — and the ruleset is now converged
via 'network acl edit', not created once: the #80 escape hatch (moving
the bridge) left the old /32 behind, stranding box DNS inside the
10.0.0.0/8 drop, which presents as a dead resolver, never as a stale
ACL
· box-firewall reads the gateway off the live bridge ('|| true' because
under pipefail an absent bridge would kill the script instead of
answering "no bridge yet")
· the drill, multiuser rehearsal and migrate-host probes derive the
prefix from 'incus network get boxnet ipv4.address' instead of
hardcoding 10.88 — a BOX_SUBNET host must not fail its own rehearsals
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>