174b6b4c29
Running the full device test suite on the real Android target found a genuine test-harness gap: every extern-network test in test_network_join_scenarios.cpp failed with "RTNETLINK answers: File exists" adding its own uplink route. Root cause: each test runs under its own ScratchXdgDirs, so its own persistent.yaml starts empty every time -- allocate_ipv4_subnet()'s own auto-allocation always picks the very first slot (10.168.0.0/24) with no way to see whatever real, non-test networks already exist on the host. The device happens to have a real, long-lived "extern" network already occupying exactly that subnet from prior manual testing, and since extern's uplink adds a route back into the (necessarily shared, host-root) routing table -- unlike intern, whose routing lives entirely inside its own isolated per-network namespace -- every extern test collided with it. Not a production bug: a real end user only ever has one persistent.yaml where allocation correctly sees every existing entry. Fixed by giving each of the 10 test networks in test_network_join_scenarios.cpp its own fixed, explicit subnet (--subnet) in 10.169.0.0/16 -- a different /16 than production's own default 10.168.0.0/16 range, so a test run can't collide with a real network regardless of how many the host already has. Also renamed every network/hostname/container-name string literal used across the test suite (test_network_join_scenarios.cpp, test_root_networking.cpp, test_rootless_run.cpp, test_session_cleanup.cpp) from "selftest*" to "test-*", to further reduce the chance of colliding with anything a real invocation might already be using. Low-level interface device literals (slkselftest0/thselftest0/ ethselftest in test_root_networking.cpp) are left as-is -- they're internal identifiers for a throwaway unit test, not network or container names. Verified: clean rebuild, meson test, and 3 consecutive [integration][root] suite runs (61 assertions, 14 test cases) with no failures.
660 lines
27 KiB
C++
660 lines
27 KiB
C++
// 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.
|
|
|
|
// [integration][root][net]: end-to-end -n/--network join scenarios, run
|
|
// twice per scenario -- once with a real veth pair (the default, when the
|
|
// kernel supports it), once forced onto the tap+relay fallback
|
|
// (network_tap_relay.h, --with-veth=false at creation) -- since the two
|
|
// mechanisms are genuinely different implementations of "get a container's
|
|
// eth<N> talking to the network's bridge", not just a policy toggle over
|
|
// otherwise-identical code. IPv6 is deliberately left untested here (every
|
|
// network below is created with --with-ipv6=false): it has a known
|
|
// device-specific peculiarity (see docs/networking-design.md) that's out of
|
|
// scope for this pass.
|
|
//
|
|
// Needs root (real bridges/namespaces/iptables) and a real busybox fixture
|
|
// (find_busybox_fixture()) -- SKIPs cleanly on either missing.
|
|
//
|
|
// Every network created here is named "test-..." and given an explicit
|
|
// subnet in 10.169.0.0/16 (see create_test_network()'s own doc comment
|
|
// below for why the explicit subnet matters) -- both deliberately distinct
|
|
// from anything a real invocation would plausibly already have in use, so
|
|
// a test run can't collide with pre-existing host state.
|
|
|
|
#include <poll.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <sys/wait.h>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <fmt/core.h>
|
|
|
|
#include "cli_args.h"
|
|
#include "commands.h"
|
|
#include "config_file.h"
|
|
#include "fixtures.h"
|
|
#include "network_dns.h"
|
|
#include "network_subnet.h"
|
|
|
|
namespace {
|
|
|
|
// Mirrors exactly what two separate real CLI invocations would each see:
|
|
// main()'s own merge of the effective global config (g_test_app_config,
|
|
// fixtures.h -- set once for this whole -t run) with persistent.yaml's own
|
|
// current volumes/networks, freshly reloaded from disk. Needed because
|
|
// create_network_command()/run_container() etc. always read/write
|
|
// persistent_file_path() directly now (config_file.h) -- a stale in-memory
|
|
// AppConfig kept across multiple dispatch_command() calls in this same test
|
|
// process would miss networks a *different* call already created.
|
|
AppConfig reload_config() {
|
|
AppConfig config = g_test_app_config;
|
|
if (auto persistent = load_persistent_config(persistent_file_path())) {
|
|
config.volumes = persistent->volumes;
|
|
config.networks = persistent->networks;
|
|
}
|
|
return config;
|
|
}
|
|
|
|
// Creates `name` via the real -n/--network (create) CLI path
|
|
// (dispatch_command(), Mode::network) -- --with-ipv6=false always (see this
|
|
// file's own top comment for why). `subnet` is always passed explicitly
|
|
// (--subnet) rather than left to allocate_ipv4_subnet()'s own auto-allocation:
|
|
// each test runs under its own ScratchXdgDirs, so its own persistent.yaml
|
|
// starts empty every time -- auto-allocation would always pick the very
|
|
// first slot (10.168.0.0/24) with no way to see whatever real, non-test
|
|
// networks already exist on the host it's running on. Confirmed by direct
|
|
// testing on the real Android target device, which happens to have a real,
|
|
// long-lived "extern" network already occupying exactly that subnet: every
|
|
// extern-network test failed with "RTNETLINK answers: File exists" adding
|
|
// its own uplink route for the identical 10.168.0.0/24 destination into the
|
|
// (necessarily shared, host-root) routing table -- not a production bug,
|
|
// since a real end user only ever has one persistent.yaml where allocation
|
|
// correctly sees every existing entry, but a real, reproducible test-harness
|
|
// gap once tests run alongside pre-existing host state. intern networks
|
|
// never hit this (their routing lives entirely inside their own isolated,
|
|
// per-network namespace, invisible to and unaffected by anything in host
|
|
// root), which is exactly why only extern tests failed. Fixed by giving each
|
|
// test network below its own fixed, explicit subnet in 10.169.0.0/16 --
|
|
// deliberately a different /16 than production's own default 10.168.0.0/16
|
|
// range, so test runs can never collide with a real network regardless of
|
|
// how many the host already has.
|
|
bool create_test_network(const std::string& name, NetworkKind kind, bool veth, const std::string& subnet) {
|
|
ParsedArgs args;
|
|
args.mode = Mode::network;
|
|
args.network_specs = {name};
|
|
args.network_extern_flag = (kind == NetworkKind::extern_);
|
|
args.network_intern_flag = (kind == NetworkKind::intern);
|
|
args.network_with_veth_flag = veth;
|
|
args.network_with_ipv6_flag = false;
|
|
args.network_subnet_flag = subnet;
|
|
AppConfig config = reload_config();
|
|
CapturedStdout capture;
|
|
return dispatch_command(args, "/nonexistent/unused-config.yaml", config) == 0;
|
|
}
|
|
|
|
// Tears down a network's live host-side state and removes it from the
|
|
// config, via the real --delete-network-full CLI path. Best-effort (used
|
|
// from RAII cleanup, where there's nothing useful to do with a failure).
|
|
void delete_test_network(const std::string& name) {
|
|
ParsedArgs args;
|
|
args.mode = Mode::delete_network_full;
|
|
args.mode_arg = name;
|
|
AppConfig config = reload_config();
|
|
CapturedStdout capture;
|
|
dispatch_command(args, "/nonexistent/unused-config.yaml", config);
|
|
}
|
|
|
|
// The subnet `name` was actually allocated -- read back from persistent.yaml
|
|
// rather than duplicating allocate_ipv4_subnet()'s own numbering logic here.
|
|
std::optional<std::string> test_network_subnet(const std::string& name) {
|
|
auto config = reload_config();
|
|
for (const auto& network : config.networks) {
|
|
if (network.name == name) {
|
|
return network.subnet;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// RAII wrapper around create_test_network()/delete_test_network() -- so a
|
|
// REQUIRE() failure partway through a test still tears down real host state
|
|
// (bridge, persistent namespace, iptables rules) via normal C++ stack
|
|
// unwinding, the same guarantee test_root_networking.cpp's own CHECK-not-
|
|
// REQUIRE convention exists for, just via RAII instead.
|
|
class TestNetwork {
|
|
public:
|
|
TestNetwork(std::string name, NetworkKind kind, bool veth, const std::string& subnet) : name_(std::move(name)) {
|
|
created_ = create_test_network(name_, kind, veth, subnet);
|
|
}
|
|
~TestNetwork() {
|
|
if (created_) {
|
|
delete_test_network(name_);
|
|
}
|
|
}
|
|
TestNetwork(const TestNetwork&) = delete;
|
|
TestNetwork& operator=(const TestNetwork&) = delete;
|
|
|
|
bool created() const { return created_; }
|
|
const std::string& name() const { return name_; }
|
|
|
|
private:
|
|
std::string name_;
|
|
bool created_ = false;
|
|
};
|
|
|
|
// A container joined to a network, started in the background (forked) so a
|
|
// *second* container can be started concurrently to interact with it (e.g.
|
|
// ping it) while it's still running. Waits for a "READY" marker on the
|
|
// child's own stdout before returning, printed only once the child's own
|
|
// script has confirmed its eth<N> actually has an assigned address (not
|
|
// merely exists -- see wait_for_eth0_then()'s own doc comment below for why
|
|
// that distinction matters) -- join_networks()
|
|
// (network_join.cpp) runs *concurrently with*, not before, the sandboxed
|
|
// command starting (bwrap execs straight into it, with no hook point in
|
|
// between), so a container that used its interface immediately could
|
|
// otherwise race its own network setup; polling for it inside the
|
|
// container's own script, before signaling READY, avoids that.
|
|
class BackgroundPeer {
|
|
public:
|
|
BackgroundPeer(const std::filesystem::path& image, const std::vector<std::string>& networks,
|
|
const std::optional<std::string>& hostname, int lifetime_seconds) {
|
|
int pipe_fds[2];
|
|
if (pipe(pipe_fds) != 0) {
|
|
return;
|
|
}
|
|
pid_ = fork();
|
|
if (pid_ == 0) {
|
|
close(pipe_fds[0]);
|
|
dup2(pipe_fds[1], STDOUT_FILENO);
|
|
close(pipe_fds[1]);
|
|
|
|
ParsedArgs args;
|
|
args.mode = Mode::run;
|
|
args.mode_arg = image.string();
|
|
args.network_specs = networks;
|
|
args.hostname_flag = hostname;
|
|
args.command = {"sh", "-c",
|
|
fmt::format("for i in $(seq 1 20); do ip -4 addr show eth0 2>/dev/null | "
|
|
"grep -q 'inet ' && break; sleep 0.5; done; echo READY; sleep {}",
|
|
lifetime_seconds)};
|
|
AppConfig config = reload_config();
|
|
dispatch_command(args, "/nonexistent/unused-config.yaml", config);
|
|
_exit(0);
|
|
}
|
|
close(pipe_fds[1]);
|
|
read_fd_ = pipe_fds[0];
|
|
ready_ = wait_for_ready();
|
|
}
|
|
|
|
~BackgroundPeer() {
|
|
if (pid_ > 0) {
|
|
kill(pid_, SIGTERM);
|
|
int status = 0;
|
|
waitpid(pid_, &status, 0);
|
|
}
|
|
if (read_fd_ >= 0) {
|
|
close(read_fd_);
|
|
}
|
|
}
|
|
|
|
BackgroundPeer(const BackgroundPeer&) = delete;
|
|
BackgroundPeer& operator=(const BackgroundPeer&) = delete;
|
|
|
|
bool ready() const { return ready_; }
|
|
|
|
private:
|
|
// Bounded (15s, 200ms poll interval) wait for "READY" to appear on the
|
|
// child's own stdout -- covers join_networks()'s own ~3s namespace-
|
|
// isolation poll plus the child script's own up-to-10s eth0 poll above,
|
|
// with headroom. Plain poll()/read(), matching this project's existing
|
|
// direct-POSIX style elsewhere rather than <chrono>/<thread>.
|
|
bool wait_for_ready() {
|
|
std::string buf;
|
|
char chunk[256];
|
|
for (int waited_ms = 0; waited_ms < 15000; waited_ms += 200) {
|
|
struct pollfd pfd {
|
|
read_fd_, POLLIN, 0
|
|
};
|
|
int rc = poll(&pfd, 1, 200);
|
|
if (rc > 0 && (pfd.revents & POLLIN)) {
|
|
ssize_t n = read(read_fd_, chunk, sizeof(chunk));
|
|
if (n <= 0) {
|
|
break;
|
|
}
|
|
buf.append(chunk, static_cast<size_t>(n));
|
|
if (buf.find("READY") != std::string::npos) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pid_t pid_ = -1;
|
|
int read_fd_ = -1;
|
|
bool ready_ = false;
|
|
};
|
|
|
|
// Prefixes `command_after_eth0` with the same "wait for eth0 to actually be
|
|
// usable" poll BackgroundPeer's own script above uses, wrapped between the
|
|
// usual BEGIN/END-TEST-OUTPUT markers. Needed for *any* sandboxed command
|
|
// that uses its network interface at all, not just BackgroundPeer's own --
|
|
// join_networks() (network_join.cpp) runs concurrently with, not before,
|
|
// the sandboxed command starting (bwrap execs straight into it, no hook
|
|
// point in between), so a command that used eth0 immediately could
|
|
// otherwise race its own join. Confirmed by testing, not assumed, in two
|
|
// stages:
|
|
//
|
|
// 1. An earlier version of this file's own tests pinged immediately, and
|
|
// the container's own near-instant "ping, fail, exit" (no eth0 yet)
|
|
// sometimes raced ahead of join_one_network()'s own veth-move step,
|
|
// which then failed outright trying to move a veth into an
|
|
// already-exited container's pid ("Invalid netns value") -- the exact
|
|
// documented limitation network_join.{h,cpp}'s own CLAUDE.md entry
|
|
// already describes for a very-short-lived sandboxed command. Fixed by
|
|
// polling for the interface's own *existence* first (`ip link show
|
|
// eth0`).
|
|
//
|
|
// 2. That alone still wasn't enough: an interface can become visible (the
|
|
// device exists, already moved/created) *before* join_one_network()'s
|
|
// own later `ip addr add`/`ip link set ... up` steps for it have run.
|
|
// A script that only waited for existence could still start pinging
|
|
// (getting "Network unreachable", no address yet) and exit almost
|
|
// immediately -- and since this sandboxed process is the pid/net
|
|
// namespace's own sole occupant (`--unshare-pid`/`--unshare-net`), its
|
|
// exit destroys that namespace outright, which then made join_one_
|
|
// network()'s own *remaining* steps for that same network -- or, in one
|
|
// observed case, the entirely separate per-session DNS resolver's own
|
|
// nsenter call (network_dns.cpp's start_dns_resolver(), also entering
|
|
// this same namespace) -- fail with "No such file or directory" against
|
|
// a namespace that had already collapsed underneath them. Confirmed via
|
|
// a temporary production-code diagnostic that the DNS resolver's own
|
|
// namespace lookup was never itself stale (proc_exists was always true
|
|
// right up to its own nsenter call), narrowing the cause to the
|
|
// sandboxed script's own premature exit, not a namespace-resolution bug.
|
|
// Fixed by polling for an actually *assigned address* on eth0 (`ip -4
|
|
// addr show eth0 | grep -q 'inet '`) instead of mere existence -- this
|
|
// only becomes true once join_one_network()'s full sequence for that
|
|
// interface has completed, so the script no longer outruns its own join.
|
|
std::string wait_for_eth0_then(const std::string& command_after_eth0) {
|
|
return fmt::format(
|
|
"echo BEGIN-TEST-OUTPUT; for i in $(seq 1 20); do ip -4 addr show eth0 2>/dev/null | "
|
|
"grep -q 'inet ' && break; sleep 0.5; done; {}; echo END-TEST-OUTPUT",
|
|
command_after_eth0);
|
|
}
|
|
|
|
// Like wait_for_eth0_then(), but for a command that resolves `hostname`
|
|
// before using it: retries the whole ping-by-name probe (not just an eth0
|
|
// existence check) until it succeeds or the bound is hit, then runs the
|
|
// real, assertable command. A single probe covers two independent races
|
|
// against the same command starting concurrently with its own join
|
|
// (network_join.{h,cpp}'s own already-documented limitation): the
|
|
// interface itself not being up yet, and the per-session dnsmasq resolver
|
|
// (network_dns.cpp's start_dns_resolver(), started from the same
|
|
// on_bwrap_pid_known callback as the join itself) not having started, or
|
|
// not yet having picked up the peer's own hosts record, yet.
|
|
std::string wait_for_hostname_then(const std::string& hostname, const std::string& command_after) {
|
|
return fmt::format(
|
|
"echo BEGIN-TEST-OUTPUT; for i in $(seq 1 20); do ping -c 1 -W 1 {0} >/dev/null 2>&1 && break; "
|
|
"sleep 0.5; done; {1}; echo END-TEST-OUTPUT",
|
|
hostname, command_after);
|
|
}
|
|
|
|
// Runs `command` inside a fresh container joined to `networks`, in the
|
|
// foreground, returning its captured stdout -- the same dispatch_command()
|
|
// path BackgroundPeer's own forked child uses, just synchronous.
|
|
std::string run_networked(const std::filesystem::path& image, const std::vector<std::string>& networks,
|
|
const std::vector<std::string>& command) {
|
|
ParsedArgs args;
|
|
args.mode = Mode::run;
|
|
args.mode_arg = image.string();
|
|
args.network_specs = networks;
|
|
args.command = command;
|
|
AppConfig config = reload_config();
|
|
CapturedStdout capture;
|
|
dispatch_command(args, "/nonexistent/unused-config.yaml", config);
|
|
return capture.contents();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("network join: two intern peers can ping each other by IP (veth)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-intern-ping-veth", NetworkKind::intern, /*veth=*/true, "10.169.0.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
auto subnet = test_network_subnet(network.name());
|
|
REQUIRE(subnet.has_value());
|
|
auto peer_a_ip = ipv4_host_address(*subnet, 2);
|
|
REQUIRE(peer_a_ip.has_value());
|
|
// ipv4_host_address() includes the prefix length (e.g. "10.168.0.2/24")
|
|
// -- strip it for a plain ping target.
|
|
std::string peer_a_addr = peer_a_ip->substr(0, peer_a_ip->find('/'));
|
|
|
|
BackgroundPeer peer_a(*image, {network.name()}, std::nullopt, 20);
|
|
REQUIRE(peer_a.ready());
|
|
|
|
auto output = run_networked(
|
|
*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then(fmt::format("ping -c 2 -W 2 {}; echo RESULT=$?", peer_a_addr))});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: two extern peers can ping each other by IP (veth)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-extern-ping-veth", NetworkKind::extern_, /*veth=*/true, "10.169.1.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
auto subnet = test_network_subnet(network.name());
|
|
REQUIRE(subnet.has_value());
|
|
auto peer_a_ip = ipv4_host_address(*subnet, 2);
|
|
REQUIRE(peer_a_ip.has_value());
|
|
std::string peer_a_addr = peer_a_ip->substr(0, peer_a_ip->find('/'));
|
|
|
|
BackgroundPeer peer_a(*image, {network.name()}, std::nullopt, 20);
|
|
REQUIRE(peer_a.ready());
|
|
|
|
auto output = run_networked(
|
|
*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then(fmt::format("ping -c 2 -W 2 {}; echo RESULT=$?", peer_a_addr))});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: two extern peers can ping each other by IP (tap+relay)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-extern-ping-tap", NetworkKind::extern_, /*veth=*/false, "10.169.2.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
auto subnet = test_network_subnet(network.name());
|
|
REQUIRE(subnet.has_value());
|
|
auto peer_a_ip = ipv4_host_address(*subnet, 2);
|
|
REQUIRE(peer_a_ip.has_value());
|
|
std::string peer_a_addr = peer_a_ip->substr(0, peer_a_ip->find('/'));
|
|
|
|
BackgroundPeer peer_a(*image, {network.name()}, std::nullopt, 20);
|
|
REQUIRE(peer_a.ready());
|
|
|
|
auto output = run_networked(
|
|
*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then(fmt::format("ping -c 2 -W 2 {}; echo RESULT=$?", peer_a_addr))});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: extern network can reach the outside (veth)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-extern-outside-veth", NetworkKind::extern_, /*veth=*/true, "10.169.3.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
// 8.8.8.8 -- real internet access confirmed available in this sandbox
|
|
// elsewhere this session, and network_bridge.h's own extern-uplink
|
|
// verification already proved outside reachability at the host-side
|
|
// level; this confirms it end-to-end through a real container join.
|
|
auto output = run_networked(*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then("ping -c 2 -W 2 8.8.8.8; echo RESULT=$?")});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: extern network can reach the outside (tap+relay)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-extern-outside-tap", NetworkKind::extern_, /*veth=*/false, "10.169.4.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
auto output = run_networked(*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then("ping -c 2 -W 2 8.8.8.8; echo RESULT=$?")});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: intern network has no route to the outside (veth)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-intern-isolation-veth", NetworkKind::intern, /*veth=*/true, "10.169.5.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
// 8.8.8.8 is real, well-known and reachable outside this project's own
|
|
// sandbox (already confirmed by direct testing elsewhere this session,
|
|
// and by network_bridge.h's own extern-uplink verification) -- an
|
|
// intern network gets no default route at all (network_join.cpp), so
|
|
// this must fail, not merely time out slower than an extern join would.
|
|
auto output = run_networked(*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then("ping -c 2 -W 2 8.8.8.8; echo RESULT=$?")});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_result = false;
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line.rfind("RESULT=", 0) == 0) {
|
|
found_result = true;
|
|
found_success = (line == "RESULT=0");
|
|
}
|
|
}
|
|
CHECK(found_result);
|
|
CHECK_FALSE(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: intern network has no route to the outside (tap+relay)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-intern-isolation-tap", NetworkKind::intern, /*veth=*/false, "10.169.6.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
auto output = run_networked(*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then("ping -c 2 -W 2 8.8.8.8; echo RESULT=$?")});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_result = false;
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line.rfind("RESULT=", 0) == 0) {
|
|
found_result = true;
|
|
found_success = (line == "RESULT=0");
|
|
}
|
|
}
|
|
CHECK(found_result);
|
|
CHECK_FALSE(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: two intern peers can ping each other by IP (tap+relay)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-intern-ping-tap", NetworkKind::intern, /*veth=*/false, "10.169.7.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
auto subnet = test_network_subnet(network.name());
|
|
REQUIRE(subnet.has_value());
|
|
auto peer_a_ip = ipv4_host_address(*subnet, 2);
|
|
REQUIRE(peer_a_ip.has_value());
|
|
std::string peer_a_addr = peer_a_ip->substr(0, peer_a_ip->find('/'));
|
|
|
|
BackgroundPeer peer_a(*image, {network.name()}, std::nullopt, 20);
|
|
REQUIRE(peer_a.ready());
|
|
|
|
auto output = run_networked(
|
|
*image, {network.name()},
|
|
{"sh", "-c", wait_for_eth0_then(fmt::format("ping -c 2 -W 2 {}; echo RESULT=$?", peer_a_addr))});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: two peers can resolve and ping each other by hostname (veth)", "[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
if (!is_dnsmasq_available()) {
|
|
SKIP("dnsmasq not available");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-dns-ping-veth", NetworkKind::intern, /*veth=*/true, "10.169.8.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
BackgroundPeer peer_a(*image, {network.name()}, std::string("test-peer-a"), 20);
|
|
REQUIRE(peer_a.ready());
|
|
|
|
auto output = run_networked(
|
|
*image, {network.name()},
|
|
{"sh", "-c", wait_for_hostname_then("test-peer-a", "ping -c 2 -W 2 test-peer-a; echo RESULT=$?")});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|
|
|
|
TEST_CASE("network join: two peers can resolve and ping each other by hostname (tap+relay)",
|
|
"[integration][root][net]") {
|
|
if (geteuid() != 0) {
|
|
SKIP("requires root");
|
|
}
|
|
if (!is_dnsmasq_available()) {
|
|
SKIP("dnsmasq not available");
|
|
}
|
|
auto image = find_busybox_fixture();
|
|
if (!image) {
|
|
SKIP("no busybox fixture (images/busybox.tar) -- see tests/setup-tests.py");
|
|
}
|
|
|
|
ScratchXdgDirs scratch;
|
|
TestNetwork network("test-dns-ping-tap", NetworkKind::intern, /*veth=*/false, "10.169.9.0/24");
|
|
REQUIRE(network.created());
|
|
|
|
BackgroundPeer peer_a(*image, {network.name()}, std::string("test-peer-a"), 20);
|
|
REQUIRE(peer_a.ready());
|
|
|
|
auto output = run_networked(
|
|
*image, {network.name()},
|
|
{"sh", "-c", wait_for_hostname_then("test-peer-a", "ping -c 2 -W 2 test-peer-a; echo RESULT=$?")});
|
|
auto lines = extract_marked_lines(output);
|
|
bool found_success = false;
|
|
for (const auto& line : lines) {
|
|
if (line == "RESULT=0") {
|
|
found_success = true;
|
|
}
|
|
}
|
|
CHECK(found_success);
|
|
}
|