Add persistent network namespace primitives

Commit 2/6 of the network isolation feature (docs/networking-design.md).
persistent_netns.{h,cpp}: create/verify/remove a network namespace kept
alive with no process in it, the way `ip netns add` does (fork a child,
unshare(CLONE_NEWNET), bind-mount its /proc/self/ns/net onto a
persistent path, exit -- the bind mount keeps it alive). Root-only
(CAP_SYS_ADMIN for the bind mount), best-effort like this project's
other host-state primitives. Not wired into -n/--network yet.

xdg_state_dir() (pid_file.cpp) moved out of its anonymous namespace so
this file can reuse the same $XDG_STATE_HOME resolution rather than a
second, drifting copy.

-t/--test now exercises the create/verify/remove cycle (skipped with a
message, not a failure, when not root) -- confirmed working via doas.
This commit is contained in:
2026-08-30 12:43:12 +00:00
parent ec09b96b56
commit db3a9d82c7
8 changed files with 276 additions and 10 deletions
+40 -5
View File
@@ -196,10 +196,17 @@ Source layout (all under `src/`):
- `self_test.{h,cpp}``run_self_tests()` implements `-t/--test`, this
project's own built-in self-test mode (distinct from the Meson-driven
fixture smoke test under `tests/`, described in "Build & test commands"
below). Currently an empty placeholder (previously reported
`detect_bwrap_unshare_args()`'s output — `bwrap.{h,cpp}` — unplugged since
that's kernel-capability diagnostics, not a test); deliberately its own
small file since real tests are expected here soon.
below; previously reported `detect_bwrap_unshare_args()`'s output —
`bwrap.{h,cpp}` — unplugged since that's kernel-capability diagnostics, not
a test). Currently exercises `persistent_netns.{h,cpp}`'s (see below)
create/verify/remove cycle: skipped with a message (not a failure) when not
root, since `create_persistent_netns()` requires it for the bind mount.
Confirms the namespace is missing before creation, exists right after
(checked from this process, *after* the forked child that actually did the
`unshare()`/bind-mount has already exited — the actual claim being tested:
the namespace outlives its creating process), then gone again after
removal. Deliberately its own small file since more real tests are expected
here as more of the networking feature lands.
- `env_spec.{h,cpp}``resolve_env_specs()` turns an ordered list of
`EnvSpec {is_file, value}` (see `cli_args.{h,cpp}` above) into a flat, ordered list of
`(key, value)` pairs. A literal (`--env`) is split at its *first* `=` (the
@@ -406,7 +413,12 @@ Source layout (all under `src/`):
back to `"container"` if that leaves nothing) is exported here (not just
`.cpp`-local) specifically so `session_cgroup.{h,cpp}` (see below) can reuse
the exact same `<name>-<pid>` naming rule for its own per-session cgroup
directory without drifting from this file's own. `session_pid_file_path()` resolves
directory without drifting from this file's own. `xdg_state_dir()`
(`$XDG_STATE_HOME/slocker-lite`, or the `$HOME/.local/state/...` fallback)
is likewise exported (moved out of this file's own anonymous namespace) so
`persistent_netns.{h,cpp}` (see below) can resolve its own subdirectory
under the same state root without a second, drifting copy of this
resolution logic. `session_pid_file_path()` resolves
`$XDG_STATE_HOME/slocker-lite/run/<container_name>-<pid>` (falling back to
`$HOME/.local/state/...` when `XDG_STATE_HOME` is unset/empty — same
resolution pattern as `config_file_path()` below, for state instead of
@@ -450,6 +462,29 @@ Source layout (all under `src/`):
a check and a later removal. Only files it actually removes are reported back
(as `SessionInfo`s with `running=false`); still-locked (running) files are
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()`
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-
mounted file: `stat()`s the path and its parent directory and compares
`st_dev` — a genuine bind mount always has a different device number than
its parent, the same "is this a mountpoint" technique used elsewhere. Never
needs root itself (just `stat()`). `create_persistent_netns()` forks a
child (never touches the caller's own network namespace — `unshare(2)`
affects only the calling process) that `unshare(CLONE_NEWNET)`s its own
fresh namespace, bind-mounts its `/proc/self/ns/net` onto the target path,
then exits immediately — the bind mount itself is what keeps the namespace
alive from then on, independent of the now-exited child, exactly `ip netns
add`'s own technique. Requires `CAP_SYS_ADMIN` (root) for the bind mount,
matching this feature's current root-only scope (see
`docs/networking-design.md`) — best-effort like this project's other
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.
- `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