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:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user