diff --git a/CLAUDE.md b/CLAUDE.md index 61da62d..14e9aa4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `), the + container-side tap gets created with the requested name inside the target + namespace (checked via `nsenter --net=/proc//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 diff --git a/meson.build b/meson.build index 328f3c4..c35d91a 100644 --- a/meson.build +++ b/meson.build @@ -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) diff --git a/src/network_tap_relay.cpp b/src/network_tap_relay.cpp new file mode 100644 index 0000000..4d68b90 --- /dev/null +++ b/src/network_tap_relay.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#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//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 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(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(n)); + } + return report; +} + +} // namespace + +std::optional 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); +} diff --git a/src/network_tap_relay.h b/src/network_tap_relay.h new file mode 100644 index 0000000..b9d28fa --- /dev/null +++ b/src/network_tap_relay.h @@ -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 +#include + +#include + +#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 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); diff --git a/src/self_test.cpp b/src/self_test.cpp index 00bb7f4..e686680 100644 --- a/src/self_test.cpp +++ b/src/self_test.cpp @@ -16,23 +16,29 @@ #include "self_test.h" +#include +#include +#include +#include #include +#include +#include + #include #include +#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; } diff --git a/src/self_test.h b/src/self_test.h index deca4e3..af917dc 100644 --- a/src/self_test.h +++ b/src/self_test.h @@ -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();