From 2c03624013d98812c2c3c700ce132c867ebe9184 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Tue, 14 Jul 2026 11:58:36 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(doctor):=20the=20gateway=20does=20not?= =?UTF-8?q?=20answer=20ping=20=E2=80=94=20by=20design,=20so=20stop=20askin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claudebox-firewall.sh drops everything from a box to the host except DNS (53) and DHCP (67); ICMP to 10.87.0.1 dies in that trailing drop on every healthy host. The doctor used exactly that ping as its routing probe, so it reported 'cannot even reach the gateway' — and a NOT-fit- to-drill verdict — on a host whose very next line proved DNS working through that same gateway. Probe routing the way the contract states it: a box reaches the public internet. curl to 1.1.1.1 by address, reusing the one probe for the DNS-failure diagnosis instead of running it twice. --- drill/doctor.sh | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/drill/doctor.sh b/drill/doctor.sh index 718c777..ee20e23 100755 --- a/drill/doctor.sh +++ b/drill/doctor.sh @@ -212,17 +212,23 @@ if [ -n "$probe" ] && [ "$FIX" != 1 ]; then inf "probing inside '$probe' — this separates DNS from routing, which is the whole question:" inf "its resolv.conf: $(timeout -k 5 20 incus exec "$probe" -- sh -c 'grep -m2 nameserver /etc/resolv.conf' /dev/null | tr '\n' ' ')" - timeout -k 5 20 incus exec "$probe" -- ping -c1 -W2 10.87.0.1 /dev/null 2>&1 \ - && ok "reaches the gateway (10.87.0.1) — routing is fine" \ - || no "cannot even reach the gateway — this is routing, not DNS" + # Routing is probed by ADDRESS against the public internet, NOT by pinging + # the gateway: claudebox-firewall.sh drops everything from a box to the host + # except DNS/DHCP, so ICMP to 10.87.0.1 fails BY DESIGN on a healthy host. + # A gateway ping here is a check that can only ever lie. + if timeout -k 5 25 incus exec "$probe" -- curl -sS -m 10 -o /dev/null https://1.1.1.1 /dev/null; then + routing=1; ok "reaches 1.1.1.1 by address — egress routing is fine" + else + routing=0; no "cannot reach 1.1.1.1 by address — egress routing is broken (this is not DNS)" + fi if timeout -k 5 25 incus exec "$probe" -- getent hosts deb.debian.org /dev/null 2>&1; then ok "resolves deb.debian.org — DNS works" else no "CANNOT resolve deb.debian.org — this is exactly what kills cloud-init on every cold mint" - # Does the box reach the internet at all WITHOUT DNS? If yes, the fault is - # purely name resolution — i.e. the forwarder, i.e. issue #33. - if timeout -k 5 25 incus exec "$probe" -- curl -sS -m 10 -o /dev/null https://1.1.1.1 /dev/null; then + # Egress by address was probed above. If it worked, the fault is purely + # name resolution — i.e. the forwarder, i.e. issue #33. + if [ "$routing" = 1 ]; then inf "…but it CAN reach 1.1.1.1 by address. So egress works and only NAME RESOLUTION is broken:" inf "the fault is the forwarder the box inherits from the host — issue #33." inf "test the fix: bash drill/doctor.sh --pin-dns then re-run the drill" -- 2.45.2 From 730d7da54ee47af1c759ccb31aae32f488171712 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Tue, 14 Jul 2026 12:09:44 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(drill):=20stop=20demanding=20an=20empty?= =?UTF-8?q?=20host=20=E2=80=94=20assert=20OUR=20boxes,=20not=20NO=20boxes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 11: 45 passed, 2 failed — and both failures were the same non-fact. The host had two operator boxes (t1, t2) minted before the run, so 'claudebox list' could not say 'no boxes yet' at either end of the drill. The empty-host message is only testable on an actually-empty host: skip it (with a note) when tenants pre-exist, instead of failing it. The teardown check contradicted its own rm loop: the loop deliberately removes only the names the drill minted — the multi-tenant discipline — and then the assertion demanded a globally empty host, flagging that very restraint as a failure. Assert the drill's own names are gone; name the survivor when one is. --- drill/drill.sh | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/drill/drill.sh b/drill/drill.sh index 4c5e07f..6b1f000 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -348,8 +348,17 @@ expected="$(cat "$HOME/.local/share/claudebox/VERSION" 2>/dev/null || echo '?')" v="$(claudebox --version 2>&1)" case "$v" in *"$expected"*) ok "claudebox --version → $v" ;; *) no "version mismatch: CLI says '$v', VERSION file says '$expected'" ;; esac -claudebox list >/dev/null 2>&1 && claudebox list 2>&1 | grep -q 'no boxes yet' \ - && ok "empty host: 'no boxes yet', exit 0" || no "empty-host message wrong" +# The drill must not require an empty host: operator boxes tagged +# user.claudebox=1 are legitimate tenants, and the teardown below deliberately +# refuses to touch them. The empty-host message is only TESTABLE when the host +# is actually empty — on a shared host, skip it instead of failing it. +tenants="$(incus list user.claudebox=1 --format csv --columns n 2>/dev/null | tr '\n' ' ')" +if [ -n "${tenants% }" ]; then + inf "host already has claudebox boxes (${tenants% }) — the empty-host message cannot be tested this run" +else + claudebox list >/dev/null 2>&1 && claudebox list 2>&1 | grep -q 'no boxes yet' \ + && ok "empty host: 'no boxes yet', exit 0" || no "empty-host message wrong" +fi printf '\n minting a box (cold, ~10 min)…\n' t0=$SECONDS @@ -603,7 +612,12 @@ if [ "$KEEP" = 1 ]; then else # every name the drill can have left, whatever branch a partial run took for n in drill clone archive peer; do claudebox rm "$n" --force >/dev/null 2>&1; done - claudebox list 2>&1 | grep -q 'no boxes yet' && ok "teardown: no boxes left" || no "a box survived teardown" + # Assert OUR boxes are gone — not that the host is empty. The rm loop above + # already embodies the discipline (only names the drill minted); demanding + # 'no boxes yet' here would flag any pre-existing operator box as a failure. + leftover="$(claudebox list 2>/dev/null | grep -E '^(drill|clone|archive|peer)([[:space:]]|$)' || true)" + [ -z "$leftover" ] && ok "teardown: every box the drill minted is gone" \ + || no "a drill box survived teardown: $(printf '%s' "$leftover" | awk '{print $1}' | tr '\n' ' ')" fi phase "Summary" -- 2.45.2