fix: expose asks for too much, and the fake legacy box had no claude user

Run 18: 74/2. grok passes (reading the installer worked), migrate's
retire passes. Two left, both mine:

1. box expose — read the Incus proxy docs instead of guessing again.
   Proxy IS supported on VMs, but NAT mode only (correct already), and
   crucially it does NOT need a static address: with no static IP, Incus
   reads the box's address off the bridge's neighbour table and keeps
   the NAT rules in step. My first cut pinned the lease with a device
   override anyway — unnecessary, and almost certainly the step that was
   failing before the proxy was ever added. Dropped it. Ask for less.

   The drill also stopped throwing incus's reason away: it swallowed
   stderr, then RE-RAN the command and printed only the last line ('box:
   expose failed') — box's own words, never incus's. It now captures the
   first attempt and prints all of it.

2. 'legacy box never came up' was unfixable by any timeout. wait_box
   uses 'box exec', which for a legacy-tagged box resolves the user to
   'claude' — and the drill's synthetic legacy box was a BARE image with
   no claude user, so sudo -u claude could never answer, on a box that
   was perfectly healthy (every migration check against it passed). The
   fake legacy box now creates a claude user, like a real pre-0.4.0 box
   had.
This commit is contained in:
claude-hdb 2026-07-14 20:30:01 +00:00
parent 8729c3e522
commit e4b546cd29
2 changed files with 22 additions and 19 deletions

26
bin/box
View file

@ -1005,23 +1005,17 @@ cmd_expose() {
incus network acl rule add box-isolate ingress action=allow \ incus network acl rule add box-isolate ingress action=allow \
"destination=$ip/32" "destination_port=$port" protocol=tcp >/dev/null 2>&1 || true "destination=$ip/32" "destination_port=$port" protocol=tcp >/dev/null 2>&1 || true
# A VM's proxy device needs NAT mode — the plain userspace forkproxy is a # A VM's proxy device must be NAT mode — Incus supports proxy on containers in
# container thing. NAT mode DNATs host:port → instance:port in netfilter, and # both modes, but on VMs "NAT mode only" (the userspace forkproxy is a
# it REQUIRES the instance's NIC to hold a static address, so pin the lease # container thing). NAT mode DNATs host:port → instance:port in netfilter, and
# the box already has. Pinning also fixes the restart caveat: the exposure # it needs the host to be the instance's gateway, which boxnet makes true.
# survives a reboot instead of pointing at a lease the box no longer owns. #
# It does NOT need a static address: with no static IP, Incus reads the box's
# current address off the bridge's neighbour table and keeps the NAT rules in
# step. The first cut of this pinned the lease with a device override anyway —
# unnecessary, and it is what made expose fail before it ever reached the
# proxy. Ask for less; the docs said so.
local err; err="$(mktemp)" local err; err="$(mktemp)"
if ! incus config device override "$inst" eth0 "ipv4.address=$ip" >/dev/null 2>"$err"; then
# Already overridden (a second exposure on the same box) is fine.
grep -qi 'already' "$err" || {
echo "box: could not pin the box's address (needed for a VM proxy):" >&2
sed 's/^/ /' "$err" >&2; rm -f "$err"
incus network acl rule remove box-isolate ingress action=allow \
"destination=$ip/32" "destination_port=$port" protocol=tcp >/dev/null 2>&1
die "expose failed"
}
fi
if incus config device add "$inst" "$dev" proxy \ if incus config device add "$inst" "$dev" proxy \
"listen=tcp:127.0.0.1:$hport" "connect=tcp:$ip:$port" \ "listen=tcp:127.0.0.1:$hport" "connect=tcp:$ip:$port" \
bind=host nat=true >/dev/null 2>"$err"; then bind=host nat=true >/dev/null 2>"$err"; then

View file

@ -714,7 +714,8 @@ if incus file push "$srv" archive/tmp/srv.js >/dev/null 2>&1; then
# nothing holds its stdout (trap 2/3). The listener outlives the exec. # nothing holds its stdout (trap 2/3). The listener outlives the exec.
timeout -k 5 20 incus exec archive -- sh -c 'setsid node /tmp/srv.js >/tmp/srv.log 2>&1 </dev/null &' </dev/null timeout -k 5 20 incus exec archive -- sh -c 'setsid node /tmp/srv.js >/tmp/srv.log 2>&1 </dev/null &' </dev/null
sleep 3 sleep 3
if box expose archive "$EP" "$EHP" >/dev/null 2>&1; then xlog="$(mktemp)"
if box expose archive "$EP" "$EHP" >"$xlog" 2>&1; then
ok "box expose archive $EP $EHP — the device was added" ok "box expose archive $EP $EHP — the device was added"
box expose archive --list 2>/dev/null | grep -q "$EP" \ box expose archive --list 2>/dev/null | grep -q "$EP" \
&& ok "expose --list shows the open door" || no "expose --list does not show the exposure" && ok "expose --list shows the open door" || no "expose --list does not show the exposure"
@ -741,9 +742,12 @@ if incus file push "$srv" archive/tmp/srv.js >/dev/null 2>&1; then
&& no "the host still reaches the box after --remove — the door did not shut" \ && no "the host still reaches the box after --remove — the door did not shut" \
|| ok "after --remove, 127.0.0.1:$EHP is dead — the door shut" || ok "after --remove, 127.0.0.1:$EHP is dead — the door shut"
else else
no "box expose failed to add the device — tail: $(box expose archive "$EP" "$EHP" 2>&1 | tail -1)" no "box expose failed to add the device"
inf "what box and incus actually said:"
sed 's/^/ /' "$xlog" 2>/dev/null
rm -f "$srv" 2>/dev/null rm -f "$srv" 2>/dev/null
fi fi
rm -f "$xlog" 2>/dev/null
timeout -k 5 15 incus exec archive -- pkill -f srv.js </dev/null >/dev/null 2>&1 timeout -k 5 15 incus exec archive -- pkill -f srv.js </dev/null >/dev/null 2>&1
else else
rm -f "$srv" rm -f "$srv"
@ -807,9 +811,14 @@ else
# A minimal legacy box: no template payload, just boots and networks on the # A minimal legacy box: no template payload, just boots and networks on the
# old stack, wearing the old tag. This is what migrate has to move. # old stack, wearing the old tag. This is what migrate has to move.
printf '\n minting a faithful legacy box on the old stack…\n' printf '\n minting a faithful legacy box on the old stack…\n'
# The legacy box must carry a 'claude' user, because that is what a real
# pre-0.4.0 box had — and box_user() maps the legacy tag to it. Without the
# user, 'box exec' (sudo -u claude) can never answer and wait_box fails
# forever on a box that is perfectly healthy. Run 17/18 lost a FAIL to this.
if mint_legacy=$(incus launch images:debian/13/cloud legacybox --profile claude-dev \ if mint_legacy=$(incus launch images:debian/13/cloud legacybox --profile claude-dev \
--config user.claudebox=1 --vm --device root,size=20GiB \ --config user.claudebox=1 --vm --device root,size=20GiB \
--config security.secureboot=false 2>&1); then --config security.secureboot=false \
--config cloud-init.user-data="$(printf '#cloud-config\nusers:\n - name: claude\n shell: /bin/bash\n sudo: "ALL=(ALL) NOPASSWD:ALL"\n lock_passwd: true\n')" 2>&1); then
wait_box legacybox && ok "legacy box up on the old stack (claudenet, user.claudebox=1)" \ wait_box legacybox && ok "legacy box up on the old stack (claudenet, user.claudebox=1)" \
|| no "legacy box never came up — cannot drill migration" || no "legacy box never came up — cannot drill migration"
box list 2>/dev/null | grep -q '^legacybox' \ box list 2>/dev/null | grep -q '^legacybox' \