Add --delete-network-full to fully tear down a network's live state

--delete-network only ever removed the config entry, leaving the
bridge/iptables/persistent-namespace state behind. Since
ensure_network_provisioned() treats "bridge exists" as "already fully
provisioned" and skips re-adding anything, a network recreated with
the same name after a plain --delete-network silently never got a
fresh MASQUERADE rule if the old one had been removed separately by
hand -- the bridge itself was still there the whole time.

teardown_network_state() (network_bridge.{h,cpp}) is the reverse of
ensure_network_provisioned(): for extern, removes the MASQUERADE
rule(s) then deletes the bridge; for intern, removes the whole
persistent namespace in one step (destroys the bridge inside it too,
no separate ip link del needed). Deliberately leaves the IPv4/IPv6
forwarding sysctls alone -- those are global host state shared across
every extern network, not per-network. Each step is best-effort
(teardown_step(), logging a warning not an error on failure) since a
step "failing" because that piece was already gone by hand is the
expected case this exists to handle, not a reason to abort --
delete_network_command() doesn't gate the config removal on any of
this succeeding, unlike delete_volume_command()'s own -full variant.

--delete-network-full wired into cli_args.{h,cpp} the same way
--delete-volume-full is.

Verified as root via the doas rule: an extern network's bridge and
MASQUERADE rule were both confirmed gone after --delete-network-full,
and recreating a network with the same name went through
provision_bridge() fresh instead of short-circuiting on a stale
bridge_exists() check -- fixing exactly the gap reported (a manually
removed MASQUERADE rule never came back on delete+recreate). An intern
network's persistent namespace was likewise confirmed fully removed
and recreatable without conflict.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-30 17:59:52 +00:00
parent 69a18924b4
commit 534136339f
7 changed files with 137 additions and 10 deletions
+43 -4
View File
@@ -182,10 +182,20 @@ Source layout (all under `src/`):
it's already a pure deterministic function of the name — then the IPv6
subnet — or `"(no ipv6)"` — appended unaligned as the trailing column,
nothing follows it).
`delete_network_command()` currently only removes the config entry, the
same as `delete_volume_command()`'s default (non-`-full`) behavior — it
does not tear down the network's live bridge/namespace/iptables state (no
`--delete-network-full` analog exists yet).
`delete_network_command()` takes a `delete_full` bool, same shape as
`delete_volume_command()`'s own: `--delete-network` (`false`) only removes
the config entry, leaving the network's live host-side state untouched;
`--delete-network-full` (`true`) additionally calls
`teardown_network_state()` (`network_bridge.h`, see below) first. Unlike
`delete_volume_command()`'s `-full` variant (which bails out *before*
touching the config if its single `remove_all()` call fails),
`delete_network_command()` doesn't gate the config removal on
`teardown_network_state()`'s success at all — that function is
deliberately best-effort/non-fatal per-step (see its own doc comment), so
a step "failing" because that piece was already gone by hand (exactly
`ensure_network_provisioned()`'s own existence-check caveat, above) is
expected, not a reason to leave a network the user explicitly asked to
delete sitting in the config.
`write_config_command()` implements `-w/--write-config`: unlike
`create_volume_command()`/`delete_volume_command()`'s use of
`write_config_file()` (which only ever persists `AppConfig` fields that are
@@ -662,6 +672,35 @@ Source layout (all under `src/`):
all came up correctly; a real `intern` network's bridge came up inside its
own dedicated namespace with neither forwarding nor a NAT rule, confirming
the structural (not merely policy) isolation the design calls for.
`teardown_network_state()` is the reverse: `--delete-network-full`
(`commands.cpp`'s `delete_network_command()`) calls it to tear down
exactly what `ensure_network_provisioned()` stood up. For `extern`:
removes the `ip6tables` MASQUERADE rule (if `ipv6`) then the `iptables`
one, then deletes the bridge itself — via a new `.cpp`-local
`teardown_step()`, the same shape as `run_admin_command()` but logging a
*warning*, not an error, on failure, since a step failing because that
piece was already gone by hand is the expected, common case this exists
to handle (exactly `ensure_network_provisioned()`'s own existence-check
caveat above — this is precisely how a manually-removed MASQUERADE rule
can go from "won't come back on recreate" to "cleanly torn down and
recreated" once `--delete-network-full` exists at all). For `intern`:
removes the whole persistent namespace (`persistent_netns.h`) in one
step, which destroys everything inside it — the bridge included — with
no separate `ip link del` needed. Deliberately never touches the
IPv4/IPv6 forwarding sysctls `provision_bridge()` enables for `extern`
those are global host state shared across every `extern` network, not
per-network, so turning them off here could break others still relying
on them. **Verified end-to-end on this dev machine (root, via the scoped
`doas` rule)**: an `extern` network's bridge and MASQUERADE rule were
both confirmed gone after `--delete-network-full` (`ip link show`
reporting "Device does not exist"), and recreating a network with the
*same name* afterward correctly went through `provision_bridge()` again
from scratch (confirmed via the debug log) instead of short-circuiting
on a stale `bridge_exists()` check — fixing exactly the gap a user
reported (a manually-removed MASQUERADE rule never came back on
`--delete-network` + recreate, since the old bridge was silently still
there); an `intern` network's persistent namespace was similarly
confirmed fully removed and recreatable without conflict.
`probe_veth_support()` (added for the tap+relay fallback, see
`docs/networking-design.md`'s addendum and `network_join.{h,cpp}` below):
the real target device supports `tun`/`tap` but not `veth`