OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_phase6.cpp

475 lines · cpp ref:
// 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;
}