Merge pull request #91 from dan-claude-bot/feat/subnet-autopick
feat(setup-host): auto-pick a free subnet — nested box-in-box with zero flags (#80)
This commit is contained in:
commit
88db8cab04
6 changed files with 278 additions and 73 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -7,6 +7,23 @@ which records not just what changed but what each drill run proved.
|
|||
|
||||
### Added
|
||||
|
||||
- **`setup-host` auto-picks a free subnet — nested box-in-box with zero
|
||||
flags** (#80, completing its fix #1: "refuse … or automatically select a
|
||||
non-colliding subnet"). A bare `box setup-host` now decides the subnet
|
||||
itself, in four deliberate cases: an explicit `BOX_SUBNET` is honored or
|
||||
refused, never silently overridden (scripted hosts keep exact semantics);
|
||||
an existing `boxnet` bridge is converged on as-is — the bridge IS the pin —
|
||||
turning the old bare-re-run agree-gate refusal into plain convergence
|
||||
(unless a foreigner *also* claims the bridge's subnet: that is #80's
|
||||
poisoned state, and converging would rebuild on it, so it still refuses and
|
||||
names the bridge move); a free `10.88.0.0/24` stays the default; and a
|
||||
*claimed* default — the nested case: a drill or rehearsal running inside a
|
||||
box, whose own uplink owns 10.88 — scans `10.89.0.0/24` … `10.127.0.0/24`
|
||||
in order, takes the first free candidate, announces the pick and the
|
||||
claimant loudly, and only refuses when every candidate is claimed. The
|
||||
decision happens before any mutation, and everything downstream (the
|
||||
bridge, `BOX_GW`, the ACL's gateway carve-out, the firewall, the doctor's
|
||||
expectations) derives from it.
|
||||
- **`setup-host` refuses a claimed subnet, and `BOX_SUBNET` picks another**
|
||||
(#80) — run inside a box, `setup-host` used to build a nested `boxnet` on
|
||||
the exact subnet and gateway of the guest's own uplink: the guest then held
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -129,19 +129,23 @@ re-apply at boot via `box-firewall.service` — no post-reboot ritual. If
|
|||
the host lacks `dnsmasq-base` (Debian cloud images skip Recommends):
|
||||
`sudo apt-get install -y dnsmasq-base`.
|
||||
|
||||
The stack's subnet is `10.88.0.0/24` by default; `BOX_SUBNET` picks another
|
||||
`/24` (`BOX_SUBNET=10.89.0.0/24 box setup-host` — the bridge address, the
|
||||
ACL's gateway carve-out and the firewall all derive from it). setup-host
|
||||
**refuses to build on a subnet something already claims** — most tellingly
|
||||
when this machine's own default gateway sits inside it, which means it is
|
||||
being run *inside a box*: a nested `boxnet` on the guest's own uplink subnet
|
||||
captures its gateway address and blackholes the guest's egress in
|
||||
intermittent, maddening-to-attribute blackouts
|
||||
([#80](https://github.com/heavy-duty/box/issues/80)). `BOX_SUBNET` is the
|
||||
sanctioned way out for a nested or otherwise-conflicted install, and
|
||||
`box doctor` recognizes the poisoned state (a gateway held as a local
|
||||
address, duplicate uplink routes) on the machine it runs on and inside every
|
||||
box it probes.
|
||||
The stack's subnet is `10.88.0.0/24` when free. setup-host **never builds on
|
||||
a subnet something else already claims** — most tellingly when this machine's
|
||||
own default gateway sits inside it, which means it is being run *inside a
|
||||
box*: a nested `boxnet` on the guest's own uplink subnet captures its gateway
|
||||
address and blackholes the guest's egress in intermittent,
|
||||
maddening-to-attribute blackouts
|
||||
([#80](https://github.com/heavy-duty/box/issues/80)). Instead of refusing, a
|
||||
bare `box setup-host` decides for itself: an existing `boxnet` bridge is
|
||||
converged on as-is (the bridge is the pin — it is never re-addressed), and a
|
||||
claimed default triggers an auto-pick of the first free `/24` from
|
||||
`10.89.0.0/24` through `10.127.0.0/24`, announced loudly — so drills and
|
||||
rehearsals *inside a box* work with zero flags. `BOX_SUBNET=<a.b.c.0/24>`
|
||||
pins the subnet explicitly for scripted hosts (the bridge address, the ACL's
|
||||
gateway carve-out and the firewall all derive from it); a pin is honored or
|
||||
refused, never silently overridden. `box doctor` recognizes the poisoned
|
||||
state (a gateway held as a local address, duplicate uplink routes) on the
|
||||
machine it runs on and inside every box it probes.
|
||||
|
||||
A host still carrying the pre-0.4.0 stack: `box migrate-host --all-boxes`
|
||||
re-homes each legacy box onto `boxnet` (authed state preserved), and
|
||||
|
|
|
|||
23
bin/box
23
bin/box
|
|
@ -502,16 +502,21 @@ install.sh runs it for you, so this is for re-applying by hand.
|
|||
One run is enough. If it has to add you to the incus-admin group it re-runs
|
||||
itself under that group — no re-login, no second invocation.
|
||||
|
||||
The stack's subnet is 10.88.0.0/24 by default; BOX_SUBNET picks another /24
|
||||
(the bridge, the gateway carve-out and the firewall all derive from it). It
|
||||
REFUSES, before touching anything, where the target subnet is already claimed
|
||||
— most tellingly when this machine's own default gateway sits inside it,
|
||||
i.e. when you are running setup-host INSIDE a box: a nested stack on the
|
||||
guest's own uplink subnet captures its gateway address and blackholes its
|
||||
egress, intermittently (issue #80). The sanctioned way to nest:
|
||||
The stack's subnet: 10.88.0.0/24 when free; with an existing boxnet it
|
||||
converges on the bridge's own subnet; and when the default is claimed by
|
||||
something else — most tellingly this machine's own default gateway, i.e.
|
||||
setup-host running INSIDE a box — it auto-picks the first free /24 from
|
||||
10.89.0.0/24 through 10.127.0.0/24 and says so. A nested stack on the
|
||||
guest's own uplink subnet would capture its gateway address and blackhole
|
||||
its egress, intermittently (issue #80); the auto-pick is why a drill or
|
||||
rehearsal inside a box now works with zero flags. BOX_SUBNET pins the
|
||||
subnet explicitly (the bridge, the gateway carve-out and the firewall all
|
||||
derive from it) — a pin is never overridden: setup-host REFUSES, before
|
||||
touching anything, when the pinned subnet is claimed by a foreigner or
|
||||
disagrees with an existing bridge.
|
||||
|
||||
box setup-host
|
||||
BOX_SUBNET=10.89.0.0/24 box setup-host # nested, or a conflicted host
|
||||
box setup-host # picks/converges by itself
|
||||
BOX_SUBNET=10.90.0.0/24 box setup-host # scripted hosts: pin it
|
||||
|
||||
Multi-user hosts: setup-host builds the stack once, for everyone. An admin
|
||||
then hands individual users the restricted tier with 'box grant <user>' —
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@ if [ -n "$sig" ]; then
|
|||
while IFS= read -r line; do no "$line"; done <<<"$sig"
|
||||
inf "a box stack was built on a machine whose uplink already owns its subnet —"
|
||||
inf "run inside a box, that is issue #80: egress blacks out intermittently while"
|
||||
inf "everything looks healthy. setup-host now refuses this; this machine already has it."
|
||||
inf "everything looks healthy. setup-host now auto-picks a free subnet for the"
|
||||
inf "nested case, so this stack predates the fix (or was pinned onto the uplink)."
|
||||
inf "fix: move the nested bridge off the uplink's subnet:"
|
||||
inf " sudo incus network set boxnet ipv4.address 10.89.0.1/24"
|
||||
inf " (or remove the nested stack: box teardown-host)"
|
||||
|
|
|
|||
|
|
@ -45,15 +45,17 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# --- The subnet, and the refusal to build on one something already owns -----
|
||||
# --- The subnet: never build on one something already owns ------------------
|
||||
# (#80.) The stack's subnet was hardcoded, and running setup-host INSIDE a box
|
||||
# gave the guest a nested boxnet claiming the exact subnet and gateway of its
|
||||
# own uplink: the guest then held its gateway's address as a LOCAL address,
|
||||
# carried two connected routes for the subnet, and suffered intermittent,
|
||||
# self-recovering egress blackouts nobody could attribute — the host looked
|
||||
# clean the whole time. The flagship use case funnels agents toward doing
|
||||
# exactly this (working on box, in a box), so the guard must refuse BEFORE
|
||||
# any mutation, and name the way out (BOX_SUBNET).
|
||||
# exactly this (working on box, in a box), so the decision must happen BEFORE
|
||||
# any mutation. An explicit BOX_SUBNET is honored or refused, never overridden;
|
||||
# with no pin, choose_subnet below converges on an existing bridge or picks a
|
||||
# free /24 itself — a drill inside a box now just works, zero flags.
|
||||
|
||||
# BOX_SUBNET must be a /24 with a zero host octet — a.b.c.0/24. Everything
|
||||
# the stack derives (the bridge address, the gateway carve-out, the firewall)
|
||||
|
|
@ -93,40 +95,109 @@ subnet_claimant() {
|
|||
[ -n "$hit" ] && printf '%s\n' "$hit"
|
||||
}
|
||||
|
||||
BOX_SUBNET="${BOX_SUBNET:-10.88.0.0/24}"
|
||||
if ! valid_subnet "$BOX_SUBNET"; then
|
||||
echo "ERROR: BOX_SUBNET='$BOX_SUBNET' is not a sane subnet — the stack takes a" >&2
|
||||
echo " /24 with a zero host octet, e.g. BOX_SUBNET=10.89.0.0/24" >&2
|
||||
exit 1
|
||||
fi
|
||||
# The one place the stack's subnet is decided. Four deliberate cases (#80's
|
||||
# fix #1, completed — the refusal shipped first, this adds the auto-pick):
|
||||
# 1. explicit BOX_SUBNET — use it; a foreign claimant or a disagreeing
|
||||
# bridge still REFUSES. An operator's pin is never silently overridden:
|
||||
# a script that says 10.90 gets 10.90 or a loud stop, never a surprise.
|
||||
# 2. no pin, boxnet exists — converge to the bridge's own subnet: the
|
||||
# bridge IS the pin (boxes hold leases on it; setup-host never
|
||||
# re-addresses it). What used to be an agree-gate refusal on a bare
|
||||
# re-run against a moved bridge is now plain convergence. A FOREIGN
|
||||
# claimant on the bridge's own subnet still refuses — that is #80's
|
||||
# poisoned state, and converging would rebuild on it.
|
||||
# 3. no pin, no bridge, 10.88.0.0/24 free — the default, as always.
|
||||
# 4. no pin, no bridge, default claimed — the nested case (a drill or
|
||||
# rehearsal inside a box): scan 10.89.0.0/24 … 10.127.0.0/24 in order,
|
||||
# take the first free candidate, and say so loudly; refuse only when
|
||||
# EVERY candidate is claimed. The scan only ever runs bridge-less —
|
||||
# an existing bridge is case 2, which precedes it.
|
||||
# Prints the chosen subnet on stdout, explains itself on stderr, fails when
|
||||
# it refuses. Everything downstream (BOX_GW, the bridge, the ACL carve-out,
|
||||
# the firewall, the doctor's expectations) derives from the choice, which is
|
||||
# why it happens here, before any of them. Pure over `ip` (via
|
||||
# subnet_claimant and the bridge read), so test/cli.sh drives every case
|
||||
# against canned tables with a shim ip.
|
||||
choose_subnet() {
|
||||
local pin="$1" have_gw have_sub hit cand b
|
||||
# ('|| true': under pipefail, `ip … dev boxnet` on a fresh host — no such
|
||||
# device — would kill the script here instead of answering "no bridge".)
|
||||
have_gw="$(ip -4 -o addr show dev boxnet 2>/dev/null | awk '{ split($4, a, "/"); print a[1]; exit }' || true)"
|
||||
have_sub="${have_gw:+${have_gw%.*}.0/24}"
|
||||
|
||||
if [ -n "$pin" ]; then
|
||||
if ! valid_subnet "$pin"; then
|
||||
echo "ERROR: BOX_SUBNET='$pin' is not a sane subnet — the stack takes a" >&2
|
||||
echo " /24 with a zero host octet, e.g. BOX_SUBNET=10.89.0.0/24" >&2
|
||||
return 1
|
||||
fi
|
||||
if hit="$(subnet_claimant "$pin")"; then
|
||||
echo "ERROR: refusing to build boxnet on $pin — that subnet is already" >&2
|
||||
echo " claimed here by $hit." >&2
|
||||
echo " If that is this machine's uplink, you are INSIDE a box: a nested" >&2
|
||||
echo " stack on the guest's own subnet captures its gateway address and" >&2
|
||||
echo " blackholes its egress, intermittently (issue #80)." >&2
|
||||
echo " Nothing was changed. Drop the pin to let setup-host auto-pick a" >&2
|
||||
echo " free subnet, or pick one yourself: BOX_SUBNET=<a.b.c.0/24> box setup-host" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$have_sub" ] && [ "$have_sub" != "$pin" ]; then
|
||||
echo "ERROR: boxnet already exists on $have_sub and the target is $pin —" >&2
|
||||
echo " setup-host converges an existing bridge, it never re-addresses one." >&2
|
||||
echo " Re-run with the bridge's own subnet (a bare 'box setup-host'" >&2
|
||||
echo " converges on it automatically):" >&2
|
||||
echo " BOX_SUBNET=$have_sub box setup-host" >&2
|
||||
echo " (or move the bridge first: incus network set boxnet ipv4.address ${pin%.0/24}.1/24)" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$pin"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "$have_sub" ]; then
|
||||
if hit="$(subnet_claimant "$have_sub")"; then
|
||||
echo "ERROR: boxnet lives on $have_sub, but that subnet is ALSO claimed here" >&2
|
||||
echo " by $hit — the #80 poisoned state. Converging would rebuild on it." >&2
|
||||
echo " Move the bridge off the claimed subnet first:" >&2
|
||||
echo " incus network set boxnet ipv4.address 10.89.0.1/24" >&2
|
||||
echo " then re-run: box setup-host" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ "$have_sub" != 10.88.0.0/24 ]; then
|
||||
echo "boxnet already lives on $have_sub — converging to it." >&2
|
||||
echo "(pin it explicitly with BOX_SUBNET=$have_sub if you script this host)" >&2
|
||||
fi
|
||||
printf '%s\n' "$have_sub"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! hit="$(subnet_claimant 10.88.0.0/24)"; then
|
||||
printf '10.88.0.0/24\n'
|
||||
return 0
|
||||
fi
|
||||
for b in {89..127}; do
|
||||
cand="10.$b.0.0/24"
|
||||
subnet_claimant "$cand" >/dev/null && continue
|
||||
echo "10.88.0.0/24 is claimed here by $hit —" >&2
|
||||
echo "most likely this machine IS a box (a nested drill or rehearsal, issue #80)." >&2
|
||||
echo "auto-picked $cand for this stack instead." >&2
|
||||
echo "(pin it explicitly with BOX_SUBNET=$cand if you script this host)" >&2
|
||||
printf '%s\n' "$cand"
|
||||
return 0
|
||||
done
|
||||
echo "ERROR: refusing to build boxnet — 10.88.0.0/24 is already claimed here by" >&2
|
||||
echo " $hit, and so is every candidate through 10.127.0.0/24." >&2
|
||||
echo " If that first claimant is this machine's uplink, you are INSIDE a" >&2
|
||||
echo " box: a nested stack on the guest's own subnet captures its gateway" >&2
|
||||
echo " address and blackholes its egress, intermittently (issue #80)." >&2
|
||||
echo " Nothing was changed. Pick a free subnet yourself:" >&2
|
||||
echo " BOX_SUBNET=<a.b.c.0/24> box setup-host" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
BOX_SUBNET="$(choose_subnet "${BOX_SUBNET:-}")" || exit 1
|
||||
BOX_GW="${BOX_SUBNET%.0/24}.1"
|
||||
|
||||
if hit="$(subnet_claimant "$BOX_SUBNET")"; then
|
||||
echo "ERROR: refusing to build boxnet on $BOX_SUBNET — that subnet is already" >&2
|
||||
echo " claimed here by $hit." >&2
|
||||
echo " If that is this machine's uplink, you are INSIDE a box: a nested" >&2
|
||||
echo " stack on the guest's own subnet captures its gateway address and" >&2
|
||||
echo " blackholes its egress, intermittently (issue #80)." >&2
|
||||
echo " Nothing was changed. To build a nested stack anyway, pick a free" >&2
|
||||
echo " subnet: BOX_SUBNET=10.89.0.0/24 box setup-host" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A bridge this script built before is the one claimant that is NOT a
|
||||
# collision — but it must AGREE with the target: setup-host converges an
|
||||
# existing bridge, it never re-addresses one (boxes hold leases on it).
|
||||
# ('|| true': under pipefail, `ip … dev boxnet` on a fresh host — no such
|
||||
# device — would kill the script right here instead of answering "no bridge".)
|
||||
have_gw="$(ip -4 -o addr show dev boxnet 2>/dev/null | awk '{ split($4, a, "/"); print a[1]; exit }' || true)"
|
||||
if [ -n "$have_gw" ] && [ "$have_gw" != "$BOX_GW" ]; then
|
||||
echo "ERROR: boxnet already exists on ${have_gw%.*}.0/24 and the target is $BOX_SUBNET —" >&2
|
||||
echo " setup-host converges an existing bridge, it never re-addresses one." >&2
|
||||
echo " Re-run with the bridge's own subnet:" >&2
|
||||
echo " BOX_SUBNET=${have_gw%.*}.0/24 box setup-host" >&2
|
||||
echo " (or move the bridge first: incus network set boxnet ipv4.address $BOX_GW/24)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# apt, unattended-safe. install.sh now runs us without a human watching, and
|
||||
# a fresh cloud image has apt-daily/unattended-upgrades holding the dpkg lock
|
||||
# for the first minutes of its life — plain 'apt-get install' then waits on it
|
||||
|
|
@ -229,8 +300,9 @@ fi
|
|||
# Isolated NAT network. IPv6 off: one less egress path to reason about.
|
||||
# The default is 10.88 — not 10.87: a pre-rename host may still carry
|
||||
# claudenet on 10.87 with legacy boxes attached — two bridges must not claim
|
||||
# one subnet. BOX_SUBNET (validated and cleared by the #80 guard above) picks
|
||||
# another /24; the gateway and every rule below derive from it.
|
||||
# one subnet. BOX_SUBNET holds whatever choose_subnet decided above (an
|
||||
# explicit pin, the existing bridge, the default, or an auto-picked free
|
||||
# /24); the gateway and every rule below derive from it.
|
||||
incus network show boxnet >/dev/null 2>&1 || incus network create boxnet \
|
||||
ipv4.address="$BOX_GW/24" ipv4.nat=true ipv6.address=none
|
||||
|
||||
|
|
|
|||
132
test/cli.sh
132
test/cli.sh
|
|
@ -677,6 +677,101 @@ check "claimant: 10.8.0.0/24 does not prefix-match 10.88.x (the dot terminates)"
|
|||
1 "" claim 10.8.0.0/24 "$D_INBOX" "$A_GUEST"
|
||||
rm -f "$CLMFN"
|
||||
|
||||
# --- choose_subnet: the four-case decision, driven case by case -------------
|
||||
# 1 explicit pin: honored or refused, never overridden. 2 no pin + bridge:
|
||||
# converge to the bridge (the bridge IS the pin) — the scan never runs with a
|
||||
# bridge present. 3 no pin, no bridge, default free: default. 4 default
|
||||
# claimed: scan 10.89…10.127, first free wins, loudly; refuse when all claimed.
|
||||
PICKFN="$(mktemp)"
|
||||
awk '/^(valid_subnet|subnet_claimant|choose_subnet)\(\) \{/,/^\}/' \
|
||||
"$ROOT/host/setup-host.sh" > "$PICKFN"
|
||||
check "choose_subnet: extracted with its helpers (guards the awk)" 0 "auto-picked" cat "$PICKFN"
|
||||
check "choose_subnet: subnet_claimant came along" 0 "DEFAULT GATEWAY" cat "$PICKFN"
|
||||
check "choose_subnet: the extracted functions are valid bash" 0 "" bash -n "$PICKFN"
|
||||
pick() { # pick <pin> <default-route> <addrs> [boxnet-addr]
|
||||
FAKE_IP4_DEFAULT="$2" FAKE_IP4_ADDRS="$3" FAKE_IP4_BOXNET="${4:-}" PATH="$SHIMDIR:$PATH" \
|
||||
bash -c ". '$PICKFN'; choose_subnet \"\$1\"" _ "$1"
|
||||
}
|
||||
pickout() { pick "$@" 2>/dev/null; } # stdout only: the choice itself
|
||||
pickquiet() { [ -z "$(pick "$@" 2>&1 >/dev/null)" ]; } # stderr must be EMPTY
|
||||
picknoscan(){ ! pick "$@" 2>&1 | grep -qF auto-picked; }
|
||||
|
||||
# The bridge lines and the both-claimed / all-claimed address tables.
|
||||
B_88='5: boxnet inet 10.88.0.1/24 scope global boxnet'
|
||||
B_89='5: boxnet inet 10.89.0.1/24 scope global boxnet'
|
||||
A_TWOCLAIM="$A_GUEST
|
||||
3: virbr7 inet 10.89.0.7/24 brd 10.89.0.255 scope global virbr7"
|
||||
A_ALLCLAIM="$(for b in $(seq 88 127); do
|
||||
printf '%d: virbr%d inet 10.%d.0.7/24 brd 10.%d.0.255 scope global virbr%d\n' \
|
||||
"$((b - 85))" "$((b - 87))" "$b" "$b" "$((b - 87))"
|
||||
done)"
|
||||
|
||||
# Case 1 — the pin. Refusals identical in spirit to the pre-autopick gate.
|
||||
check "pick: pinned + gw-in-subnet REFUSES, names issue #80" \
|
||||
1 "issue #80" pick 10.88.0.0/24 "$D_INBOX" "$A_GUEST"
|
||||
check "pick: pinned + foreign interface REFUSES, names it" \
|
||||
1 "virbr7" pick 10.88.0.0/24 "$D_LAN" "$A_FOREIGN"
|
||||
check "pick: a pinned refusal still names BOX_SUBNET" \
|
||||
1 "BOX_SUBNET" pick 10.88.0.0/24 "$D_INBOX" "$A_GUEST"
|
||||
check "pick: pinned against a disagreeing bridge REFUSES (never re-addresses)" \
|
||||
1 "never re-addresses" pick 10.88.0.0/24 "$D_LAN" "$A_HOSTSTACK" "$B_89"
|
||||
check "pick: a garbage pin is refused by name" \
|
||||
1 "not a sane subnet" pick banana "$D_LAN" "$A_HOSTSTACK"
|
||||
check "pick: a pin that clears the gate is used verbatim" \
|
||||
0 "10.89.0.0/24" pickout 10.89.0.0/24 "$D_INBOX" "$A_GUEST"
|
||||
check "pick: ...silently — a pin is the operator talking, not us" \
|
||||
0 "" pickquiet 10.89.0.0/24 "$D_INBOX" "$A_GUEST"
|
||||
|
||||
# Case 2 — no pin, a bridge: converge to ITS subnet. No refusal, no scan —
|
||||
# even when the default is claimed (THIS machine: nested stack, uplink on
|
||||
# 10.88, bridge remapped to 10.89 — the #80 workaround host, bare re-run).
|
||||
check "pick: bridge present converges to the bridge's own subnet" \
|
||||
0 "10.89.0.0/24" pickout "" "$D_INBOX" "$A_GUEST
|
||||
$B_89" "$B_89"
|
||||
check "pick: ...announcing the convergence (an off-default bridge is worth a line)" \
|
||||
0 "converging" pick "" "$D_INBOX" "$A_GUEST
|
||||
$B_89" "$B_89"
|
||||
check "pick: ...and the scan never ran (case 2 precedes case 4)" \
|
||||
0 "" picknoscan "" "$D_INBOX" "$A_GUEST
|
||||
$B_89" "$B_89"
|
||||
check "pick: bridge on the DEFAULT subnet converges silently (plain re-run)" \
|
||||
0 "" pickquiet "" "$D_LAN" "$A_HOSTSTACK" "$B_88"
|
||||
check "pick: ...to the default" \
|
||||
0 "10.88.0.0/24" pickout "" "$D_LAN" "$A_HOSTSTACK" "$B_88"
|
||||
# The poisoned state (#80 verbatim: bridge AND uplink both on 10.88) must not
|
||||
# converge — rebuilding there re-arms the blackouts. Refuse, name the fix.
|
||||
check "pick: a bridge on a FOREIGN-claimed subnet refuses (the poisoned state)" \
|
||||
1 "poisoned" pick "" "$D_INBOX" "$A_GUEST
|
||||
$B_88" "$B_88"
|
||||
check "pick: ...naming the bridge move as the fix" \
|
||||
1 "ipv4.address" pick "" "$D_INBOX" "$A_GUEST
|
||||
$B_88" "$B_88"
|
||||
|
||||
# Case 3 — no pin, no bridge, default free: the default, silently.
|
||||
check "pick: a free default host gets 10.88.0.0/24" \
|
||||
0 "10.88.0.0/24" pickout "" "$D_LAN" ""
|
||||
check "pick: ...with no announcement" 0 "" pickquiet "" "$D_LAN" ""
|
||||
|
||||
# Case 4 — no pin, no bridge, default claimed: the nested case. First free
|
||||
# candidate wins, the announcement names the claimant and the pin.
|
||||
check "pick: default claimed by the gateway auto-picks 10.89.0.0/24" \
|
||||
0 "10.89.0.0/24" pickout "" "$D_INBOX" "$A_GUEST"
|
||||
check "pick: ...saying so loudly" \
|
||||
0 "auto-picked 10.89.0.0/24" pick "" "$D_INBOX" "$A_GUEST"
|
||||
check "pick: ...naming WHY (the machine's own gateway = inside a box)" \
|
||||
0 "DEFAULT GATEWAY" pick "" "$D_INBOX" "$A_GUEST"
|
||||
check "pick: ...and how to pin it for scripts" \
|
||||
0 "BOX_SUBNET=10.89.0.0/24" pick "" "$D_INBOX" "$A_GUEST"
|
||||
check "pick: default AND 10.89 claimed skips to 10.90.0.0/24" \
|
||||
0 "10.90.0.0/24" pickout "" "$D_INBOX" "$A_TWOCLAIM"
|
||||
check "pick: every candidate claimed → the old refusal" \
|
||||
1 "refusing to build boxnet" pick "" "$D_LAN" "$A_ALLCLAIM"
|
||||
check "pick: ...naming the end of the scan range" \
|
||||
1 "10.127.0.0/24" pick "" "$D_LAN" "$A_ALLCLAIM"
|
||||
check "pick: ...and BOX_SUBNET as the way out" \
|
||||
1 "BOX_SUBNET" pick "" "$D_LAN" "$A_ALLCLAIM"
|
||||
rm -f "$PICKFN"
|
||||
|
||||
# --- the whole script, driven: refuse-before-mutation, converge, plumb-through
|
||||
SETUPSHIM="$(mktemp -d)"
|
||||
cat > "$SETUPSHIM/incus" <<'SHIM'
|
||||
|
|
@ -712,19 +807,21 @@ runsetup() { # runsetup [VAR=val ...] — the real setup-host, under shims
|
|||
}
|
||||
|
||||
W80="$(mktemp -d)"
|
||||
# Refusal 1: the default gateway sits inside the target — the inside of a box.
|
||||
check "setup-host: gw-in-subnet REFUSES and names issue #80" 1 "issue #80" \
|
||||
runsetup FAKE_IP4_DEFAULT="$D_INBOX" FAKE_IP4_ADDRS="$A_GUEST" \
|
||||
# Refusal 1: an EXPLICIT pin on the subnet the default gateway sits inside —
|
||||
# the inside of a box, and the operator said 10.88 out loud. A pin is never
|
||||
# silently overridden, so this refuses exactly as it did pre-autopick.
|
||||
check "setup-host: a pinned gw-claimed subnet REFUSES and names issue #80" 1 "issue #80" \
|
||||
runsetup BOX_SUBNET=10.88.0.0/24 FAKE_IP4_DEFAULT="$D_INBOX" FAKE_IP4_ADDRS="$A_GUEST" \
|
||||
FAKE_INCUS_LOG="$W80/g1.log" FAKE_SUDO_LOG="$W80/s1.log"
|
||||
check "setup-host: ...naming BOX_SUBNET as the way out" 1 "BOX_SUBNET" \
|
||||
runsetup FAKE_IP4_DEFAULT="$D_INBOX" FAKE_IP4_ADDRS="$A_GUEST"
|
||||
runsetup BOX_SUBNET=10.88.0.0/24 FAKE_IP4_DEFAULT="$D_INBOX" FAKE_IP4_ADDRS="$A_GUEST"
|
||||
check "setup-host: the refusal made NO incus call (refuse precedes mutation)" 1 "" \
|
||||
test -e "$W80/g1.log"
|
||||
check "setup-host: the refusal made NO sudo call either" 1 "" \
|
||||
test -e "$W80/s1.log"
|
||||
# Refusal 2: a foreign interface owns an address inside the target.
|
||||
check "setup-host: a foreign interface in the subnet REFUSES" 1 "virbr7" \
|
||||
runsetup FAKE_IP4_DEFAULT="$D_LAN" FAKE_IP4_ADDRS="$A_FOREIGN"
|
||||
# Refusal 2: a pin on a subnet a foreign interface owns an address inside.
|
||||
check "setup-host: a pinned foreign-claimed subnet REFUSES" 1 "virbr7" \
|
||||
runsetup BOX_SUBNET=10.88.0.0/24 FAKE_IP4_DEFAULT="$D_LAN" FAKE_IP4_ADDRS="$A_FOREIGN"
|
||||
# Refusal 3: garbage BOX_SUBNET dies at the gate.
|
||||
check "setup-host: a garbage BOX_SUBNET is refused by name" 1 "not a sane subnet" \
|
||||
runsetup BOX_SUBNET=banana
|
||||
|
|
@ -751,16 +848,25 @@ check "setup-host: ...the bridge derives from BOX_SUBNET" 0 "" \
|
|||
grep -qF 'network create boxnet ipv4.address=10.89.0.1/24' "$W80/g2.log"
|
||||
check "setup-host: ...and so does the ACL's gateway carve-out" 0 "" \
|
||||
grep -qF 'destination: 10.89.0.1/32' "$W80/g2.log"
|
||||
# ...which also proves the guard scans the TARGET subnet: the same tables that
|
||||
# refused the default (gw 10.88.0.1) pass once BOX_SUBNET moves off it — the
|
||||
# issue's workaround host, sanctioned.
|
||||
# The nested case with ZERO flags — #80's tables, no pin, no bridge: the
|
||||
# auto-pick must land the whole build on 10.89, announced, and every derived
|
||||
# value must follow the pick, not the default.
|
||||
check "setup-host: nested with no flags auto-picks and completes" 0 "Host ready" \
|
||||
runsetup FAKE_IP4_DEFAULT="$D_INBOX" FAKE_IP4_ADDRS="$A_GUEST" \
|
||||
FAKE_INCUS_LOG="$W80/g3.log" FAKE_SUDO_LOG="$W80/s3.log"
|
||||
check "setup-host: ...announcing the auto-pick" 0 "auto-picked 10.89.0.0/24" \
|
||||
runsetup FAKE_IP4_DEFAULT="$D_INBOX" FAKE_IP4_ADDRS="$A_GUEST"
|
||||
check "setup-host: ...the bridge follows the pick" 0 "" \
|
||||
grep -qF 'network create boxnet ipv4.address=10.89.0.1/24' "$W80/g3.log"
|
||||
check "setup-host: ...the ACL carve-out follows the pick" 0 "" \
|
||||
grep -qF 'destination: 10.89.0.1/32' "$W80/g3.log"
|
||||
rm -rf "$W80" "$SETUPSHIM"
|
||||
|
||||
# The guard must be the FIRST effective act — before the incus install, the
|
||||
# The decision must be the FIRST effective act — before the incus install, the
|
||||
# usermod, every apt call. Line order, fail-closed on either grep missing.
|
||||
# shellcheck disable=SC2016 # the $-strings are literals in the target file
|
||||
check "setup-host: the subnet guard precedes the first mutation" 0 "" bash -c '
|
||||
guard="$(grep -n "subnet_claimant \"\$BOX_SUBNET\"" "'"$ROOT"'/host/setup-host.sh" | head -1 | cut -d: -f1)"
|
||||
check "setup-host: the subnet decision precedes the first mutation" 0 "" bash -c '
|
||||
guard="$(grep -n "^BOX_SUBNET=\"\$(choose_subnet " "'"$ROOT"'/host/setup-host.sh" | head -1 | cut -d: -f1)"
|
||||
mut="$(grep -n "^if ! command -v incus" "'"$ROOT"'/host/setup-host.sh" | head -1 | cut -d: -f1)"
|
||||
[ -n "$guard" ] && [ -n "$mut" ] && [ "$guard" -lt "$mut" ]'
|
||||
# box-firewall follows the bridge, wherever BOX_SUBNET put it.
|
||||
|
|
|
|||
Loading…
Reference in a new issue