Wire -t/--test to Catch2's own argv-driven Session::run()
-t now accepts trailing args (mirroring -r/-x's own trailing-command capture, ParsedArgs::test_args), forwarded unmodified to Catch2. Bare -t runs every registered TEST_CASE (currently none); a tag expression after '--' selects a subset once real tests land, e.g. -t -- "[unit]". The '--' matters since Catch2's own -r/--reporter and -c/--section collide with slocker-lite's -r/--run and -c/--cleanup. Guarded by config.h's ENABLE_TESTS macro (from Meson's existing enable_tests option, already linking catch2_dep into the binary but never actually used until now) -- a -Denable_tests=false build prints a clear message instead of failing to link. The 3 hand-rolled root-only self-tests this replaced (persistent-netns, tap-relay, dns-resolver) are being ported to proper tagged TEST_CASEs in a follow-up commit, not lost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
+20
-3
@@ -133,7 +133,7 @@ void print_usage(const char* prog) {
|
||||
" {0} --list-processes\n"
|
||||
" {0} --clean-processes\n"
|
||||
" {0} -w|--write-config\n"
|
||||
" {0} -t|--test\n"
|
||||
" {0} -t|--test [-- <catch-command-line-options>]\n"
|
||||
" {0} -h|--help\n"
|
||||
" {0} -V|--version\n"
|
||||
"\n"
|
||||
@@ -279,7 +279,20 @@ void print_usage(const char* prog) {
|
||||
" every option's current or default value --\n"
|
||||
" useful to bootstrap one for hand-editing.\n"
|
||||
" Prints the config file's full path\n"
|
||||
" -t, --test run the test suite\n"
|
||||
" -t, --test [-- <catch-command-line-options>]\n"
|
||||
" run the built-in Catch2 test suite. Bare -t\n"
|
||||
" runs everything; select a category with a tag\n"
|
||||
" expression, e.g. -t -- \"[unit]\",\n"
|
||||
" -t -- \"[integration]~[net]\",\n"
|
||||
" -t -- \"[integration][net]~[root]\",\n"
|
||||
" -t -- \"[integration][root]\", or filter out\n"
|
||||
" slow tests with -t -- \"~[slow]\". The '--' is\n"
|
||||
" needed before any option that looks like one\n"
|
||||
" of slocker-lite's own (Catch2's -r/--reporter\n"
|
||||
" collides with -r/--run, for example) -- see\n"
|
||||
" -t -- --help for Catch2's own full option list\n"
|
||||
" (not compiled in if built with\n"
|
||||
" -Denable_tests=false)\n"
|
||||
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
||||
" error, critical, off)\n"
|
||||
" -h, --help print this help and exit\n"
|
||||
@@ -572,7 +585,7 @@ std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (out.mode != Mode::run && out.mode != Mode::exec && optind != argc) {
|
||||
if (out.mode != Mode::run && out.mode != Mode::exec && out.mode != Mode::test && optind != argc) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
@@ -586,6 +599,10 @@ std::optional<int> parse_args(int argc, char* argv[], ParsedArgs& out) {
|
||||
out.command.assign(argv + optind, argv + argc);
|
||||
}
|
||||
|
||||
if (out.mode == Mode::test) {
|
||||
out.test_args.assign(argv + optind, argv + argc);
|
||||
}
|
||||
|
||||
if (out.mode == Mode::exec) {
|
||||
if (out.command.empty()) {
|
||||
spdlog::error("--exec requires a command to run");
|
||||
|
||||
@@ -89,6 +89,15 @@ struct ParsedArgs {
|
||||
// Trailing argv (after getopt_long stops), populated only for
|
||||
// Mode::run/Mode::exec -- the command to run, or to run under -x/--exec.
|
||||
std::vector<std::string> command;
|
||||
// Trailing argv (after getopt_long stops), populated only for
|
||||
// Mode::test -- forwarded as-is to Catch2's own Session::run() (see
|
||||
// self_test.h), not a command to run, hence its own field rather than
|
||||
// reusing `command` above. A literal '--' before any Catch2 flag that
|
||||
// looks like one of slocker-lite's own short options (Catch2's own
|
||||
// -r/--reporter collides with -r/--run, -c/--section with -c/--cleanup)
|
||||
// is required for the same reason -x/--exec's own trailing command
|
||||
// needs one for a command starting with '-'.
|
||||
std::vector<std::string> test_args;
|
||||
// Parsed and range-validated from mode_arg when mode == Mode::exec.
|
||||
std::optional<pid_t> exec_pid;
|
||||
// Parsed and range-validated from mode_arg when mode == Mode::kill.
|
||||
|
||||
+1
-1
@@ -891,7 +891,7 @@ int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config
|
||||
case Mode::clean_processes:
|
||||
return clean_processes_command();
|
||||
case Mode::test:
|
||||
return run_self_tests();
|
||||
return run_self_tests(args.test_args);
|
||||
case Mode::exec:
|
||||
return exec_in_session(*args.exec_pid, args.command, args.user_flag, args.group_flag);
|
||||
case Mode::kill:
|
||||
|
||||
+28
-479
@@ -16,489 +16,38 @@
|
||||
|
||||
#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 "config.h"
|
||||
|
||||
#if ENABLE_TESTS
|
||||
#include <catch2/catch_session.hpp>
|
||||
#else
|
||||
#include <fmt/core.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#endif
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
int run_self_tests(const std::vector<std::string>& args) {
|
||||
#if ENABLE_TESTS
|
||||
// args[0] is conventionally the program name in Catch2's eyes (used in
|
||||
// some of its own diagnostic/help output) -- it never sees the real
|
||||
// argv[0], since ParsedArgs::test_args only ever holds what came after
|
||||
// -t/--test on the command line (see cli_args.cpp). The vector<string>
|
||||
// is kept alive for the duration of the call so the char* vector handed
|
||||
// to Catch2 stays valid.
|
||||
std::vector<std::string> owned_args = {"slocker-lite -t"};
|
||||
owned_args.insert(owned_args.end(), args.begin(), args.end());
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "config_file.h"
|
||||
#include "network_dns.h"
|
||||
#include "network_tap_relay.h"
|
||||
#include "persistent_netns.h"
|
||||
#include "process.h"
|
||||
#include "sandbox_process.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool test_persistent_netns() {
|
||||
constexpr std::string_view test_netns_name = "selftest";
|
||||
|
||||
// Clean up a leftover from a previous interrupted run, if any, before
|
||||
// starting -- create_persistent_netns() refuses to overwrite an existing
|
||||
// live namespace.
|
||||
if (persistent_netns_exists(test_netns_name)) {
|
||||
remove_persistent_netns(test_netns_name);
|
||||
std::vector<char*> argv;
|
||||
argv.reserve(owned_args.size());
|
||||
for (auto& arg : owned_args) {
|
||||
argv.push_back(arg.data());
|
||||
}
|
||||
|
||||
if (!create_persistent_netns(test_netns_name)) {
|
||||
spdlog::error("self-test: failed to create persistent network namespace");
|
||||
return false;
|
||||
}
|
||||
// Checked from this (parent) process, after the child that actually did
|
||||
// the unshare()/bind-mount has already exited -- this is exactly what
|
||||
// confirms the namespace outlives its creating process, the whole point
|
||||
// of the bind-mount technique.
|
||||
if (!persistent_netns_exists(test_netns_name)) {
|
||||
spdlog::error("self-test: persistent network namespace missing right after creating it");
|
||||
remove_persistent_netns(test_netns_name);
|
||||
return false;
|
||||
}
|
||||
if (!remove_persistent_netns(test_netns_name)) {
|
||||
spdlog::error("self-test: failed to remove persistent network namespace");
|
||||
return false;
|
||||
}
|
||||
if (persistent_netns_exists(test_netns_name)) {
|
||||
spdlog::error("self-test: persistent network namespace still exists after removing it");
|
||||
return false;
|
||||
}
|
||||
|
||||
fmt::print("persistent network namespace create/verify/remove: OK\n");
|
||||
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, inside a throwaway
|
||||
// persistent namespace (persistent_netns.h) standing in for a real network's
|
||||
// own dedicated one -- create_tap_relay() now always enters that namespace
|
||||
// first (network_bridge.cpp's wrap_for_network() reaches a real network's
|
||||
// bridge the same way, both kinds), so this test needs one too, even though
|
||||
// network_bridge.h's own provisioning isn't otherwise exercised here (a tap
|
||||
// device only cares that *some* bridge interface exists to attach to). 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, once stop_tap_relay() stops the relay, the
|
||||
// host-side device is explicitly removed (`ip link del`, since it's now
|
||||
// created via `ip tuntap add` and no longer disappears on its own just
|
||||
// because its one-and-only fd closes -- see stop_tap_relay()'s own doc
|
||||
// comment) while the container-side device deliberately survives, unaffected
|
||||
// by the relay stopping: it only goes away once the container's own network
|
||||
// namespace itself is torn down (below, when this test kills container_pid).
|
||||
bool test_tap_relay() {
|
||||
const std::string test_network_name = "selftest-tap-relay";
|
||||
const std::string test_bridge = "slkselftest0";
|
||||
const std::string host_tap = "thselftest0";
|
||||
const std::string container_if = "ethselftest";
|
||||
|
||||
if (persistent_netns_exists(test_network_name)) {
|
||||
remove_persistent_netns(test_network_name);
|
||||
}
|
||||
if (!create_persistent_netns(test_network_name)) {
|
||||
spdlog::error("self-test: failed to create persistent namespace for tap-relay test");
|
||||
return false;
|
||||
}
|
||||
std::vector<std::string> netns_wrap = {
|
||||
"nsenter", fmt::format("--net={}", persistent_netns_path(test_network_name).string()), "--"};
|
||||
auto in_netns = [&netns_wrap](std::vector<std::string> argv) {
|
||||
std::vector<std::string> wrapped = netns_wrap;
|
||||
wrapped.insert(wrapped.end(), argv.begin(), argv.end());
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
if (run_process(in_netns({"ip", "link", "add", test_bridge, "type", "bridge"})).exit_code != 0) {
|
||||
spdlog::error("self-test: failed to create throwaway test bridge");
|
||||
remove_persistent_netns(test_network_name);
|
||||
return false;
|
||||
}
|
||||
if (run_process(in_netns({"ip", "link", "set", test_bridge, "up"})).exit_code != 0) {
|
||||
spdlog::error("self-test: failed to bring up throwaway test bridge");
|
||||
run_process(in_netns({"ip", "link", "del", test_bridge}));
|
||||
remove_persistent_netns(test_network_name);
|
||||
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(in_netns({"ip", "link", "del", test_bridge}));
|
||||
remove_persistent_netns(test_network_name);
|
||||
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(in_netns({"ip", "link", "del", test_bridge}));
|
||||
remove_persistent_netns(test_network_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
NetworkEntry network{test_network_name, 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(in_netns({"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(in_netns({"ip", "link", "show", host_tap})).exit_code == 0) {
|
||||
spdlog::error("self-test: host-side tap device still exists after stopping the relay");
|
||||
ok = false;
|
||||
}
|
||||
// The container-side device is expected to survive the relay
|
||||
// stopping -- it's persistent now (create_persistent_tap(),
|
||||
// network_tap_relay.cpp) and lives inside the container's own
|
||||
// network namespace, which stopping the relay doesn't touch at all.
|
||||
// It only disappears once that namespace itself is destroyed (this
|
||||
// test does that below, by killing container_pid) -- not re-checked
|
||||
// here, since nsenter can't target a namespace whose only holding
|
||||
// process has already exited.
|
||||
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 unexpectedly gone after stopping the relay");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
kill(container_pid, SIGKILL);
|
||||
int status = 0;
|
||||
waitpid(container_pid, &status, 0);
|
||||
run_process(in_netns({"ip", "link", "del", test_bridge}));
|
||||
remove_persistent_netns(test_network_name);
|
||||
|
||||
if (ok) {
|
||||
fmt::print("tap-relay create/attach/teardown: OK\n");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Builds a minimal DNS query (one question, A record, class IN) for `name`,
|
||||
// wire format -- just enough to exercise the resolver's own answer path, not
|
||||
// a general-purpose DNS client.
|
||||
std::vector<uint8_t> build_dns_a_query(const std::string& name, uint16_t id) {
|
||||
std::vector<uint8_t> packet;
|
||||
auto push16 = [&](uint16_t v) {
|
||||
packet.push_back(static_cast<uint8_t>(v >> 8));
|
||||
packet.push_back(static_cast<uint8_t>(v & 0xff));
|
||||
};
|
||||
push16(id);
|
||||
push16(0x0100); // standard query, recursion desired
|
||||
push16(1); // qdcount
|
||||
push16(0);
|
||||
push16(0);
|
||||
push16(0); // ancount/nscount/arcount
|
||||
|
||||
size_t start = 0;
|
||||
while (start <= name.size()) {
|
||||
size_t dot = name.find('.', start);
|
||||
std::string label = name.substr(start, dot == std::string::npos ? std::string::npos : dot - start);
|
||||
packet.push_back(static_cast<uint8_t>(label.size()));
|
||||
packet.insert(packet.end(), label.begin(), label.end());
|
||||
if (dot == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = dot + 1;
|
||||
}
|
||||
packet.push_back(0);
|
||||
push16(1); // qtype A
|
||||
push16(1); // qclass IN
|
||||
return packet;
|
||||
}
|
||||
|
||||
// Sends build_dns_a_query(name) to 127.0.0.1:53 from *inside* `ns_pid`'s own
|
||||
// network namespace (a plain setns() here, same technique
|
||||
// network_tap_relay.cpp's own enter_namespace() uses, done in a throwaway
|
||||
// forked child so this process's own namespace is untouched either way) and
|
||||
// parses the first answer's 4-byte A-record rdata back out. Returns false on
|
||||
// any failure (including NXDOMAIN/no answer) -- out_ip is only meaningful
|
||||
// when this returns true.
|
||||
bool query_dns_a_record(pid_t ns_pid, const std::string& name, std::string& out_ip) {
|
||||
int report_pipe[2];
|
||||
if (pipe(report_pipe) != 0) {
|
||||
return false;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
close(report_pipe[0]);
|
||||
close(report_pipe[1]);
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
close(report_pipe[0]);
|
||||
int ns_fd = open(fmt::format("/proc/{}/ns/net", ns_pid).c_str(), O_RDONLY);
|
||||
if (ns_fd < 0 || setns(ns_fd, CLONE_NEWNET) != 0) {
|
||||
_exit(1);
|
||||
}
|
||||
close(ns_fd);
|
||||
|
||||
int sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (sock < 0) {
|
||||
_exit(1);
|
||||
}
|
||||
struct timeval tv {
|
||||
2, 0
|
||||
};
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr {};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(53);
|
||||
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
auto query = build_dns_a_query(name, 0x1234);
|
||||
if (sendto(sock, query.data(), query.size(), 0, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) <
|
||||
0) {
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
uint8_t buf[512];
|
||||
socklen_t addrlen = sizeof(addr);
|
||||
ssize_t n = recvfrom(sock, buf, sizeof(buf), 0, reinterpret_cast<struct sockaddr*>(&addr), &addrlen);
|
||||
if (n < 12) {
|
||||
_exit(1);
|
||||
}
|
||||
uint16_t rcode = static_cast<uint16_t>(buf[3] & 0x0f);
|
||||
uint16_t ancount = static_cast<uint16_t>((buf[6] << 8) | buf[7]);
|
||||
if (ancount == 0) {
|
||||
// Exit code carries the RCODE (offset by 10 to stay clear of the
|
||||
// other exit codes above) so a failure's own spdlog::debug below
|
||||
// can say *why* -- e.g. 13 = NXDOMAIN, 15 = REFUSED (the latter
|
||||
// is exactly what caught this project's own dnsmasq
|
||||
// privilege-drop bug during development, see start_dns_resolver()'s
|
||||
// own --user=root/--group=root comment).
|
||||
_exit(10 + rcode);
|
||||
}
|
||||
|
||||
// Skip the 12-byte header, then the question (the exact same qname
|
||||
// this process just sent, plus 4 bytes qtype/qclass).
|
||||
size_t pos = 12;
|
||||
while (pos < static_cast<size_t>(n) && buf[pos] != 0) {
|
||||
pos += static_cast<size_t>(buf[pos]) + 1;
|
||||
}
|
||||
pos += 1 + 4;
|
||||
|
||||
// The answer's own name field: a compression pointer (top two bits
|
||||
// set) is 2 bytes; a literal label sequence is read the same way as
|
||||
// the question's own qname above.
|
||||
if (pos < static_cast<size_t>(n) && (buf[pos] & 0xc0) == 0xc0) {
|
||||
pos += 2;
|
||||
} else {
|
||||
while (pos < static_cast<size_t>(n) && buf[pos] != 0) {
|
||||
pos += static_cast<size_t>(buf[pos]) + 1;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
pos += 2 + 2 + 4; // type, class, ttl
|
||||
if (pos + 2 > static_cast<size_t>(n)) {
|
||||
_exit(1);
|
||||
}
|
||||
uint16_t rdlength = static_cast<uint16_t>((buf[pos] << 8) | buf[pos + 1]);
|
||||
pos += 2;
|
||||
if (rdlength != 4 || pos + 4 > static_cast<size_t>(n)) {
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
char ip_str[32];
|
||||
std::snprintf(ip_str, sizeof(ip_str), "%u.%u.%u.%u", buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]);
|
||||
ssize_t unused = write(report_pipe[1], ip_str, std::strlen(ip_str));
|
||||
(void)unused;
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
close(report_pipe[1]);
|
||||
char buf[64] = {0};
|
||||
ssize_t n = read(report_pipe[0], buf, sizeof(buf) - 1);
|
||||
close(report_pipe[0]);
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
if (n <= 0) {
|
||||
if (WIFEXITED(status)) {
|
||||
spdlog::debug("self-test: DNS query for '{}' got no answer, child exit code {}", name,
|
||||
WEXITSTATUS(status));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
out_ip.assign(buf, static_cast<size_t>(n));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exercises network_dns.h's create/answer/teardown cycle end to end: a
|
||||
// throwaway network namespace (same fork+unshare(CLONE_NEWNET)+pause()
|
||||
// technique test_tap_relay() uses) stands in for a real -r/--run session's
|
||||
// own isolated namespace. A single dns-hosts record is written by hand
|
||||
// (record_dns_host()) for a fabricated `intern` network, start_dns_resolver()
|
||||
// is pointed at it, and query_dns_a_record() above confirms the resolver
|
||||
// actually answers with the recorded address -- real UDP wire format, not
|
||||
// just "the process started". Skipped (not a failure) if dnsmasq isn't
|
||||
// installed, same as the whole suite's own root-only skip.
|
||||
bool test_dns_resolver() {
|
||||
if (!is_dnsmasq_available()) {
|
||||
fmt::print("skipping DNS resolver test (dnsmasq not found in PATH)\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string test_network_name = "selftest-dns";
|
||||
const std::string test_hostname = "selftest-peer";
|
||||
const std::string test_ip = "10.99.99.2";
|
||||
|
||||
pid_t container_pid = fork();
|
||||
if (container_pid < 0) {
|
||||
spdlog::error("self-test: failed to fork throwaway container namespace holder for DNS test");
|
||||
return false;
|
||||
}
|
||||
if (container_pid == 0) {
|
||||
if (unshare(CLONE_NEWNET) != 0) {
|
||||
_exit(1);
|
||||
}
|
||||
// Not automatically up in a fresh network namespace -- the resolver
|
||||
// needs to bind 127.0.0.1.
|
||||
run_process({"ip", "link", "set", "lo", "up"});
|
||||
pause();
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
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 (DNS test)");
|
||||
kill(container_pid, SIGKILL);
|
||||
int reap_status = 0;
|
||||
waitpid(container_pid, &reap_status, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(dns_hosts_dir(test_network_name), ec);
|
||||
record_dns_host(test_network_name, "selftest-peer-container", 1, test_ip, test_hostname);
|
||||
|
||||
bool ok = true;
|
||||
NetworkEntry test_network{test_network_name, NetworkKind::intern, "", false, "", true};
|
||||
JoinedNetwork joined_network{test_network, test_ip, std::nullopt};
|
||||
auto resolver = start_dns_resolver("selftest-dns-session", container_pid, container_pid, {joined_network});
|
||||
if (!resolver) {
|
||||
spdlog::error("self-test: failed to start DNS resolver");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
std::string answered_ip;
|
||||
if (!query_dns_a_record(container_pid, test_hostname, answered_ip) || answered_ip != test_ip) {
|
||||
spdlog::error("self-test: DNS resolver did not answer '{}' correctly (got '{}', expected '{}')",
|
||||
test_hostname, answered_ip, test_ip);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolver) {
|
||||
stop_dns_resolver(*resolver);
|
||||
}
|
||||
remove_dns_resolver_record("selftest-dns-session", container_pid);
|
||||
remove_dns_host_record(test_network_name, "selftest-peer-container", 1);
|
||||
std::filesystem::remove_all(dns_hosts_dir(test_network_name), ec);
|
||||
kill(container_pid, SIGKILL);
|
||||
int status = 0;
|
||||
waitpid(container_pid, &status, 0);
|
||||
|
||||
if (ok) {
|
||||
fmt::print("dns-resolver create/answer/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;
|
||||
ok = test_dns_resolver() && ok;
|
||||
|
||||
return ok ? 0 : 1;
|
||||
Catch::Session session;
|
||||
return session.run(static_cast<int>(argv.size()), argv.data());
|
||||
#else
|
||||
(void)args;
|
||||
fmt::print(stderr,
|
||||
"tests were not compiled into this build (reconfigure with "
|
||||
"-Denable_tests=true and rebuild)\n");
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
+16
-7
@@ -16,10 +16,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
// Implements -t/--test, this project's own built-in self-test mode (distinct
|
||||
// from the Meson-driven fixture smoke test under tests/). Currently exercises
|
||||
// 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();
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Implements -t/--test, this project's own built-in Catch2-driven test
|
||||
// suite (distinct from the Meson-driven fixture smoke test under tests/,
|
||||
// which stays a separate, always-on mount/unmount/cleanup check).
|
||||
// `args` is `ParsedArgs::test_args` (cli_args.h) -- everything on the
|
||||
// command line after `-t`, forwarded to Catch2's own Session::run()
|
||||
// unmodified (tag expressions, --list-tests, --reporter, etc.); a bare
|
||||
// `-t` (empty `args`) runs every registered TEST_CASE. Only compiled to
|
||||
// actually run Catch2 when this build has ENABLE_TESTS set (config.h,
|
||||
// from Meson's `enable_tests` option, default on) -- otherwise prints a
|
||||
// clear "not compiled into this build" message and returns nonzero,
|
||||
// since a -Denable_tests=false build has no TEST_CASEs (or Catch2 itself)
|
||||
// linked in at all.
|
||||
int run_self_tests(const std::vector<std::string>& args);
|
||||
|
||||
Reference in New Issue
Block a user