Add network_tap_relay: tap-backed veth substitute, standalone

Second step of the tap+relay fallback for veth-less kernels (the real
target device supports tun/tap but not veth). Reuses the existing
bridge as the switching fabric -- provision_bridge()'s NAT/forwarding
setup needs no changes -- and only replaces how a container's
namespace gets connected to it:

- a host-side tap device, created wherever the network's bridge lives
  and enslaved to it, playing veth's host-side role
- a container-side tap device, created directly inside the container's
  namespace and named eth<N> from the start (no rename step needed)
- a relay process holding both fds open, copying raw Ethernet frames
  bidirectionally between them -- reproducing a veth pair's kernel
  wire via one userspace hop

Not wired into join_one_network() yet -- this commit only adds
create_tap_relay()/stop_tap_relay() and exercises them standalone via
a new self-test (throwaway bridge + throwaway namespace).

A real synchronization bug turned up while writing that self-test:
fork() returning to the parent doesn't mean the child has reached its
own unshare(CLONE_NEWNET) yet, so using its pid immediately raced and
created the container-side tap in the wrong (host) namespace. Fixed
by polling namespace_isolated() first, the same guard
network_join.cpp's wait_for_isolated_net_namespace() already uses for
a real session.

Verified twice as root via the doas rule: host-side tap gets created
and attached to the bridge, container-side tap gets created with the
right name inside the target namespace, and -- the biggest open
assumption from the design doc addendum -- both devices disappear on
their own once stop_tap_relay() stops the process, no explicit
`ip link del` needed.

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 16:06:32 +00:00
parent 08895297a9
commit 96efcf37e1
6 changed files with 578 additions and 19 deletions
+93 -2
View File
@@ -284,8 +284,34 @@ Source layout (all under `src/`):
(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.
removal. Also exercises `network_tap_relay.{h,cpp}`'s (see below)
create/attach/teardown cycle, same root-only skip: a throwaway bridge
(`ip link add ... type bridge`, no `network_bridge.h` provisioning needed —
a tap device only cares that *some* bridge exists to attach to) stands in
for a real network's bridge, and a throwaway network namespace (a forked
child that `unshare(CLONE_NEWNET)`s then blocks in `pause()` until
signaled) stands in for a real `-r/--run` session's isolated one. **Real
race caught by testing, not assumed**: `fork()` returning to the parent
doesn't mean the child has actually reached its own `unshare(CLONE_NEWNET)`
call yet — the same race `network_join.cpp`'s own
`wait_for_isolated_net_namespace()` already guards against for a real
session; an earlier version of this test used the child's pid immediately
and the container-side tap device silently ended up created in the *host's*
namespace instead (confirmed: it wasn't visible via `nsenter` into the
child's namespace at all). Fixed the same way — polling (bounded, 1s,
20ms interval) `namespace_isolated()` (`sandbox_process.h`) until the
child's own net namespace actually differs from this process's before
trusting its pid. Confirms the host-side tap gets created and attached to
the bridge (`ip link show` output contains `master <bridge>`), the
container-side tap gets created with the requested name inside the target
namespace (checked via `nsenter --net=/proc/<pid>/ns/net -- ip link show`),
and — the biggest previously-unverified assumption from
`docs/networking-design.md`'s tap+relay addendum — **both devices actually
disappear on their own once `stop_tap_relay()` stops the relay process,
with no explicit `ip link del` needed** (neither was created with
`IFF_PERSIST`) — confirmed directly, twice, on this dev machine (root, via
the scoped `doas` rule). 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
@@ -824,6 +850,71 @@ Source layout (all under `src/`):
its rule-removal attempted — visibly failing only for lack of root in this
particular rootless test — and its record file actually removed,
reported as `removed stale port-forward rules for 'faketest-999999'`).
- `network_tap_relay.{h,cpp}` — a tap-backed substitute for one veth pair,
used when `should_use_veth()` (`network_bridge.h`) is false: either the
running kernel doesn't support veth at all (the real target device's
kernel supports `tun`/`tap` but lacks `CONFIG_VETH`, commonly stripped
from mobile kernels — see `docs/networking-design.md`'s tap+relay
addendum), or the network was created with `--no-veth` (`cli_args.{h,cpp}`)
specifically to exercise this path on a veth-capable machine. **Why tap
can't just replace veth 1:1**: a veth pair is two real kernel netdevices,
switched between (or into a bridge) entirely by the kernel; a tap device
only has *one* kernel-side netdevice — the other "end" is a raw-Ethernet-
frame file descriptor only a userspace process can read/write, so there's
no second kernel endpoint to attach to a bridge (the same reason
`slirp4netns`/QEMU's own tap networking need a userspace process on the fd
side). `create_tap_relay(network, bridge, host_tap_name, container_ns_pid,
container_if_name)` reproduces a veth pair's role with two tap devices and
one relay process that copies bytes between them, **reusing the existing
bridge as the switching fabric** so `network_bridge.cpp`'s
`provision_bridge()`/NAT setup needs no changes at all: a host-side tap
device (created wherever `network`'s bridge lives — `network_bridge.h`'s
`wrap_for_network()` namespace, reached here via a direct `setns()` rather
than that function's `nsenter`-argv-wrapping, since this whole sequence
must keep running, and later hold onto live fds, across each namespace
switch, not just for the duration of one external command) gets enslaved
to `bridge` exactly like veth's host-side end does in
`network_join.cpp`'s `join_one_network()`; a container-side tap device
gets created directly inside the namespace named by `container_ns_pid`,
**named `container_if_name` (e.g. `eth0`) from the start** — no
peer-name-then-rename dance needed, unlike veth. `host_tap_name` is
caller-provided (not derived here) specifically so a future caller
(`join_one_network()`, once this is wired in) can reuse its own existing
fnv1a-based veth-naming scheme rather than this file growing a second,
drifting copy of that six-line hash. `open_tap()` (`.cpp`-local) creates
each device via a direct `open("/dev/net/tun")` + `ioctl(TUNSETIFF,
IFF_TAP | IFF_NO_PI)``IFF_NO_PI` so both ends agree on raw-frame
framing with no extra header, deliberately **not** `IFF_PERSIST`, so each
device is expected to disappear on its own once its one-and-only fd
closes, the same "no explicit teardown" property veth already has (see
`self_test.{h,cpp}` above for where this got verified). The relay child's
entire setup sequence — enter the network's own namespace first if
`intern` (`persistent_netns_path()`, `persistent_netns.h`), create+attach
the host-side tap, `setns()` into the container's namespace (the
already-open host-side fd stays valid across this switch — fds aren't
namespace-scoped, only their *creation* is, the same property
`wrap_for_root_namespace()`, `bwrap.cpp`, already relies on), create the
container-side tap — reports success/failure back to `create_tap_relay()`
over a `pipe2(O_CLOEXEC)` (same handshake shape `daemonize()`,
`daemonize.cpp`, already uses), then falls into an unbounded
`poll()`/`read()`/`write()` loop copying raw frames bidirectionally
between the two fds — this loop *is* the actual "veth wire," just
implemented once in userspace instead of by the kernel. No `SIGTERM`
handler is installed in the relay: default disposition (terminate) already
closes both fds on the way out, which is all `stop_tap_relay()`'s
"no explicit teardown" contract needs. `stop_tap_relay()` sends `SIGTERM`
and reaps the process. Once `create_tap_relay()` returns successfully,
`container_if_name` is a completely ordinary interface from the
container's own point of view — `join_one_network()`'s existing IP
assignment/route/DNAT-target-address code (unchanged, not yet wired to
call this) needs no changes at all. **Not yet wired into
`join_one_network()`/`run_container()`** — see `self_test.{h,cpp}` above
for how this file's own create/attach/teardown cycle was verified
end-to-end in isolation first; full traffic-plane verification (does
`-p`/inter-container connectivity actually work end-to-end through this
path) is deferred to the commit that does that wiring, where two real
`--no-veth`-joined containers pinging/curling each other is a more natural
and simpler proof than hand-rolled raw-socket test traffic.
- `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
View File
@@ -23,7 +23,7 @@ slocker_lite = executable('slocker-lite',
'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/persistent_netns.cpp', 'src/network_bridge.cpp',
'src/network_join.cpp', 'src/port_forward.cpp'],
'src/network_join.cpp', 'src/port_forward.cpp', 'src/network_tap_relay.cpp'],
include_directories : include_directories('.'),
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
install : true)
+251
View File
@@ -0,0 +1,251 @@
// 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 "network_tap_relay.h"
#include <fcntl.h>
#include <linux/if_tun.h>
#include <net/if.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <vector>
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include "network_bridge.h"
#include "persistent_netns.h"
#include "process.h"
namespace {
// Opens /dev/net/tun and creates a tap device named `name` in whatever
// network namespace this process is currently in -- IFF_NO_PI so both ends
// of a relay agree on raw-frame framing with no extra header, IFF_TAP (not
// IFF_TUN) so the host-side end can be enslaved to a bridge like any other
// Ethernet device. Deliberately not IFF_PERSIST: the device should disappear
// on its own once this fd (the only one ever opened on it) closes, the same
// property veth already has -- see stop_tap_relay()'s own doc comment.
// Returns -1 (logging why) on failure.
int open_tap(const std::string& name) {
int fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) {
spdlog::error("failed to open /dev/net/tun: {}", strerror(errno));
return -1;
}
struct ifreq ifr {};
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
std::strncpy(ifr.ifr_name, name.c_str(), IFNAMSIZ - 1);
if (ioctl(fd, TUNSETIFF, &ifr) < 0) {
spdlog::error("failed to create tap device '{}': {}", name, strerror(errno));
close(fd);
return -1;
}
return fd;
}
// setns(2) into the CLONE_NEWNET namespace named by the open file at `path`
// (e.g. /proc/<pid>/ns/net, or a persistent_netns.h path) -- the direct
// syscall equivalent of what every other namespace-crossing operation in
// this project reaches via shelling out to `nsenter`, needed here because
// this whole sequence must keep running (and later hold onto live fds) in
// the *same* process across each namespace switch, not just for the
// duration of one external command's argv.
bool enter_namespace(const std::string& path) {
int fd = open(path.c_str(), O_RDONLY);
if (fd < 0) {
spdlog::error("failed to open namespace {}: {}", path, strerror(errno));
return false;
}
bool ok = setns(fd, CLONE_NEWNET) == 0;
if (!ok) {
spdlog::error("failed to enter namespace {}: {}", path, strerror(errno));
}
close(fd);
return ok;
}
void report_line(int fd, const std::string& line) {
// Best-effort: if this write fails, the parent's own read-until-EOF loop
// still unblocks (with an empty/partial report) once report_fd is closed
// by this process exiting, which read_relay_report() already treats as
// failure.
ssize_t unused = write(fd, line.data(), line.size());
(void)unused;
}
// The relay child's entire lifetime, from namespace/device setup through the
// frame-copy loop -- never returns to its caller (always _exit()s, whether
// setup failed or the loop itself ended). Deliberately _exit(), not exit():
// this is a forked child, and _exit() skips flushing this process's own
// (copied-by-fork, possibly stale) buffered stdio state -- the same
// reasoning bwrap.cpp's kernel_supports_namespace() and this file's own
// probe_veth_support() (network_bridge.cpp) already rely on for their own
// throwaway forked children.
[[noreturn]] void relay_child_main(int report_fd, const NetworkEntry& network, const std::string& bridge,
const std::string& host_tap_name, pid_t container_ns_pid,
const std::string& container_if_name) {
if (network.kind == NetworkKind::intern) {
if (!enter_namespace(persistent_netns_path(network.name).string())) {
report_line(report_fd, "ERROR failed to enter network's persistent namespace\n");
_exit(1);
}
}
int fd_host = open_tap(host_tap_name);
if (fd_host < 0) {
report_line(report_fd, "ERROR failed to create host-side tap device\n");
_exit(1);
}
// Now running in whichever namespace network's bridge actually lives in
// (host root for extern, entered above for intern) -- exactly where
// network_join.cpp's join_one_network() attaches veth's host-side end,
// just via a direct setns() here instead of wrap_for_network()'s argv
// wrapping (this whole function needs to keep running across later
// namespace switches, not just for one external command's duration).
if (run_process({"ip", "link", "set", host_tap_name, "master", bridge}).exit_code != 0) {
report_line(report_fd, "ERROR failed to attach host-side tap device to bridge\n");
_exit(1);
}
if (run_process({"ip", "link", "set", host_tap_name, "up"}).exit_code != 0) {
report_line(report_fd, "ERROR failed to bring host-side tap device up\n");
_exit(1);
}
// The already-open fd_host stays valid across this switch -- fds aren't
// namespace-scoped, only their *creation* is, the same property
// slirp4netns and this project's own wrap_for_root_namespace() (bwrap.cpp)
// already rely on.
if (!enter_namespace(fmt::format("/proc/{}/ns/net", container_ns_pid))) {
report_line(report_fd, "ERROR failed to enter container's network namespace\n");
_exit(1);
}
int fd_container = open_tap(container_if_name);
if (fd_container < 0) {
report_line(report_fd, "ERROR failed to create container-side tap device\n");
_exit(1);
}
report_line(report_fd, "OK\n");
close(report_fd);
// The actual "veth wire", reimplemented in userspace: whatever arrives
// on one fd is written verbatim to the other. No SIGTERM handler is
// installed -- default disposition (terminate) already closes both fds
// on the way out, which is all that's needed for the "no explicit
// teardown" property documented on stop_tap_relay() to hold.
struct pollfd fds[2] = {{fd_host, POLLIN, 0}, {fd_container, POLLIN, 0}};
std::vector<char> buffer(65536);
while (true) {
int ready = poll(fds, 2, -1);
if (ready < 0) {
if (errno == EINTR) {
continue;
}
break;
}
for (int i = 0; i < 2; ++i) {
if ((fds[i].revents & (POLLIN | POLLERR | POLLHUP | POLLNVAL)) == 0) {
continue;
}
int from = fds[i].fd;
int to = fds[1 - i].fd;
ssize_t n = read(from, buffer.data(), buffer.size());
if (n <= 0) {
// A namespace this fd's device lived in was torn down, or a
// genuine error -- either way, this relay's job is done.
_exit(0);
}
ssize_t written = 0;
while (written < n) {
ssize_t w = write(to, buffer.data() + written, static_cast<size_t>(n - written));
if (w < 0) {
if (errno == EINTR) {
continue;
}
_exit(0);
}
written += w;
}
}
}
_exit(0);
}
std::string read_relay_report(int fd) {
std::string report;
char chunk[256];
ssize_t n;
while ((n = read(fd, chunk, sizeof(chunk))) > 0) {
report.append(chunk, static_cast<size_t>(n));
}
return report;
}
} // namespace
std::optional<TapRelayHandle> create_tap_relay(const NetworkEntry& network, const std::string& bridge,
const std::string& host_tap_name, pid_t container_ns_pid,
const std::string& container_if_name) {
int report_pipe[2];
if (pipe2(report_pipe, O_CLOEXEC) != 0) {
spdlog::error("failed to set up tap-relay report pipe: {}", strerror(errno));
return std::nullopt;
}
pid_t pid = fork();
if (pid < 0) {
spdlog::error("failed to fork tap relay for network '{}': {}", network.name, strerror(errno));
close(report_pipe[0]);
close(report_pipe[1]);
return std::nullopt;
}
if (pid == 0) {
close(report_pipe[0]);
relay_child_main(report_pipe[1], network, bridge, host_tap_name, container_ns_pid, container_if_name);
}
close(report_pipe[1]);
std::string report = read_relay_report(report_pipe[0]);
close(report_pipe[0]);
if (report.rfind("OK", 0) != 0) {
spdlog::error("failed to create tap relay for network '{}': {}", network.name,
report.empty() ? "no response from relay process" : report);
int status = 0;
waitpid(pid, &status, 0); // relay_child_main always _exit()s before this point on failure
return std::nullopt;
}
return TapRelayHandle{pid, host_tap_name};
}
void stop_tap_relay(const TapRelayHandle& handle) {
if (kill(handle.relay_pid, SIGTERM) != 0 && errno != ESRCH) {
spdlog::warn("failed to signal tap relay pid {}: {}", handle.relay_pid, strerror(errno));
}
int status = 0;
waitpid(handle.relay_pid, &status, 0);
}
+71
View File
@@ -0,0 +1,71 @@
// 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 <optional>
#include <string>
#include <sys/types.h>
#include "config_file.h"
// A tap-backed substitute for one veth pair, used when should_use_veth()
// (network_bridge.h) is false: either the running kernel doesn't support
// veth at all (the real target device's kernel lacks CONFIG_VETH, though it
// does support tun/tap), or the network was created with --no-veth to
// exercise this path on a veth-capable machine. See
// docs/networking-design.md's addendum for the full design.
struct TapRelayHandle {
pid_t relay_pid;
std::string host_tap_name;
};
// Creates a tap-backed substitute for one veth pair between `network`'s
// bridge and the container namespace named by `container_ns_pid`, mirroring
// the role network_join.cpp's join_one_network() gives a veth pair:
// - a host-side tap device, named `host_tap_name` (caller-provided --
// callers already compute a unique name for the veth case the same
// way, e.g. join_one_network()'s own fnv1a-based suffix; this avoids a
// second, drifting copy of that naming scheme here), created wherever
// `network`'s bridge lives (network_bridge.h's wrap_for_network()
// namespace -- host root for extern, network's own persistent
// namespace for intern) and enslaved to `bridge`.
// - a container-side tap device, created directly inside the namespace
// named by `container_ns_pid` and named `container_if_name` (e.g.
// "eth0") -- no rename step needed, unlike veth's peer-name-then-rename.
// - a relay process holding both fds open for as long as it runs, copying
// raw Ethernet frames bidirectionally between them -- this is what
// actually reproduces a veth pair's kernel-internal wire, just via one
// userspace hop; the bridge continues to do the actual switching
// exactly as it does for a veth-joined container.
// Once this returns successfully, `container_if_name` is a completely
// ordinary interface from the container's own point of view -- IP
// assignment, routes, etc. all work exactly as they do for a veth-created
// interface (join_one_network()'s existing code, unchanged).
// Returns nullopt (logging why) on any setup failure; the relay process, if
// one was forked, is guaranteed to have already exited (and been reaped) by
// the time this returns in that case.
std::optional<TapRelayHandle> create_tap_relay(const NetworkEntry& network, const std::string& bridge,
const std::string& host_tap_name, pid_t container_ns_pid,
const std::string& container_if_name);
// Stops a relay started by create_tap_relay(): sends SIGTERM and reaps it.
// Both tap devices are expected to disappear on their own once the relay
// process exits and its fds close (neither was created with IFF_PERSIST) --
// the same "no explicit teardown needed" property veth already has; this
// function doesn't attempt any further cleanup beyond stopping the process.
void stop_tap_relay(const TapRelayHandle& handle);
+158 -13
View File
@@ -16,23 +16,29 @@
#include "self_test.h"
#include <sched.h>
#include <signal.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <cerrno>
#include <string>
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include "config_file.h"
#include "network_tap_relay.h"
#include "persistent_netns.h"
#include "process.h"
#include "sandbox_process.h"
int run_self_tests() {
namespace {
bool test_persistent_netns() {
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.
@@ -42,7 +48,7 @@ int run_self_tests() {
if (!create_persistent_netns(test_netns_name)) {
spdlog::error("self-test: failed to create persistent network namespace");
return 1;
return false;
}
// Checked from this (parent) process, after the child that actually did
// the unshare()/bind-mount has already exited -- this is exactly what
@@ -51,17 +57,156 @@ int run_self_tests() {
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;
return false;
}
if (!remove_persistent_netns(test_netns_name)) {
spdlog::error("self-test: failed to remove persistent network namespace");
return 1;
return false;
}
if (persistent_netns_exists(test_netns_name)) {
spdlog::error("self-test: persistent network namespace still exists after removing it");
return 1;
return false;
}
fmt::print("persistent network namespace create/verify/remove: OK\n");
return 0;
return true;
}
// Exercises network_tap_relay.h's create/verify/teardown cycle end to end: a
// throwaway bridge stands in for a real network's bridge (network_bridge.h's
// own provisioning isn't needed here -- a tap device only cares that *some*
// bridge interface exists to attach to), and a throwaway network namespace
// (kept alive by a child blocked in pause()) stands in for a real -r/--run
// session's isolated net namespace. Confirms: the host-side tap gets created
// and attached to the bridge; the container-side tap gets created, with the
// requested name, inside the target namespace; and -- the biggest unverified
// assumption in docs/networking-design.md's addendum -- both devices
// disappear on their own once stop_tap_relay() stops the relay process, with
// no explicit `ip link del` needed.
bool test_tap_relay() {
const std::string test_bridge = "slkselftest0";
const std::string host_tap = "thselftest0";
const std::string container_if = "ethselftest";
if (run_process({"ip", "link", "add", test_bridge, "type", "bridge"}).exit_code != 0) {
spdlog::error("self-test: failed to create throwaway test bridge");
return false;
}
if (run_process({"ip", "link", "set", test_bridge, "up"}).exit_code != 0) {
spdlog::error("self-test: failed to bring up throwaway test bridge");
run_process({"ip", "link", "del", test_bridge});
return false;
}
// A throwaway, otherwise-empty network namespace standing in for a real
// session's own -- kept alive only by this child blocking in pause()
// until signaled, mirroring how bwrap's own sandboxed child keeps a real
// session's namespace alive for as long as it runs.
pid_t container_pid = fork();
if (container_pid < 0) {
spdlog::error("self-test: failed to fork throwaway container namespace holder");
run_process({"ip", "link", "del", test_bridge});
return false;
}
if (container_pid == 0) {
if (unshare(CLONE_NEWNET) != 0) {
_exit(1);
}
pause();
_exit(0);
}
// fork() returning to this (parent) process doesn't mean the child has
// actually reached its own unshare(CLONE_NEWNET) call yet -- the same
// race network_join.cpp's wait_for_isolated_net_namespace() already
// guards against for a real session's namespace. Poll (bounded, 1s) for
// container_pid's own net namespace to actually differ from ours before
// trusting its pid.
bool isolated = false;
for (int elapsed_ms = 0; elapsed_ms <= 1000; elapsed_ms += 20) {
if (namespace_isolated(getpid(), container_pid, "net")) {
isolated = true;
break;
}
struct timespec ts {
0, 20L * 1000000L
};
while (nanosleep(&ts, &ts) != 0 && errno == EINTR) {
}
}
if (!isolated) {
spdlog::error("self-test: throwaway container namespace never isolated");
kill(container_pid, SIGKILL);
int reap_status = 0;
waitpid(container_pid, &reap_status, 0);
run_process({"ip", "link", "del", test_bridge});
return false;
}
bool ok = true;
NetworkEntry network{"selftest-tap-relay", NetworkKind::extern_, "", false, "", true};
auto relay = create_tap_relay(network, test_bridge, host_tap, container_pid, container_if);
if (!relay) {
spdlog::error("self-test: failed to create tap relay");
ok = false;
}
if (ok) {
auto host_check = run_process({"ip", "link", "show", host_tap});
if (host_check.exit_code != 0 || host_check.stdout_output.find("master " + test_bridge) == std::string::npos) {
spdlog::error("self-test: host-side tap device missing or not attached to the bridge");
ok = false;
}
}
if (ok) {
auto container_check = run_process(
{"nsenter", fmt::format("--net=/proc/{}/ns/net", container_pid), "--", "ip", "link", "show", container_if});
if (container_check.exit_code != 0) {
spdlog::error("self-test: container-side tap device missing inside the target namespace");
ok = false;
}
}
if (relay) {
stop_tap_relay(*relay);
if (run_process({"ip", "link", "show", host_tap}).exit_code == 0) {
spdlog::error("self-test: host-side tap device still exists after stopping the relay");
ok = false;
}
if (run_process({"nsenter", fmt::format("--net=/proc/{}/ns/net", container_pid), "--", "ip", "link", "show",
container_if})
.exit_code == 0) {
spdlog::error("self-test: container-side tap device still exists after stopping the relay");
ok = false;
}
}
kill(container_pid, SIGKILL);
int status = 0;
waitpid(container_pid, &status, 0);
run_process({"ip", "link", "del", test_bridge});
if (ok) {
fmt::print("tap-relay create/attach/teardown: OK\n");
}
return ok;
}
} // namespace
int run_self_tests() {
// Both tests below need root: create_persistent_netns() bind-mounts, and
// test_tap_relay() creates bridges/tap devices -- report and skip rather
// than treating a rootless dev machine as a failure.
if (geteuid() != 0) {
fmt::print("skipping persistent network namespace and tap-relay tests (requires root)\n");
return 0;
}
bool ok = test_persistent_netns();
ok = test_tap_relay() && ok;
return ok ? 0 : 1;
}
+4 -3
View File
@@ -18,7 +18,8 @@
// Implements -t/--test, this project's own built-in self-test mode (distinct
// 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.
// persistent_netns.{h,cpp}'s create/verify/remove cycle and
// network_tap_relay.{h,cpp}'s create/attach/teardown cycle (both 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();