fix(expose): it was pointing the proxy at docker0 — the drill's own oldest trap

Incus finally said it, once the drill stopped swallowing the error:

  Connect IP "172.17.0.1" must be one of the instance's static IPv4 addresses

172.17.0.1 is DOCKER0. box_ipv4() returns whatever Incus lists first, and
a box running docker lists docker0 first — so expose has been aiming the
proxy at the wrong interface all along. This is drill trap 4, verbatim
('docker0 (172.17.x) is the decoy'), which the drill has known since run
4 and the CLI never learned. Now bin/box has its own box_net_ip(): the
address ON boxnet, with the prefix derived from the network rather than
hardcoded.

And the connect address is now the wildcard 0.0.0.0: in NAT mode Incus
resolves the instance's own current address off the bridge's neighbour
table. Naming an address makes it demand a *static* one — the very
demand that produced the error, for an address that was wrong anyway.
Ask Incus for less and it finds the box itself.
This commit is contained in:
claude-hdb 2026-07-14 20:45:09 +00:00
parent e4b546cd29
commit a5d54e4b70

29
bin/box
View file

@ -762,6 +762,21 @@ box_ipv4() { # first address only; strips Incus's " (iface)" suffix. "-" if no
| grep . || echo "-"
}
# The box's address ON BOXNET — which is NOT the same as "its first address".
# A box running docker also carries 172.17.0.1 (docker0), and Incus happily
# lists that FIRST. box_ipv4() hands you the decoy, and pointing anything at it
# is pointing at the wrong interface: 'box expose' did exactly that until Incus
# refused with `Connect IP "172.17.0.1" must be one of the instance's static
# IPv4 addresses`. The drill has known this trap since run 4; the CLI had not.
# Derive the prefix from the network rather than hardcoding it.
box_net_ip() {
local pfx
pfx="$(incus network get boxnet ipv4.address 2>/dev/null | cut -d/ -f1 | cut -d. -f1-3)"
[ -n "$pfx" ] || return 1
incus list "$1" --format csv --columns 4 2>/dev/null \
| tr -d '"' | tr ' ,' '\n\n' | grep -E "^${pfx//./\\.}\.[0-9]+$" | head -n1 | grep .
}
# VIRTUAL-MACHINE is a mouthful in a table; anything unexpected passes through.
short_type() {
case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
@ -981,8 +996,8 @@ cmd_expose() {
&& echo "box: closed the door on port $port"
# Remove the scoped ACL allow, if one was added. Best-effort: its absence
# is not an error (the drill may show the allow was never needed).
local ip; ip="$(box_ipv4 "$inst")"
[ "$ip" != "-" ] && incus network acl rule remove box-isolate ingress \
local ip; ip="$(box_net_ip "$inst" || true)"
[ -n "$ip" ] && incus network acl rule remove box-isolate ingress \
action=allow "destination=$ip/32" "destination_port=$port" protocol=tcp >/dev/null 2>&1
return 0
fi
@ -992,8 +1007,8 @@ cmd_expose() {
[ -n "$port" ] || usage_error "usage: $(synopsis_of expose)"
case "$port$hport" in *[!0-9]*) usage_error "ports must be numbers — got port='$port' host-port='$hport'" ;; esac
local ip; ip="$(box_ipv4 "$inst")"
[ "$ip" != "-" ] || die "$box has no IP yet — is it running? (box info $box)"
local ip; ip="$(box_net_ip "$inst")" \
|| die "$box has no boxnet address yet — is it running? (box info $box)"
local dev; dev="$(exposure_dev "$port")"
if incus config device get "$inst" "$dev" listen >/dev/null 2>&1; then
@ -1016,8 +1031,12 @@ cmd_expose() {
# 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)"
# connect=0.0.0.0 is deliberate: in NAT mode Incus resolves the instance's
# OWN current address off the bridge's neighbour table. Naming an address
# here instead makes Incus demand it be a *static* one — and the address we
# would have named was docker0's. Let it find the box; it knows where it is.
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:0.0.0.0:$port" \
bind=host nat=true >/dev/null 2>"$err"; then
rm -f "$err"
echo "box: 127.0.0.1:$hport → $box:$port"