Files
slocker-lite/tests/integration/test_network_join_scenarios.cpp
T
ceamac 7c33375d4d Fix flaky network-join tests: wait for an assigned address, not just link existence
The tap+relay intern-ping test and the DNS hostname-ping test both
flaked intermittently (roughly 1 in 13-20 runs), always in a "found_success
== false" shape with no assertion-level clue as to why.

Root cause, confirmed via a temporary production-code diagnostic (since
reverted) and cross-referenced against network_join.cpp's own code: the
readiness poll only waited for eth0 to *exist* (`ip link show eth0`), but
an interface can become visible before join_one_network()'s own later
`ip addr add`/`ip link set ... up` steps for it have actually run. A
script that used the interface as soon as it merely existed could ping,
get "Network unreachable" (no address yet), and exit almost immediately
-- and since a session's own sandboxed process is the sole occupant of
its pid/net namespace (--unshare-pid/--unshare-net), its exit destroys
that namespace outright. That, in turn, made whichever other nsenter
call was still in flight against the same namespace -- join_one_network()'s
own remaining steps, or the entirely separate per-session DNS resolver
(network_dns.cpp's start_dns_resolver(), which enters every networked
session's namespace regardless of whether --hostname was given) -- fail
with "No such file or directory" against a namespace that had already
collapsed underneath it.

This is the same general class of race network_join.{h,cpp}'s own
CLAUDE.md entry already documents (a very-short-lived sandboxed command
can outrun its own concurrent network setup), just one step further than
the eth0-existence race already fixed earlier in this file -- a test-code
issue, not a production bug. Fixed by polling for an actually assigned
address on eth0 instead of mere existence, in both wait_for_eth0_then()
and BackgroundPeer's own inline readiness script.

Verified with the diagnostic in place that the DNS resolver's own
namespace lookup was never itself stale, isolating the cause to the
script's own premature exit. Re-verified extensively after the fix:
8/8 isolated repeats of the previously-flaky tap+relay test, and 6
consecutive full [integration][root][net] suite runs (78 test-case
executions total) with no failures.
2026-09-05 12:00:29 +00:00

633 lines
25 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.
#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). Returns true on success.
bool create_test_network(const std::string& name, NetworkKind kind, bool veth) {
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;
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) : name_(std::move(name)) {
created_ = create_test_network(name_, kind, veth);
}
~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("selftest-intern-ping-veth", NetworkKind::intern, /*veth=*/true);
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("selftest-extern-ping-veth", NetworkKind::extern_, /*veth=*/true);
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("selftest-extern-ping-tap", NetworkKind::extern_, /*veth=*/false);
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("selftest-extern-outside-veth", NetworkKind::extern_, /*veth=*/true);
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("selftest-extern-outside-tap", NetworkKind::extern_, /*veth=*/false);
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("selftest-intern-isolation-veth", NetworkKind::intern, /*veth=*/true);
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("selftest-intern-isolation-tap", NetworkKind::intern, /*veth=*/false);
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("selftest-intern-ping-tap", NetworkKind::intern, /*veth=*/false);
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("selftest-dns-ping-veth", NetworkKind::intern, /*veth=*/true);
REQUIRE(network.created());
BackgroundPeer peer_a(*image, {network.name()}, std::string("peer-a"), 20);
REQUIRE(peer_a.ready());
auto output = run_networked(
*image, {network.name()},
{"sh", "-c", wait_for_hostname_then("peer-a", "ping -c 2 -W 2 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("selftest-dns-ping-tap", NetworkKind::intern, /*veth=*/false);
REQUIRE(network.created());
BackgroundPeer peer_a(*image, {network.name()}, std::string("peer-a"), 20);
REQUIRE(peer_a.ready());
auto output = run_networked(
*image, {network.name()},
{"sh", "-c", wait_for_hostname_then("peer-a", "ping -c 2 -W 2 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);
}