Files
slocker-lite/tests/integration/test_network_join_scenarios.cpp
ceamac 174b6b4c29 Use explicit test-only subnets and test- prefixed names in the test suite
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.
2026-09-05 14:03:14 +00:00

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);
}