Add bridge provisioning for networks (root-only)
Commit 3/6 of the network isolation feature (docs/networking-design.md).
network_bridge.{h,cpp}: ensure_network_provisioned() stands up a
network's real bridge -- idempotent (checks `ip link show` first), so
this doubles as the reboot-reconciliation mechanism, no separate code
path. extern's bridge lives in the host's own root namespace with
net.ipv4.ip_forward + an iptables MASQUERADE rule for the subnet (+
IPv6 equivalents if enabled); intern's bridge lives inside its own
dedicated persistent namespace (persistent_netns.h) with no forwarding
or NAT at all -- a structural isolation boundary, not just a missing
rule. Bridge names are a deterministic FNV-1a hash of the network name
(not std::hash, whose value isn't guaranteed stable across a rebuild),
kept under Linux's 15-char interface name limit.
network_subnet.{h,cpp} gains ipv4_gateway_address()/
ipv6_gateway_address() (mask a CIDR to its network address, +1 for the
bridge's own ".1"). create_network_command() now calls
ensure_network_provisioned() before persisting the config entry -- a
network that fails to provision isn't saved.
Verified end-to-end as root (via a scoped doas rule): a real extern
network's bridge/gateway IPs/forwarding/NAT rule, and a real intern
network's isolated bridge with neither, both came up correctly; test
networks removed via --delete-network afterward.
This commit is contained in:
@@ -135,19 +135,28 @@ Source layout (all under `src/`):
|
||||
`create_network_command()`/`list_networks_command()`/`delete_network_command()`
|
||||
are the direct network equivalents (`Mode::network`/`Mode::list_networks`/
|
||||
`Mode::delete_network`) — see `docs/networking-design.md` for the full feature
|
||||
design and `config_file.{h,cpp}` below for `NetworkEntry`. This commit is
|
||||
config-only: no bridge, namespace, or iptables state is created yet, only the
|
||||
config entry (later commits in the design doc's sequence wire up the actual
|
||||
host-side networking). `create_network_command()` rejects a duplicate name
|
||||
first, then resolves `subnet`/`subnet6`: an explicit `--subnet`/`--subnet6`
|
||||
is validated (`is_valid_ipv4_cidr()`/`is_valid_ipv6_cidr()`) and checked for
|
||||
overlap against every existing network's subnet (`ipv4_cidrs_overlap()`/
|
||||
design and `config_file.{h,cpp}` below for `NetworkEntry`. Joining a network
|
||||
from `-r/--run` isn't wired up yet (a later commit in the design doc's
|
||||
sequence). `create_network_command()` rejects a duplicate name first, then
|
||||
resolves `subnet`/`subnet6`: an explicit `--subnet`/`--subnet6` is validated
|
||||
(`is_valid_ipv4_cidr()`/`is_valid_ipv6_cidr()`) and checked for overlap
|
||||
against every existing network's subnet (`ipv4_cidrs_overlap()`/
|
||||
`ipv6_cidrs_overlap()`, `network_subnet.{h,cpp}`, see below); otherwise the
|
||||
next free block is auto-allocated (`allocate_ipv4_subnet()`/
|
||||
`allocate_ipv6_subnet()`). `list_networks_command()` reuses the same
|
||||
`allocate_ipv6_subnet()`). Once a `subnet`/`subnet6` is resolved,
|
||||
`create_network_command()` calls `ensure_network_provisioned()`
|
||||
(`network_bridge.h`, see below) to actually stand up the network's
|
||||
host-side state (bridge, sysctls, iptables rules for `extern`; a dedicated
|
||||
persistent namespace + bridge for `intern`) — only once that succeeds is
|
||||
the entry appended to `config.networks` and persisted; a network that
|
||||
fails to provision isn't saved. `list_networks_command()` reuses the same
|
||||
independently-per-column tab-alignment scheme as `list_processes_command()`
|
||||
(name/kind/subnet each aligned, 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).
|
||||
`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
|
||||
@@ -464,9 +473,9 @@ Source layout (all under `src/`):
|
||||
left untouched and not reported.
|
||||
- `persistent_netns.{h,cpp}` — generic, narrow infrastructure for keeping a
|
||||
network namespace alive with no process in it, the way `ip netns add` does;
|
||||
no `intern`/`extern` policy or bridge logic here (that's a later commit,
|
||||
`network_bridge.{h,cpp}`, per `docs/networking-design.md`'s commit
|
||||
sequence), and not yet wired into `-n/--network` at all. `persistent_netns_path()`
|
||||
no `intern`/`extern` policy or bridge logic here (that's `network_bridge.{h,cpp}`,
|
||||
see below, which is what actually calls `create_persistent_netns()` for an
|
||||
`intern` network). `persistent_netns_path()`
|
||||
resolves `xdg_state_dir() / "netns" / sanitize_for_filename(name)`
|
||||
(`pid_file.h`, see above). `persistent_netns_exists()` checks whether that
|
||||
path is actually a live bind-mounted namespace, not just a stale/never-
|
||||
@@ -485,6 +494,54 @@ Source layout (all under `src/`):
|
||||
host-state primitives (session locks, cgroups): logs and returns `false` on
|
||||
any failure (already exists, fork/unshare/mount failure) rather than
|
||||
throwing. `remove_persistent_netns()` unmounts then removes the file.
|
||||
- `network_bridge.{h,cpp}` — stands up (or confirms already-standing) a
|
||||
network's actual host-side state: `ensure_network_provisioned()` is
|
||||
idempotent by design (checks `ip link show <bridge>` first and does nothing
|
||||
further if it's already there) — this is deliberately also the reboot-
|
||||
reconciliation mechanism, not a separate code path: nothing about a
|
||||
network's live state (bridge, veths, iptables rules; the persistent
|
||||
namespace itself for `intern`) survives a reboot except its `config.yaml`
|
||||
entry, so calling this again after one just recreates whatever's missing.
|
||||
`bridge_name()` derives a stable, `.cpp`-local interface name from the
|
||||
network's own name via a hand-rolled 32-bit FNV-1a (`"slk" + 8 hex chars`,
|
||||
11 characters, comfortably under Linux's `IFNAMSIZ - 1` = 15-character
|
||||
limit regardless of how long the network name is) — deliberately not
|
||||
`std::hash<std::string>`, whose exact value is implementation-defined and
|
||||
not guaranteed stable across a rebuild with a different standard library,
|
||||
which would silently orphan an already-provisioned bridge a rebuilt binary
|
||||
can no longer find by the name it now computes. For `extern`, every command
|
||||
runs directly (the bridge lives in the host's own root namespace, and this
|
||||
whole feature is root-only for now — `docs/networking-design.md` — so
|
||||
`slocker-lite`'s own current namespace already is the right one). For
|
||||
`intern`, every command is wrapped through `nsenter --net=<persistent path>`
|
||||
(`wrap_for_network()`, `.cpp`-local) into the network's own dedicated
|
||||
namespace (`persistent_netns.h`, created here first if it doesn't exist
|
||||
yet) — the same pattern `wrap_for_root_namespace()` (`bwrap.cpp`) already
|
||||
uses for the rootless `containers-storage` mount's namespace, just
|
||||
targeting a persistent bind-mounted path instead of a live pid's `/proc`
|
||||
entry. `provision_bridge()` (`.cpp`-local): creates the bridge, assigns it
|
||||
the gateway address from `network_subnet.h`'s `ipv4_gateway_address()`
|
||||
(and `ipv6_gateway_address()` if `network.ipv6`), brings it up, then —
|
||||
`extern` only — `sysctl -w net.ipv4.ip_forward=1` (+ the IPv6 equivalent if
|
||||
`ipv6`, both idempotent global host sysctls, not per-bridge, so no separate
|
||||
"already enabled" tracking is needed) and one `iptables`/`ip6tables`
|
||||
`POSTROUTING`/`MASQUERADE` rule for the subnet (`! -o <bridge>`, the same
|
||||
`docker0` shape, so bridge-local inter-container traffic isn't
|
||||
unnecessarily NAT'd). `check_network_dependencies()` gates on the tool set
|
||||
each kind actually needs (`ip` always; `iptables`/`sysctl` [+`ip6tables` if
|
||||
`ipv6`] for `extern`; `nsenter` for `intern`) — same shape/spirit as
|
||||
`commands.cpp`'s own `check_required_dependencies()`, kept separate since
|
||||
the tool set here depends on the network's own kind/`ipv6` setting.
|
||||
`create_network_command()` (`commands.cpp`, see above) calls
|
||||
`ensure_network_provisioned()` after resolving subnets but *before*
|
||||
persisting the config entry — a network that fails to provision isn't
|
||||
saved, so a later join doesn't find a config entry for something that
|
||||
doesn't actually exist on the host. **Verified end-to-end on this dev
|
||||
machine (root, via a scoped `doas` rule)**: a real `extern` network's
|
||||
bridge, gateway IPv4/IPv6 addresses, `ip_forward`, and `MASQUERADE` rules
|
||||
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.
|
||||
- `session_cgroup.{h,cpp}` — gives `--kill` (`kill_session.{h,cpp}`, see
|
||||
below) a reliable way to find every process a session ever started, however
|
||||
deeply forked/daemonized/reparented, by putting it in a dedicated cgroup v2
|
||||
@@ -820,8 +877,8 @@ Source layout (all under `src/`):
|
||||
`networks`, `ipv6` re-serialized as canonical `"true"`/`"false"` like the
|
||||
`unshare-*` keys.
|
||||
- `network_subnet.{h,cpp}` — pure CIDR arithmetic backing `-n/--network`'s
|
||||
subnet allocation, no kernel/`ip`/`iptables` calls (those come in a later
|
||||
commit per `docs/networking-design.md`'s sequence). `is_valid_ipv4_cidr()`/
|
||||
subnet allocation and `network_bridge.{h,cpp}`'s (see below) gateway-address
|
||||
computation; no kernel/`ip`/`iptables` calls of its own. `is_valid_ipv4_cidr()`/
|
||||
`is_valid_ipv6_cidr()` and `ipv4_cidrs_overlap()`/`ipv6_cidrs_overlap()` all
|
||||
build on one `.cpp`-local `parse_cidr()` (via `inet_pton()`, not hand-rolled
|
||||
parsing) producing a plain byte-vector address (4 bytes for IPv4, 16 for
|
||||
@@ -838,7 +895,14 @@ Source layout (all under `src/`):
|
||||
since IPv6 hextets are hexadecimal, `n >= 10` renders as a valid but
|
||||
numerically-different-from-`n` address (e.g. `n=15` becomes `...:15::/64`,
|
||||
which is hex `0x15` = 21) — purely cosmetic, allocation correctness doesn't
|
||||
depend on the two matching numerically.
|
||||
depend on the two matching numerically. `ipv4_gateway_address()`/
|
||||
`ipv6_gateway_address()` (`network_bridge.cpp`'s `provision_bridge()`)
|
||||
return a network's bridge gateway address within a CIDR: masked down to its
|
||||
network address first (a shared `.cpp`-local `mask_to_network()`, in case
|
||||
the CIDR given — e.g. a manual `--subnet`/`--subnet6` — wasn't already a
|
||||
canonical network address), then `| 1` in the last bit for the `.1`
|
||||
convention this project's bridges use, reusing the same `parse_cidr()` as
|
||||
the validation/overlap functions above.
|
||||
- `volume_mount.{h,cpp}` — `is_valid_volume_name()` (no `/`, checked by both
|
||||
`create_volume_command()` and to tell a `-v` spec's name/path apart) and
|
||||
`resolve_volume_mount()`, called once per `-v` occurrence from `run_container()`
|
||||
|
||||
Reference in New Issue
Block a user