feat(setup-host): auto-pick a free subnet — nested box-in-box with zero flags (#80) #91
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
|
### 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**
|
- **`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
|
(#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
|
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):
|
the host lacks `dnsmasq-base` (Debian cloud images skip Recommends):
|
||||||
`sudo apt-get install -y dnsmasq-base`.
|
`sudo apt-get install -y dnsmasq-base`.
|
||||||
|
|
||||||
The stack's subnet is `10.88.0.0/24` by default; `BOX_SUBNET` picks another
|
The stack's subnet is `10.88.0.0/24` when free. setup-host **never builds on
|
||||||
`/24` (`BOX_SUBNET=10.89.0.0/24 box setup-host` — the bridge address, the
|
a subnet something else already claims** — most tellingly when this machine's
|
||||||
ACL's gateway carve-out and the firewall all derive from it). setup-host
|
own default gateway sits inside it, which means it is being run *inside a
|
||||||
**refuses to build on a subnet something already claims** — most tellingly
|
box*: a nested `boxnet` on the guest's own uplink subnet captures its gateway
|
||||||
when this machine's own default gateway sits inside it, which means it is
|
address and blackholes the guest's egress in intermittent,
|
||||||
being run *inside a box*: a nested `boxnet` on the guest's own uplink subnet
|
maddening-to-attribute blackouts
|
||||||
captures its gateway address and blackholes the guest's egress in
|
([#80](https://github.com/heavy-duty/box/issues/80)). Instead of refusing, a
|
||||||
intermittent, maddening-to-attribute blackouts
|
bare `box setup-host` decides for itself: an existing `boxnet` bridge is
|
||||||
([#80](https://github.com/heavy-duty/box/issues/80)). `BOX_SUBNET` is the
|
converged on as-is (the bridge is the pin — it is never re-addressed), and a
|
||||||
sanctioned way out for a nested or otherwise-conflicted install, and
|
claimed default triggers an auto-pick of the first free `/24` from
|
||||||
`box doctor` recognizes the poisoned state (a gateway held as a local
|
`10.89.0.0/24` through `10.127.0.0/24`, announced loudly — so drills and
|
||||||
address, duplicate uplink routes) on the machine it runs on and inside every
|
rehearsals *inside a box* work with zero flags. `BOX_SUBNET=<a.b.c.0/24>`
|
||||||
box it probes.
|
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`
|
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
|
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
|
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.
|
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 stack's subnet: 10.88.0.0/24 when free; with an existing boxnet it
|
||||||
(the bridge, the gateway carve-out and the firewall all derive from it). It
|
converges on the bridge's own subnet; and when the default is claimed by
|
||||||
REFUSES, before touching anything, where the target subnet is already claimed
|
something else — most tellingly this machine's own default gateway, i.e.
|
||||||
— most tellingly when this machine's own default gateway sits inside it,
|
setup-host running INSIDE a box — it auto-picks the first free /24 from
|
||||||
i.e. when you are running setup-host INSIDE a box: a nested stack on the
|
10.89.0.0/24 through 10.127.0.0/24 and says so. A nested stack on the
|
||||||
guest's own uplink subnet captures its gateway address and blackholes its
|
guest's own uplink subnet would capture its gateway address and blackhole
|
||||||
egress, intermittently (issue #80). The sanctioned way to nest:
|
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 setup-host # picks/converges by itself
|
||||||
BOX_SUBNET=10.89.0.0/24 box setup-host # nested, or a conflicted host
|
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
|
Multi-user hosts: setup-host builds the stack once, for everyone. An admin
|
||||||
then hands individual users the restricted tier with 'box grant <user>' —
|
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"
|
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 "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 "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 "fix: move the nested bridge off the uplink's subnet:"
|
||||||
inf " sudo incus network set boxnet ipv4.address 10.89.0.1/24"
|
inf " sudo incus network set boxnet ipv4.address 10.89.0.1/24"
|
||||||
inf " (or remove the nested stack: box teardown-host)"
|
inf " (or remove the nested stack: box teardown-host)"
|
||||||
|
|
|
||||||
|
|
@ -45,15 +45,17 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# (#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
|
# 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,
|
# own uplink: the guest then held its gateway's address as a LOCAL address,
|
||||||
# carried two connected routes for the subnet, and suffered intermittent,
|
# carried two connected routes for the subnet, and suffered intermittent,
|
||||||
# self-recovering egress blackouts nobody could attribute — the host looked
|
# self-recovering egress blackouts nobody could attribute — the host looked
|
||||||
# clean the whole time. The flagship use case funnels agents toward doing
|
# 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
|
# exactly this (working on box, in a box), so the decision must happen BEFORE
|
||||||
# any mutation, and name the way out (BOX_SUBNET).
|
# 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
|
# 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)
|
# the stack derives (the bridge address, the gateway carve-out, the firewall)
|
||||||
|
|
@ -93,40 +95,109 @@ subnet_claimant() {
|
||||||
[ -n "$hit" ] && printf '%s\n' "$hit"
|
[ -n "$hit" ] && printf '%s\n' "$hit"
|
||||||
}
|
}
|
||||||
|
|
||||||
BOX_SUBNET="${BOX_SUBNET:-10.88.0.0/24}"
|
# The one place the stack's subnet is decided. Four deliberate cases (#80's
|
||||||
if ! valid_subnet "$BOX_SUBNET"; then
|
# fix #1, completed — the refusal shipped first, this adds the auto-pick):
|
||||||
echo "ERROR: BOX_SUBNET='$BOX_SUBNET' is not a sane subnet — the stack takes a" >&2
|
# 1. explicit BOX_SUBNET — use it; a foreign claimant or a disagreeing
|
||||||
echo " /24 with a zero host octet, e.g. BOX_SUBNET=10.89.0.0/24" >&2
|
# bridge still REFUSES. An operator's pin is never silently overridden:
|
||||||
exit 1
|
# a script that says 10.90 gets 10.90 or a loud stop, never a surprise.
|
||||||
fi
|
# 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"
|
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
|
# 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
|
# 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
|
# 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.
|
# 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
|
# 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
|
# 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
|
# one subnet. BOX_SUBNET holds whatever choose_subnet decided above (an
|
||||||
# another /24; the gateway and every rule below derive from it.
|
# 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 \
|
incus network show boxnet >/dev/null 2>&1 || incus network create boxnet \
|
||||||
ipv4.address="$BOX_GW/24" ipv4.nat=true ipv6.address=none
|
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"
|
1 "" claim 10.8.0.0/24 "$D_INBOX" "$A_GUEST"
|
||||||
rm -f "$CLMFN"
|
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
|
# --- the whole script, driven: refuse-before-mutation, converge, plumb-through
|
||||||
SETUPSHIM="$(mktemp -d)"
|
SETUPSHIM="$(mktemp -d)"
|
||||||
cat > "$SETUPSHIM/incus" <<'SHIM'
|
cat > "$SETUPSHIM/incus" <<'SHIM'
|
||||||
|
|
@ -712,19 +807,21 @@ runsetup() { # runsetup [VAR=val ...] — the real setup-host, under shims
|
||||||
}
|
}
|
||||||
|
|
||||||
W80="$(mktemp -d)"
|
W80="$(mktemp -d)"
|
||||||
# Refusal 1: the default gateway sits inside the target — the inside of a box.
|
# Refusal 1: an EXPLICIT pin on the subnet the default gateway sits inside —
|
||||||
check "setup-host: gw-in-subnet REFUSES and names issue #80" 1 "issue #80" \
|
# the inside of a box, and the operator said 10.88 out loud. A pin is never
|
||||||
runsetup FAKE_IP4_DEFAULT="$D_INBOX" FAKE_IP4_ADDRS="$A_GUEST" \
|
# 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"
|
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" \
|
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 "" \
|
check "setup-host: the refusal made NO incus call (refuse precedes mutation)" 1 "" \
|
||||||
test -e "$W80/g1.log"
|
test -e "$W80/g1.log"
|
||||||
check "setup-host: the refusal made NO sudo call either" 1 "" \
|
check "setup-host: the refusal made NO sudo call either" 1 "" \
|
||||||
test -e "$W80/s1.log"
|
test -e "$W80/s1.log"
|
||||||
# Refusal 2: a foreign interface owns an address inside the target.
|
# Refusal 2: a pin on a subnet a foreign interface owns an address inside.
|
||||||
check "setup-host: a foreign interface in the subnet REFUSES" 1 "virbr7" \
|
check "setup-host: a pinned foreign-claimed subnet REFUSES" 1 "virbr7" \
|
||||||
runsetup FAKE_IP4_DEFAULT="$D_LAN" FAKE_IP4_ADDRS="$A_FOREIGN"
|
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.
|
# 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" \
|
check "setup-host: a garbage BOX_SUBNET is refused by name" 1 "not a sane subnet" \
|
||||||
runsetup BOX_SUBNET=banana
|
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"
|
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 "" \
|
check "setup-host: ...and so does the ACL's gateway carve-out" 0 "" \
|
||||||
grep -qF 'destination: 10.89.0.1/32' "$W80/g2.log"
|
grep -qF 'destination: 10.89.0.1/32' "$W80/g2.log"
|
||||||
# ...which also proves the guard scans the TARGET subnet: the same tables that
|
# The nested case with ZERO flags — #80's tables, no pin, no bridge: the
|
||||||
# refused the default (gw 10.88.0.1) pass once BOX_SUBNET moves off it — the
|
# auto-pick must land the whole build on 10.89, announced, and every derived
|
||||||
# issue's workaround host, sanctioned.
|
# 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"
|
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.
|
# usermod, every apt call. Line order, fail-closed on either grep missing.
|
||||||
# shellcheck disable=SC2016 # the $-strings are literals in the target file
|
# shellcheck disable=SC2016 # the $-strings are literals in the target file
|
||||||
check "setup-host: the subnet guard precedes the first mutation" 0 "" bash -c '
|
check "setup-host: the subnet decision precedes the first mutation" 0 "" bash -c '
|
||||||
guard="$(grep -n "subnet_claimant \"\$BOX_SUBNET\"" "'"$ROOT"'/host/setup-host.sh" | head -1 | cut -d: -f1)"
|
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)"
|
mut="$(grep -n "^if ! command -v incus" "'"$ROOT"'/host/setup-host.sh" | head -1 | cut -d: -f1)"
|
||||||
[ -n "$guard" ] && [ -n "$mut" ] && [ "$guard" -lt "$mut" ]'
|
[ -n "$guard" ] && [ -n "$mut" ] && [ "$guard" -lt "$mut" ]'
|
||||||
# box-firewall follows the bridge, wherever BOX_SUBNET put it.
|
# box-firewall follows the bridge, wherever BOX_SUBNET put it.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue