Break the network isolation feature into a 6-commit sequence

Documentation only, no code: config schema + subnet/IPv6 allocation +
-n/--network CRUD, persistent netns primitives, bridge provisioning
(also doubles as reboot reconciliation), veth join at -r/--run time,
-p port forwarding, then a crash-orphan cleanup sweep. Each step is
independently buildable and manually verifiable, since the full
feature is too large for one commit.
This commit is contained in:
2026-08-30 12:10:41 +00:00
parent f0aa913014
commit e663e30221
+124 -3
View File
@@ -1,7 +1,8 @@
# Network isolation design
Status: design confirmed, not yet broken into a file-by-file implementation
plan; no code written yet. Captured 2026-08-30 on the `networking` branch.
Status: design confirmed, broken into a commit sequence below (see
"Implementation plan: commit sequence"); no code written yet. Captured
2026-08-30 on the `networking` branch.
## Goal
@@ -158,7 +159,127 @@ cgroups (`session_cgroup.{h,cpp}`), plus a `--clean-processes`-style sweep for
anything orphaned by a crash, so a dead `slocker-lite` doesn't leave stray
bridges/veths/iptables rules behind forever.
## Explicitly out of scope for now
## Implementation plan: commit sequence
The whole feature is too large for one commit (unlike, say, `--kill`, which
landed as a single commit despite touching several new files). Split into six
commits, each a coherent, independently buildable and manually verifiable
unit, in dependency order. Docs (`CLAUDE.md`/`README.md`) get updated *within*
each commit, matching this branch's existing practice — not saved for a final
pass.
1. **Config schema + subnet/IPv6 allocation + `-n/--network create/list/delete`
(config-only, no host-side effects yet)**
- `config_file.{h,cpp}`: new `NetworkEntry {name, kind, subnet, ipv6,
subnet6}` (`kind` = `extern`/`intern`), a `networks` section — directly
parallel to `VolumeEntry`/`volumes`.
- New `network_subnet.{h,cpp}`: IPv4 `/24` auto-allocation starting at
`10.168.0.0/24` incrementing per existing network, `--subnet` override;
paired IPv6 ULA `/64` auto-allocation (`fd00:168:0:N::/64`) when enabled
(default), `--subnet6` override, `--no-ipv6` to skip. Pure allocation
logic against the already-loaded config's existing networks — no kernel/
`ip`/`iptables` calls in this commit.
- `cli_args.{h,cpp}`: `Mode::network`/`list_networks`/`delete_network`,
`-n`/`--network` (`required_argument`, single token = name; separate
`--extern`/`--intern`/`--subnet`/`--no-ipv6`/`--subnet6` flags for the
create case), `--list-networks`, `--delete-network <name>` — directly
mirroring `-v/--volume`'s existing three-mode shape in the same file.
- `commands.cpp`: `create_network_command()`/`list_networks_command()`/
`delete_network_command()` — mirroring `create_volume_command()`/
`list_volumes_command()`/`delete_volume_command()`, including the shared
tab-aligned listing helper.
- Verify: `-n mynet --extern`, `-n other --intern --no-ipv6`,
`--list-networks` shows both with correct kind/subnet, `--subnet`/
`--subnet6` overrides land correctly in `config.yaml`, `--delete-network`
removes an entry. No bridges/namespaces/iptables rules exist yet — purely
config bookkeeping, same as a freshly-created volume before it's ever
mounted.
2. **Persistent network namespace primitives (generic infra for `intern`
networks)**
- New `persistent_netns.{h,cpp}`: create/find/remove a persistent network
namespace kept alive with no process in it, the way `ip netns add` does
(bind-mount a fresh namespace's `ns/net` onto a path that outlives the
creating process) — narrow, reusable infra, no `intern`/`extern`
branching or bridge logic here (parallels how `session_cgroup.{h,cpp}`
stayed narrowly scoped to cgroup mechanics only).
- Not wired into `-n/--network` yet in this commit.
- Verify: a small manual exercise (or a `-t/--test` addition) creating a
persistent namespace, confirming it survives after the creating process
exits, then removing it.
3. **Bridge provisioning for a network (idempotent — this is also the reboot-
reconciliation mechanism, not a separate later step)**
- New `network_bridge.{h,cpp}`: given a `NetworkEntry`, ensure its bridge
exists and is configured — creating it if missing (idempotent, so this
doubles as "reconcile after reboot" with no separate code path):
- `extern`: bridge in the *host's own* root namespace; assign it the
gateway IP from the network's subnet; `net.ipv4.ip_forward=1` (+ IPv6
forwarding sysctl if `ipv6`); one iptables `MASQUERADE` rule for the
subnet (`ip6tables` too, if `ipv6`).
- `intern`: bridge inside its own dedicated `persistent_netns.h`
namespace (commit 2); gateway IP assigned; no forwarding, no NAT rule
— no route out at all.
- Wire this "ensure provisioned" call into `create_network_command()` (so
creating a network actually stands up its bridge immediately) — later
commits also call it lazily before a join, covering the reboot case.
- `check_required_dependencies()`-style availability check added for `ip`/
`iptables` (and `ip6tables` when needed), alongside the existing
`containers-storage`/`bwrap` check.
- Verify: `-n mynet --extern` produces a real bridge with the expected
gateway IP, `ip_forward` enabled, and a matching `MASQUERADE` rule
(`ip link show`, `iptables -t nat -L`); an `intern` network's bridge
exists in its own namespace with no such rule. Delete/recreate a
network's config entry, delete its bridge by hand (`ip link del`), then
trigger provisioning again (e.g. re-running `--network create` or the
first join in commit 4) and confirm it comes back.
4. **Joining networks at `-r/--run` time: veth creation, IP assignment, route**
- Repeatable `-n <name>` with `-r/--run` (`cli_args.cpp`, same
repeatable-with-`-r` shape `-v/--volume` already has).
- `commands.cpp`'s `run_container()`: once `bwrap`'s pid (and via
`resolve_namespace_pid()`-style lookup, `sandbox_process.h`, its actual
net namespace) is known — same timing hook `on_bwrap_pid_known`/
`on_start` already provides for session locks/cgroups (`bwrap.cpp`) —
for each joined network: ensure it's provisioned (commit 3, covers
reboot recreation), create a veth pair, move the container-side end into
the container's net namespace, attach the bridge-side end, assign the
container's veth an IP from the subnet, and (for an `extern` join) set
it as the default route.
- This is the core connectivity commit — no veth pairs exist before it,
regardless of how many networks are configured/joined.
- Verify: two containers joined to the same `intern` network can ping each
other and cannot reach the host or outside; a container joined to an
`extern` network can reach the outside (and the host cannot reach *it*
without commit 5's port forwarding); a container joined to both loses
neither path (two interfaces, both functional).
5. **`-p` port forwarding**
- `cli_args.cpp`: `-p [<network>:]<host-port>:<container-port>`,
`<network>` optional (resolves to the container's sole `extern` network;
error if ambiguous).
- New `port_forward.{h,cpp}`: add/remove the iptables `DNAT`
(`PREROUTING`) + `FORWARD ACCEPT` rule pair for one mapping, tied to the
container's own session lifecycle the same create-on-start/
remove-on-stop way `pid_file.{h,cpp}`/`session_cgroup.{h,cpp}` already
are.
- Verify: `-p 8080:80` against a container on an extern network answering
on port 80 is reachable via `curl localhost:8080` from the host; the
rule is gone after the container exits.
6. **Crash-orphan cleanup sweep**
- Extend `--clean-processes` (or add a dedicated `--clean-networks`,
whichever reads better once this is reached) to find and remove
bridges/veths/iptables rules left behind by a `slocker-lite` that died
before its own teardown ran — mirroring `clean_stale_sessions()`
(`pid_file.cpp`)'s existing stale-pid-file sweep, but for host-global
network state instead of pid files.
- Verify: kill `-9` a running `-r/--run` session mid-flight (bypassing its
normal cleanup), confirm the orphaned veth/iptables rule is detected and
removed by the sweep, and that a *still-running* session's state is left
untouched.
- **Rootless networking.** An earlier draft of this design considered a
hybrid strategy (real bridge+veth when root, a simpler shared-network-