// Phase 6 tests: Advanced NAT traversal — holepunch stage transitions,
// birthday attack socket allocation/winner, random punch cooldown, punch stats.
//
// Build: see build_tests.sh (test_phase6 target)
// Run: ./test_phase6
#include "compact_encoding.h"
#include "dht_connector.h"
#include "dht_messages.h"
#include "holepuncher.h"
#include "hyperdht.h"
#include "noise_ik.h"
#include <cstdio>
#include <cstring>
#include <sodium.h>
extern "C" {
#include <udx.h>
#include <uv.h>
}
// ---- Test harness ----
static int tests_run = 0;
static int tests_passed = 0;
static int tests_failed = 0;
static void check(bool cond, const char *name) {
tests_run++;
if (cond) {
tests_passed++;
printf(" PASS: %s\n", name);
} else {
tests_failed++;
printf(" FAIL: %s\n", name);
}
}
// ---- Helper: create a UDX socket on a loop ----
struct TestEnv {
uv_loop_t loop;
udx_t udx;
udx_socket_t sock;
};
static void init_env(TestEnv &env) {
uv_loop_init(&env.loop);
udx_init(&env.loop, &env.udx, nullptr);
udx_socket_init(&env.udx, &env.sock, nullptr);
struct sockaddr_in bind_addr;
uv_ip4_addr("127.0.0.1", 0, &bind_addr);
udx_socket_bind(&env.sock, (struct sockaddr *)&bind_addr, 0);
}
static void cleanup_env(TestEnv &env) {
udx_socket_close(&env.sock);
uv_run(&env.loop, UV_RUN_NOWAIT);
uv_loop_close(&env.loop);
}
// ---- Test: stage transitions ----
static void test_stage_transitions() {
printf("\n--- test_stage_transitions ---\n");
TestEnv env;
init_env(env);
Holepuncher puncher;
check(puncher.stage() == HP_IDLE, "initial stage is HP_IDLE");
// Init with consistent local firewall.
puncher.init(&env.sock, FIREWALL_CONSISTENT);
// Set remote info to consistent.
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_CONSISTENT;
remote.addresses[0] = {{127, 0, 0, 1}, 5000};
remote.address_count = 1;
remote.has_verified = true;
remote.verified_address = remote.addresses[0];
puncher.update_remote(remote);
// Start — should go directly to HP_PUNCH_CONSISTENT (both firewalls known).
puncher.start();
check(puncher.stage() == HP_PUNCH_CONSISTENT,
"both CONSISTENT → HP_PUNCH_CONSISTENT");
check(puncher.is_punching(), "punching after start");
// Simulate probe received — should transition to HP_CONNECTED.
IPv4Address from = {{127, 0, 0, 1}, 5000};
puncher.on_probe_received(from);
check(puncher.stage() == HP_CONNECTED, "probe received → HP_CONNECTED");
check(puncher.is_connected(), "connected after probe received");
check(!puncher.is_punching(), "not punching after connected");
cleanup_env(env);
}
// ---- Test: stage with unknown firewall → analyze ----
static void test_analyze_stage() {
printf("\n--- test_analyze_stage ---\n");
TestEnv env;
init_env(env);
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_UNKNOWN); // Unknown local.
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_CONSISTENT;
remote.addresses[0] = {{127, 0, 0, 1}, 5000};
remote.address_count = 1;
puncher.update_remote(remote);
puncher.start();
check(puncher.stage() == HP_ANALYZE,
"unknown local firewall → HP_ANALYZE");
// Tick without resolving firewall — should stay in analyze.
puncher.tick(1000);
check(puncher.stage() == HP_ANALYZE, "still analyzing");
// Timeout after 5 seconds — should fall back to consistent.
puncher.tick(1000 + HP_ANALYZE_TIMEOUT_MS + 1);
check(puncher.stage() == HP_PUNCH_CONSISTENT,
"analyze timeout → HP_PUNCH_CONSISTENT fallback");
cleanup_env(env);
}
// ---- Test: strategy decision for random NAT ----
static void test_strategy_random_vs_consistent() {
printf("\n--- test_strategy_random_vs_consistent ---\n");
TestEnv env;
init_env(env);
// Test 1: Local CONSISTENT → Remote RANDOM → HP_PUNCH_RANDOM.
{
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_CONSISTENT);
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_RANDOM;
remote.addresses[0] = {{10, 0, 0, 1}, 5000};
remote.address_count = 1;
remote.has_verified = true;
remote.verified_address = remote.addresses[0];
puncher.update_remote(remote);
puncher.start();
check(puncher.stage() == HP_PUNCH_RANDOM,
"CONSISTENT→RANDOM → HP_PUNCH_RANDOM");
}
// Test 2: Local RANDOM → Remote CONSISTENT → HP_PUNCH_BIRTHDAY.
{
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_RANDOM);
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_CONSISTENT;
remote.addresses[0] = {{10, 0, 0, 1}, 5000};
remote.address_count = 1;
remote.has_verified = true;
remote.verified_address = remote.addresses[0];
puncher.update_remote(remote);
puncher.start();
check(puncher.stage() == HP_PUNCH_BIRTHDAY,
"RANDOM→CONSISTENT → HP_PUNCH_BIRTHDAY");
// Stop to clean up birthday sockets.
puncher.stop();
}
// Run the loop to process close callbacks for birthday sockets.
uv_run(&env.loop, UV_RUN_NOWAIT);
uv_run(&env.loop, UV_RUN_NOWAIT);
// Test 3: Both RANDOM → HP_FAILED (impossible to punch).
{
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_RANDOM);
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_RANDOM;
remote.addresses[0] = {{10, 0, 0, 1}, 5000};
remote.address_count = 1;
puncher.update_remote(remote);
bool abort_called = false;
puncher.on_abort = [&]() { abort_called = true; };
puncher.start();
check(puncher.stage() == HP_FAILED,
"both RANDOM → HP_FAILED");
check(abort_called, "abort callback fired for double random");
}
cleanup_env(env);
}
// ---- Test: birthday socket allocation ----
static void test_birthday_socket_alloc() {
printf("\n--- test_birthday_socket_alloc ---\n");
TestEnv env;
init_env(env);
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_RANDOM);
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_CONSISTENT;
remote.addresses[0] = {{127, 0, 0, 1}, 5000};
remote.address_count = 1;
remote.has_verified = true;
remote.verified_address = remote.addresses[0];
puncher.update_remote(remote);
puncher.start();
check(puncher.stage() == HP_PUNCH_BIRTHDAY,
"birthday attack started");
check(puncher.is_punching(), "punching in birthday mode");
// Stop and cleanup.
puncher.stop();
// Run loop to process close callbacks.
for (int i = 0; i < 5; i++) {
uv_run(&env.loop, UV_RUN_NOWAIT);
}
check(!puncher.is_punching(), "not punching after stop");
check(puncher.stage() == HP_FAILED, "stage is FAILED after stop");
cleanup_env(env);
}
// ---- Test: birthday winner ----
static void test_birthday_winner() {
printf("\n--- test_birthday_winner ---\n");
TestEnv env;
init_env(env);
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_RANDOM);
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_CONSISTENT;
remote.addresses[0] = {{127, 0, 0, 1}, 5000};
remote.address_count = 1;
remote.has_verified = true;
remote.verified_address = remote.addresses[0];
puncher.update_remote(remote);
bool connect_called = false;
IPv4Address connected_addr = {};
puncher.on_connect = [&](const IPv4Address &addr) {
connect_called = true;
connected_addr = addr;
};
puncher.start();
check(puncher.stage() == HP_PUNCH_BIRTHDAY, "birthday started");
// Simulate a probe arriving on the main socket (fallback path).
// In practice, birthday probes arrive on birthday sockets via the
// _on_birthday_recv callback. But on_probe_received also works.
//
// ⛔ FROM THE REMOTE'S IP, ON A DIFFERENT PORT. This used to arrive from
// 10.0.0.1 while the remote advertised 127.0.0.1, so the _is_remote_ip guard
// correctly ignored it and four assertions failed — a stale gate, the same
// one as test_phase4 (docs/PARITY-hyperswarm.md 8.3). The differing PORT is
// deliberate and is the case that matters: the guard compares the IP only,
// because a NAT rewriting the source port is the entire reason we punch.
IPv4Address from = {{127, 0, 0, 1}, 6000};
puncher.on_probe_received(from);
check(connect_called, "on_connect fired");
check(puncher.is_connected(), "connected after probe");
check(puncher.stage() == HP_CONNECTED, "stage HP_CONNECTED");
check(connected_addr.port == 6000, "connected address correct");
// No winning birthday socket for main-socket probe.
udx_socket_t *winner = puncher.take_winning_socket();
check(winner == nullptr, "no birthday winner (probe came on main socket)");
// Run loop to process close callbacks.
for (int i = 0; i < 5; i++) {
uv_run(&env.loop, UV_RUN_NOWAIT);
}
cleanup_env(env);
}
// ---- Test: consistent probe timeout ----
static void test_consistent_probe_timeout() {
printf("\n--- test_consistent_probe_timeout ---\n");
TestEnv env;
init_env(env);
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_CONSISTENT);
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_CONSISTENT;
remote.addresses[0] = {{127, 0, 0, 1}, 5000};
remote.address_count = 1;
remote.has_verified = true;
remote.verified_address = remote.addresses[0];
puncher.update_remote(remote);
bool abort_called = false;
puncher.on_abort = [&]() { abort_called = true; };
puncher.start();
// Tick through all 10 rounds without receiving a probe.
uint64_t now = 1000;
for (int i = 0; i <= HP_CONSISTENT_PROBE_MAX_ROUNDS; i++) {
puncher.tick(now);
now += HP_CONSISTENT_PROBE_INTERVAL_MS;
}
check(abort_called, "abort called after max rounds");
check(puncher.stage() == HP_FAILED, "stage HP_FAILED after timeout");
check(!puncher.is_punching(), "not punching after timeout");
cleanup_env(env);
}
// ---- Test: punch stats ----
static void test_punch_stats() {
printf("\n--- test_punch_stats ---\n");
// Verify PunchStats struct initializes to zero.
PunchStats stats;
memset(&stats, 0, sizeof(stats));
check(stats.total == 0, "initial total == 0");
check(stats.consistent_ok == 0, "initial consistent_ok == 0");
check(stats.random_ok == 0, "initial random_ok == 0");
check(stats.open_ok == 0, "initial open_ok == 0");
check(stats.timeouts == 0, "initial timeouts == 0");
// Simulate incrementing (these are set by DhtConnector during connection lifecycle).
stats.total = 5;
stats.consistent_ok = 3;
stats.random_ok = 1;
stats.open_ok = 1;
stats.timeouts = 2;
check(stats.total == 5, "total after updates");
check(stats.consistent_ok == 3, "consistent after updates");
check(stats.random_ok + stats.open_ok + stats.consistent_ok == 5,
"sum of ok == total");
}
// ---- Test: random punch cooldown constants ----
static void test_random_cooldown_constants() {
printf("\n--- test_random_cooldown_constants ---\n");
// Verify constants match JS DEFAULTS.
check(RANDOM_PUNCH_COOLDOWN_MS == 20000,
"cooldown == 20s (matching JS randomPunchInterval)");
check(RANDOM_PUNCH_LIMIT == 1,
"limit == 1 (matching JS _randomPunchLimit)");
// Birthday attack constants.
check(HP_BIRTHDAY_SOCKET_COUNT == 256,
"birthday socket count == 256 (matching JS BIRTHDAY_SOCKETS)");
check(HP_RANDOM_PROBE_INTERVAL_MS == 20,
"random probe interval == 20ms");
check(HP_RANDOM_PROBE_MAX_ITERATIONS == 1750,
"max iterations == 1750 (~35 seconds)");
check(HP_TTL_LOW == 5,
"low TTL == 5 (matching JS HOLEPUNCH_TTL)");
check(HP_TTL_NORMAL == 64,
"normal TTL == 64 (matching JS DEFAULT_TTL)");
}
// ---- Test: random probe iteration ----
static void test_random_probe_iteration() {
printf("\n--- test_random_probe_iteration ---\n");
TestEnv env;
init_env(env);
Holepuncher puncher;
puncher.init(&env.sock, FIREWALL_CONSISTENT);
RemotePeerInfo remote;
memset(&remote, 0, sizeof(remote));
remote.firewall = FIREWALL_RANDOM;
remote.addresses[0] = {{10, 0, 0, 1}, 5000};
remote.address_count = 1;
remote.has_verified = true;
remote.verified_address = remote.addresses[0];
puncher.update_remote(remote);
bool abort_called = false;
puncher.on_abort = [&]() { abort_called = true; };
puncher.start();
check(puncher.stage() == HP_PUNCH_RANDOM, "random probe started");
// Run through all 1750 iterations at 20ms intervals.
uint64_t now = 1000;
for (int i = 0; i <= HP_RANDOM_PROBE_MAX_ITERATIONS; i++) {
if (!puncher.is_punching()) break;
puncher.tick(now);
now += HP_RANDOM_PROBE_INTERVAL_MS;
}
check(abort_called, "abort called after max random iterations");
check(puncher.stage() == HP_FAILED, "stage HP_FAILED after random timeout");
cleanup_env(env);
}
// ---- Main ----
int main() {
if (sodium_init() < 0) {
fprintf(stderr, "sodium_init failed\n");
return 1;
}
printf("=== Phase 6 Tests: Advanced NAT + Birthday Attack ===\n");
// Stage transition tests.
test_stage_transitions();
test_analyze_stage();
test_strategy_random_vs_consistent();
// Birthday attack tests.
test_birthday_socket_alloc();
test_birthday_winner();
// Probe timeout tests.
test_consistent_probe_timeout();
test_random_probe_iteration();
// Stats and constants tests.
test_punch_stats();
test_random_cooldown_constants();
printf("\n=== Summary: %d/%d passed ===\n", tests_passed, tests_run);
return tests_failed > 0 ? 1 : 0;
}