fix(doctor): a fresh host is not a dirty one #62

Merged
dan-claude-bot merged 1 commit from fix/doctor-fresh-host into main 2026-07-15 00:57:48 +00:00
3 changed files with 27 additions and 3 deletions

View file

@ -65,7 +65,7 @@ installer retires — but nothing current carries the old name.
- **Debrand complete** — env vars, install dir, docs, template descriptions - **Debrand complete** — env vars, install dir, docs, template descriptions
and the README all say `box`; the install URL is and the README all say `box`; the install URL is
`heavy-duty/box` (GitHub redirects the old one, `BOX_REPO` overrides). `heavy-duty/box` (GitHub redirects the old one, `BOX_REPO` overrides).
- **The drill grew from 47 to 83 checks** — the expose door opened, exercised - **The drill grew from 47 to 84 checks** — the expose door opened, exercised
and shut (with the contract re-probed around it), every template minted and shut (with the contract re-probed around it), every template minted
cold, a faithful pre-0.4.0 box re-homed through `migrate-host`, and the cold, a faithful pre-0.4.0 box re-homed through `migrate-host`, and the
inline resource flags asserted (including their precedence over the inline resource flags asserted (including their precedence over the

View file

@ -252,7 +252,7 @@ drill installs the whole stack, mints every template cold, snapshots and
clones, probes every boundary from inside the boxes, opens and shuts the clones, probes every boundary from inside the boxes, opens and shuts the
`expose` door (and checks the contract survives it), re-homes a faithful `expose` door (and checks the contract survives it), re-homes a faithful
pre-0.4.0 box through `migrate-host`, and removes what it minted — pre-0.4.0 box through `migrate-host`, and removes what it minted —
currently **83 checks**. [drill/RUNS.md](drill/RUNS.md) is the full currently **84 checks, 84 passing**. [drill/RUNS.md](drill/RUNS.md) is the full
history, including every trap that fooled a run into a wrong verdict. history, including every trap that fooled a run into a wrong verdict.
### Run the drill yourself ### Run the drill yourself

View file

@ -40,6 +40,12 @@ timeout 10 incus list >/dev/null 2>&1 || {
exit 1 exit 1
} }
# A FRESH host (no boxnet) is not a DIRTY one. Everything below that would
# scream about a missing piece must first ask: missing from a stack, or never
# set up? setup-host creates all of it, and the drill runs setup-host itself —
# a bare host went 84/84 green minutes after this doctor called it unfit.
FRESH=0
head_ "Network — boxnet" head_ "Network — boxnet"
if incus network show boxnet >/dev/null 2>&1; then if incus network show boxnet >/dev/null 2>&1; then
# dns.mode=none is SHIPPED — it is what stops a box enumerating its siblings # dns.mode=none is SHIPPED — it is what stops a box enumerating its siblings
@ -79,12 +85,15 @@ if incus network show boxnet >/dev/null 2>&1; then
[ "$ipv6" = none ] && ok "ipv6.address = none (the isolation contract — every ACL rule is IPv4-only)" \ [ "$ipv6" = none ] && ok "ipv6.address = none (the isolation contract — every ACL rule is IPv4-only)" \
|| no "ipv6.address = $ipv6 — IPv6 is on and NOT covered by any ACL rule" || no "ipv6.address = $ipv6 — IPv6 is on and NOT covered by any ACL rule"
else else
FRESH=1
inf "boxnet does not exist (a fresh host — setup-host.sh will create it)" inf "boxnet does not exist (a fresh host — setup-host.sh will create it)"
fi fi
head_ "Firewall — the box-to-box drop" head_ "Firewall — the box-to-box drop"
if sudo nft list table bridge box >/dev/null 2>&1; then if sudo nft list table bridge box >/dev/null 2>&1; then
ok "nft bridge table 'box' is present — boxes cannot reach each other" ok "nft bridge table 'box' is present — boxes cannot reach each other"
elif [ "$FRESH" = 1 ]; then
inf "not installed yet (a fresh host — setup-host.sh installs it)"
else else
no "the box-to-box drop is MISSING — boxes can reach each other" no "the box-to-box drop is MISSING — boxes can reach each other"
inf "an L3 ACL never sees frames switched between two ports of one bridge;" inf "an L3 ACL never sees frames switched between two ports of one bridge;"
@ -186,6 +195,16 @@ raw="$(incus network get boxnet raw.dnsmasq 2>/dev/null | tr '\n' ';')"
if [ -n "$raw" ]; then if [ -n "$raw" ]; then
ok "boxnet has a pinned resolver (raw.dnsmasq: $raw)" ok "boxnet has a pinned resolver (raw.dnsmasq: $raw)"
inf "boxes do NOT inherit the host's resolver — good (issue #33)" inf "boxes do NOT inherit the host's resolver — good (issue #33)"
elif [ "$FRESH" = 1 ]; then
# No boxnet, so there is nothing to pin YET. A VPN resolver on the host is
# worth naming, but it is a fact about the host, not a fault in a stack
# that does not exist — setup-host pins around it at creation.
if printf '%s' "$hostns" | grep -qE '(^| )100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.'; then
inf "the host resolves via a CGNAT/Tailscale resolver ($hostns) — noted, not a fault:"
inf "setup-host pins the box resolver to public upstreams, so boxes will not inherit it (issue #33)"
else
inf "no boxnet yet — setup-host.sh pins its resolver at creation"
fi
else else
# 100.64.0.0/10 is CGNAT — which is exactly Tailscale's range. # 100.64.0.0/10 is CGNAT — which is exactly Tailscale's range.
if printf '%s' "$hostns" | grep -qE '(^| )100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.'; then if printf '%s' "$hostns" | grep -qE '(^| )100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.'; then
@ -255,7 +274,12 @@ fi
head_ "Verdict" head_ "Verdict"
if [ "$bad" -eq 0 ]; then if [ "$bad" -eq 0 ]; then
printf ' \033[32mclean\033[0m — this host is fit to mint boxes (and to drill).\n\n' if [ "$FRESH" = 1 ]; then
printf ' \033[32mfresh\033[0m — no box stack on this host yet, and nothing dirty either.\n'
printf ' run: box setup-host (or the drill — it sets the host up itself)\n\n'
else
printf ' \033[32mclean\033[0m — this host is fit to mint boxes (and to drill).\n\n'
fi
exit 0 exit 0
fi fi
printf ' \033[31m%s problem(s)\033[0m — this host is NOT fit to mint boxes (or to drill).\n' "$bad" printf ' \033[31m%s problem(s)\033[0m — this host is NOT fit to mint boxes (or to drill).\n' "$bad"