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:
@@ -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
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ slocker_lite = executable('slocker-lite',
|
||||
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp',
|
||||
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp',
|
||||
'src/sandbox_process.cpp', 'src/session_cgroup.cpp', 'src/kill_session.cpp',
|
||||
'src/network_subnet.cpp'],
|
||||
'src/network_subnet.cpp', 'src/persistent_netns.cpp'],
|
||||
include_directories : include_directories('.'),
|
||||
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
|
||||
install : true)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#include "persistent_netns.h"
|
||||
|
||||
#include <sched.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <system_error>
|
||||
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "pid_file.h"
|
||||
|
||||
std::filesystem::path persistent_netns_path(std::string_view name) {
|
||||
return xdg_state_dir() / "netns" / sanitize_for_filename(name);
|
||||
}
|
||||
|
||||
bool persistent_netns_exists(std::string_view name) {
|
||||
auto path = persistent_netns_path(name);
|
||||
|
||||
struct stat file_stat {};
|
||||
struct stat parent_stat {};
|
||||
if (stat(path.c_str(), &file_stat) != 0 || stat(path.parent_path().c_str(), &parent_stat) != 0) {
|
||||
return false;
|
||||
}
|
||||
return file_stat.st_dev != parent_stat.st_dev;
|
||||
}
|
||||
|
||||
bool create_persistent_netns(std::string_view name) {
|
||||
if (persistent_netns_exists(name)) {
|
||||
spdlog::error("persistent network namespace '{}' already exists", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto path = persistent_netns_path(name);
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
spdlog::warn("failed to create directory {}: {}", path.parent_path().string(), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
// A plain regular file for the bind mount to land on -- created (or, if a
|
||||
// stale never-mounted leftover from a previous failed attempt, simply
|
||||
// reused) before forking, so the child has a stable target to bind onto.
|
||||
int fd = open(path.c_str(), O_CREAT | O_RDONLY | O_CLOEXEC, 0644);
|
||||
if (fd < 0) {
|
||||
spdlog::warn("failed to create {}: {}", path.string(), strerror(errno));
|
||||
return false;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
spdlog::warn("failed to fork while creating persistent network namespace '{}': {}", name, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
// unshare(2) affects only the calling process -- doing this in a
|
||||
// forked child, never the caller itself, means slocker-lite's own
|
||||
// network namespace is never touched by creating one of these.
|
||||
if (unshare(CLONE_NEWNET) != 0) {
|
||||
_exit(1);
|
||||
}
|
||||
// The bind mount is what actually keeps the namespace alive after
|
||||
// this child exits -- exactly the technique `ip netns add` itself
|
||||
// uses (bind-mounting /proc/self/ns/net onto a persistent path).
|
||||
if (mount("/proc/self/ns/net", path.c_str(), nullptr, MS_BIND, nullptr) != 0) {
|
||||
_exit(1);
|
||||
}
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
spdlog::warn(
|
||||
"failed to create persistent network namespace '{}' (unshare/bind-mount failed -- requires root)",
|
||||
name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool remove_persistent_netns(std::string_view name) {
|
||||
auto path = persistent_netns_path(name);
|
||||
if (umount2(path.c_str(), 0) != 0) {
|
||||
spdlog::warn("failed to unmount persistent network namespace '{}' at {}: {}", name, path.string(),
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
if (ec) {
|
||||
spdlog::warn("failed to remove {}: {}", path.string(), ec.message());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2026 Viorel Munteanu
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
// Generic infrastructure for keeping a network namespace alive with no
|
||||
// process in it, the way `ip netns add` does -- narrow, reusable primitives
|
||||
// only; no `intern`/`extern` policy or bridge logic here (that's
|
||||
// network_bridge.{h,cpp}, a later commit -- see docs/networking-design.md's
|
||||
// commit sequence). Not yet wired into -n/--network.
|
||||
|
||||
// $XDG_STATE_HOME/slocker-lite/netns/<sanitized-name> (see pid_file.h's
|
||||
// xdg_state_dir()/sanitize_for_filename(), reused here rather than a second,
|
||||
// drifting copy of the same resolution/sanitization rules).
|
||||
std::filesystem::path persistent_netns_path(std::string_view name);
|
||||
|
||||
// True if persistent_netns_path(name) exists and is actually a live
|
||||
// bind-mounted namespace (its st_dev differs from its parent directory's --
|
||||
// the same "is this a mountpoint" check used elsewhere in this project's
|
||||
// spirit of trusting stat() over guessing from a path alone), not just a
|
||||
// stale, never-mounted, or already-unmounted leftover file.
|
||||
bool persistent_netns_exists(std::string_view name);
|
||||
|
||||
// Creates a brand-new persistent network namespace at
|
||||
// persistent_netns_path(name): forks a child that unshare(CLONE_NEWNET)s its
|
||||
// own network namespace, bind-mounts its /proc/self/ns/net onto the target
|
||||
// path, then exits immediately -- the bind mount itself keeps the namespace
|
||||
// alive from then on, independent of the (now-exited) child, exactly like
|
||||
// `ip netns add`. Requires CAP_SYS_ADMIN (root) for the bind mount, matching
|
||||
// this feature's current root-only scope. Best-effort like this project's
|
||||
// other host-state primitives (session locks, cgroups): logs and returns
|
||||
// false on any failure (already exists, can't fork/unshare/mount) rather
|
||||
// than throwing.
|
||||
bool create_persistent_netns(std::string_view name);
|
||||
|
||||
// Removes the persistent network namespace at persistent_netns_path(name)
|
||||
// (unmounts it, then removes the now-plain file). Returns false (logging a
|
||||
// warning) if it doesn't exist or can't be removed.
|
||||
bool remove_persistent_netns(std::string_view name);
|
||||
+2
-2
@@ -42,8 +42,6 @@ std::string sanitize_for_filename(std::string_view name) {
|
||||
return result.empty() ? "container" : result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path xdg_state_dir() {
|
||||
const char* xdg_state_home = std::getenv("XDG_STATE_HOME");
|
||||
std::filesystem::path state_home;
|
||||
@@ -56,6 +54,8 @@ std::filesystem::path xdg_state_dir() {
|
||||
return state_home / "slocker-lite";
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path session_run_dir() {
|
||||
return xdg_state_dir() / "run";
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
// cgroup directory, so the two don't drift apart.
|
||||
std::string sanitize_for_filename(std::string_view name);
|
||||
|
||||
// $XDG_STATE_HOME/slocker-lite, or $HOME/.local/state/slocker-lite if
|
||||
// XDG_STATE_HOME is unset/empty. Exported (not just this file's own internal
|
||||
// helper) so persistent_netns.{h,cpp} can resolve its own subdirectory under
|
||||
// the same state root without a second, drifting copy of this logic.
|
||||
std::filesystem::path xdg_state_dir();
|
||||
|
||||
// Tracks one running -r/--run session (a live bwrap process) as a PID file with an
|
||||
// advisory flock() held for as long as this process is running it -- so a *different*
|
||||
// process can tell a stale leftover file apart from a genuinely still-running
|
||||
|
||||
@@ -16,6 +16,52 @@
|
||||
|
||||
#include "self_test.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "persistent_netns.h"
|
||||
|
||||
int run_self_tests() {
|
||||
constexpr std::string_view test_netns_name = "selftest";
|
||||
|
||||
// create_persistent_netns() bind-mounts, which requires root -- report
|
||||
// and skip rather than treating a rootless dev machine as a failure.
|
||||
if (geteuid() != 0) {
|
||||
fmt::print("skipping persistent network namespace test (requires root)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Clean up a leftover from a previous interrupted run, if any, before
|
||||
// starting -- create_persistent_netns() refuses to overwrite an existing
|
||||
// live namespace.
|
||||
if (persistent_netns_exists(test_netns_name)) {
|
||||
remove_persistent_netns(test_netns_name);
|
||||
}
|
||||
|
||||
if (!create_persistent_netns(test_netns_name)) {
|
||||
spdlog::error("self-test: failed to create persistent network namespace");
|
||||
return 1;
|
||||
}
|
||||
// Checked from this (parent) process, after the child that actually did
|
||||
// the unshare()/bind-mount has already exited -- this is exactly what
|
||||
// confirms the namespace outlives its creating process, the whole point
|
||||
// of the bind-mount technique.
|
||||
if (!persistent_netns_exists(test_netns_name)) {
|
||||
spdlog::error("self-test: persistent network namespace missing right after creating it");
|
||||
remove_persistent_netns(test_netns_name);
|
||||
return 1;
|
||||
}
|
||||
if (!remove_persistent_netns(test_netns_name)) {
|
||||
spdlog::error("self-test: failed to remove persistent network namespace");
|
||||
return 1;
|
||||
}
|
||||
if (persistent_netns_exists(test_netns_name)) {
|
||||
spdlog::error("self-test: persistent network namespace still exists after removing it");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fmt::print("persistent network namespace create/verify/remove: OK\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
+4
-2
@@ -17,6 +17,8 @@
|
||||
#pragma once
|
||||
|
||||
// Implements -t/--test, this project's own built-in self-test mode (distinct
|
||||
// from the Meson-driven fixture smoke test under tests/). Currently an empty
|
||||
// placeholder -- real tests are expected here soon.
|
||||
// from the Meson-driven fixture smoke test under tests/). Currently exercises
|
||||
// persistent_netns.{h,cpp}'s create/verify/remove cycle (root-only, skipped
|
||||
// with a message otherwise -- see docs/networking-design.md); more real tests
|
||||
// are expected here as more of that feature lands.
|
||||
int run_self_tests();
|
||||
|
||||
Reference in New Issue
Block a user