OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_phase4.cpp

513 lines · cpp ref:
// Phase 4 tests: Holepuncher state machine, DhtConnector states, SecurePayload round-trips.
// Build:
//   cd /storage/projects/code/godot/hyperswarm-peer/test
//   g++ -std=c++17 -I../src -I../thirdparty/libudx/include \
//       test_phase4.cpp ../src/noise_ik.cpp ../src/holepuncher.cpp \
//       ../src/dht_rpc.cpp ../src/hyperdht.cpp ../src/dht_connector.cpp \
//       ../thirdparty/libudx/src/udx.c ../thirdparty/libudx/src/udx_bbr.c \
//       ../thirdparty/libudx/src/udx_rate.c ../thirdparty/libudx/src/cirbuf.c \
//       ../thirdparty/libudx/src/endian.c ../thirdparty/libudx/src/io_posix.c \
//       ../thirdparty/libudx/src/queue.c ../thirdparty/libudx/src/win_filter.c \
//       ../thirdparty/libudx/src/win_filter_f64.c \
//       -luv -lsodium -lpthread -o test_phase4
//
// Run: ./test_phase4

#include "compact_encoding.h"
#include "dht_connector.h"
#include "dht_messages.h"
#include "holepuncher.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);
	}
}

// ---- Holepuncher tests ----

static void test_holepuncher_init() {
	printf("\n--- Holepuncher Init ---\n");

	Holepuncher hp;
	check(!hp.is_punching(), "Not punching initially");
	check(!hp.is_connected(), "Not connected initially");
}

static void test_holepuncher_start_stop() {
	printf("\n--- Holepuncher Start/Stop ---\n");

	Holepuncher hp;
	// Can't use init with a real socket, but we can test state transitions.

	hp.start();
	check(hp.is_punching(), "Punching after start");
	check(!hp.is_connected(), "Not connected after start");

	bool abort_fired = false;
	hp.on_abort = [&abort_fired]() { abort_fired = true; };

	hp.stop();
	check(!hp.is_punching(), "Not punching after stop");
	check(abort_fired, "Abort callback fired on stop");
}

// The guard itself, which had no test at all -- the stale gate above was the
// only thing exercising it, and it was exercising it BY ACCIDENT while
// asserting the opposite. A probe from an IP the remote never advertised must
// not be mistaken for a punch.
static void test_holepuncher_ignores_unknown_source() {
	printf("\n--- Holepuncher Ignores Unknown Source ---\n");

	Holepuncher hp;
	hp.start();

	bool connect_fired = false;
	hp.on_connect = [&](const IPv4Address &) { connect_fired = true; };

	IPv4Address known;
	known.ip[0] = 10; known.ip[1] = 0; known.ip[2] = 0; known.ip[3] = 1;
	known.port = 12345;

	RemotePeerInfo remote;
	memset(&remote, 0, sizeof(remote));
	remote.address_count = 1;
	remote.addresses[0] = known;
	hp.update_remote(remote);

	// Same port, different host -- a local process, not the peer.
	IPv4Address stranger = known;
	stranger.ip[0] = 127; stranger.ip[1] = 0; stranger.ip[2] = 0; stranger.ip[3] = 1;

	hp.on_probe_received(stranger);

	check(!connect_fired, "Connect NOT fired for a probe from an unknown source");
	check(!hp.is_connected(), "Still not connected after an unknown-source probe");
	check(hp.is_punching(), "Still punching after an unknown-source probe");

	// And the known source still works, so the guard is not simply refusing all.
	hp.on_probe_received(known);
	check(connect_fired, "Connect fired for a probe from the known remote");
}

static void test_holepuncher_probe_received() {
	printf("\n--- Holepuncher Probe Received ---\n");

	Holepuncher hp;
	hp.start();

	IPv4Address connected_addr;
	bool connect_fired = false;
	hp.on_connect = [&](const IPv4Address &addr) {
		connected_addr = addr;
		connect_fired = true;
	};

	// Simulate receiving a probe.
	IPv4Address from;
	from.ip[0] = 10; from.ip[1] = 0; from.ip[2] = 0; from.ip[3] = 1;
	from.port = 12345;

	// ⛔ THE PEER MUST BE KNOWN FIRST, and this gate did not say so.
	//
	// on_probe_received() ignores a probe whose source IP is not one of the
	// remote's advertised addresses -- otherwise any local process sending UDP
	// at us (the keet daemon did) is accepted as a successful punch. That guard
	// is deliberate. This test predated it, never configured a remote, and so
	// asserted the PRE-GUARD behaviour: five failures that looked like a broken
	// holepuncher and were a stale gate. See docs/PARITY-hyperswarm.md 8.3.
	RemotePeerInfo remote;
	memset(&remote, 0, sizeof(remote));
	remote.address_count = 1;
	remote.addresses[0] = from;
	hp.update_remote(remote);

	hp.on_probe_received(from);

	check(connect_fired, "Connect callback fired on probe received");
	check(hp.is_connected(), "Connected after probe received");
	check(!hp.is_punching(), "Not punching after connected");
	check(connected_addr.ip[0] == 10, "Connected address IP correct");
	check(connected_addr.port == 12345, "Connected address port correct");
}

static void test_holepuncher_timeout() {
	printf("\n--- Holepuncher Timeout ---\n");

	Holepuncher hp;
	// Init with known firewalls to skip HP_ANALYZE phase.
	hp.init(nullptr, FIREWALL_CONSISTENT);
	RemotePeerInfo rinfo;
	memset(&rinfo, 0, sizeof(rinfo));
	rinfo.firewall = FIREWALL_CONSISTENT;
	hp.update_remote(rinfo);

	bool abort_fired = false;
	hp.on_abort = [&abort_fired]() { abort_fired = true; };

	hp.start();

	// Tick with advancing time — should timeout after HP_CONSISTENT_PROBE_MAX_ROUNDS rounds.
	// Each round takes HP_CONSISTENT_PROBE_INTERVAL_MS (1000ms), max 10 rounds.
	uint64_t now = 1000;
	for (int i = 0; i < 15; i++) {
		now += HP_CONSISTENT_PROBE_INTERVAL_MS;
		bool still_punching = hp.tick(now);
		if (!still_punching) break;
	}

	check(abort_fired, "Abort fired after timeout");
	check(!hp.is_punching(), "Not punching after timeout");
	check(!hp.is_connected(), "Not connected after timeout (no probe received)");
}

// ---- SecurePayload tests ----

static void test_secure_payload_roundtrip() {
	printf("\n--- SecurePayload Round-Trip ---\n");

	uint8_t key[32];
	randombytes_buf(key, 32);

	// Build a holepunch payload.
	dht_msg::HolepunchPayload payload;
	memset(&payload, 0, sizeof(payload));
	payload.firewall = FIREWALL_CONSISTENT;
	payload.round = 3;
	payload.punching = true;
	payload.addresses_count = 1;
	payload.addresses[0].ip[0] = 192;
	payload.addresses[0].ip[1] = 168;
	payload.addresses[0].ip[2] = 1;
	payload.addresses[0].ip[3] = 100;
	payload.addresses[0].port = 5000;

	// Encode.
	EncodeState ps = { 0, 0, nullptr };
	dht_msg::holepunch_payload_preencode(ps, payload);
	std::vector<uint8_t> pt(ps.end);
	ps.buffer = pt.data();
	ps.start = 0;
	dht_msg::holepunch_payload_encode(ps, payload);

	check(pt.size() > 0, "Payload encoded (non-empty)");

	// Encrypt.
	auto encrypted = dht_msg::secure_payload_encrypt(pt.data(), pt.size(), key);
	check(encrypted.size() > pt.size(), "Encrypted is larger than plaintext (nonce + tag)");

	// Decrypt.
	std::vector<uint8_t> decrypted;
	bool ok = dht_msg::secure_payload_decrypt(encrypted.data(), encrypted.size(), key, decrypted);
	check(ok, "Decryption succeeded");
	check(decrypted.size() == pt.size(), "Decrypted size matches original");
	check(memcmp(decrypted.data(), pt.data(), pt.size()) == 0, "Decrypted bytes match original");

	// Decode the decrypted payload.
	EncodeState ds = { 0, decrypted.size(), decrypted.data() };
	dht_msg::HolepunchPayload decoded;
	bool decode_ok = dht_msg::holepunch_payload_decode(ds, decoded);
	check(decode_ok, "Decoded decrypted payload");
	check(decoded.firewall == FIREWALL_CONSISTENT, "Firewall roundtrip");
	check(decoded.round == 3, "Round roundtrip");
	check(decoded.punching == true, "Punching flag roundtrip");
	check(decoded.addresses_count == 1, "Address count roundtrip");
	check(decoded.addresses[0].port == 5000, "Address port roundtrip");
}

static void test_secure_payload_wrong_key() {
	printf("\n--- SecurePayload Wrong Key ---\n");

	uint8_t key1[32], key2[32];
	randombytes_buf(key1, 32);
	randombytes_buf(key2, 32);

	uint8_t data[] = "secret message";
	auto encrypted = dht_msg::secure_payload_encrypt(data, sizeof(data), key1);

	std::vector<uint8_t> decrypted;
	bool ok = dht_msg::secure_payload_decrypt(encrypted.data(), encrypted.size(), key2, decrypted);
	check(!ok, "Decryption with wrong key fails");
}

static void test_secure_payload_tampered() {
	printf("\n--- SecurePayload Tampered ---\n");

	uint8_t key[32];
	randombytes_buf(key, 32);

	uint8_t data[] = "intact data";
	auto encrypted = dht_msg::secure_payload_encrypt(data, sizeof(data), key);

	// Tamper with a byte in the ciphertext.
	if (encrypted.size() > 30) {
		encrypted[30] ^= 0xFF;
	}

	std::vector<uint8_t> decrypted;
	bool ok = dht_msg::secure_payload_decrypt(encrypted.data(), encrypted.size(), key, decrypted);
	check(!ok, "Tampered ciphertext fails decryption");
}

// ---- NoisePayload encode/decode round-trip ----

static void test_noise_payload_roundtrip() {
	printf("\n--- NoisePayload Full Round-Trip ---\n");

	dht_msg::NoisePayload np;
	memset(&np, 0, sizeof(np));
	np.version = 1;
	np.error = 0;
	np.firewall = FIREWALL_CONSISTENT;
	np.addresses4_count = 2;
	np.addresses4[0].ip[0] = 10; np.addresses4[0].ip[1] = 0;
	np.addresses4[0].ip[2] = 0; np.addresses4[0].ip[3] = 1;
	np.addresses4[0].port = 9000;
	np.addresses4[1].ip[0] = 192; np.addresses4[1].ip[1] = 168;
	np.addresses4[1].ip[2] = 1; np.addresses4[1].ip[3] = 50;
	np.addresses4[1].port = 9001;
	np.has_udx = true;
	np.udx.reusable_socket = true;
	np.udx.id = 42;
	np.udx.seq = 100;
	np.has_secret_stream = true;

	// Encode.
	EncodeState ps = { 0, 0, nullptr };
	dht_msg::noise_payload_preencode(ps, np);
	std::vector<uint8_t> buf(ps.end);
	ps.buffer = buf.data();
	ps.start = 0;
	dht_msg::noise_payload_encode(ps, np);

	check(buf.size() > 0, "NoisePayload encoded");

	// Decode.
	EncodeState ds = { 0, buf.size(), buf.data() };
	dht_msg::NoisePayload decoded;
	bool ok = dht_msg::noise_payload_decode(ds, decoded);
	check(ok, "NoisePayload decoded");
	check(decoded.version == 1, "version roundtrip");
	check(decoded.firewall == FIREWALL_CONSISTENT, "firewall roundtrip");
	check(decoded.addresses4_count == 2, "addresses count roundtrip");
	check(decoded.addresses4[0].port == 9000, "address[0] port roundtrip");
	check(decoded.addresses4[1].port == 9001, "address[1] port roundtrip");
	check(decoded.has_udx, "has_udx roundtrip");
	check(decoded.udx.reusable_socket, "udx.reusable_socket roundtrip");
	check(decoded.udx.id == 42, "udx.id roundtrip");
	check(decoded.udx.seq == 100, "udx.seq roundtrip");
	check(decoded.has_secret_stream, "has_secret_stream roundtrip");
}

// ---- HolepunchMessage encode/decode round-trip ----

static void test_holepunch_message_roundtrip() {
	printf("\n--- HolepunchMessage Round-Trip ---\n");

	uint8_t payload_data[] = { 0xDE, 0xAD, 0xBE, 0xEF };

	dht_msg::HolepunchMessage msg;
	memset(&msg, 0, sizeof(msg));
	msg.mode = 1;
	msg.id = 7;
	msg.payload = payload_data;
	msg.payload_len = 4;

	// Encode.
	EncodeState es = { 0, 0, nullptr };
	dht_msg::holepunch_msg_preencode(es, msg);
	std::vector<uint8_t> buf(es.end);
	es.buffer = buf.data();
	es.start = 0;
	dht_msg::holepunch_msg_encode(es, msg);

	check(buf.size() > 0, "HolepunchMessage encoded");

	// Decode.
	EncodeState ds = { 0, buf.size(), buf.data() };
	dht_msg::HolepunchMessage decoded;
	bool ok = dht_msg::holepunch_msg_decode(ds, decoded);
	check(ok, "HolepunchMessage decoded");
	check(decoded.mode == 1, "mode roundtrip");
	check(decoded.id == 7, "id roundtrip");
	check(decoded.payload_len == 4, "payload_len roundtrip");
	check(memcmp(decoded.payload, payload_data, 4) == 0, "payload roundtrip");
}

// ---- DhtConnector allocation tests ----

static void test_dht_connector_alloc() {
	printf("\n--- DhtConnector Allocation ---\n");

	DhtConnector connector;
	// Can't call init() without a real DHT, but we can test that
	// the constructor doesn't crash and basic state is correct.
	check(!connector.is_listening(), "Not listening initially");
}

// ---- Noise IK key derivation consistency ----

static void test_noise_ik_key_consistency() {
	printf("\n--- Noise IK Key Derivation Consistency ---\n");

	sodium_init();

	Ed25519KeyPair kp_i, kp_r;
	crypto_sign_keypair(kp_i.public_key, kp_i.secret_key);
	crypto_sign_keypair(kp_r.public_key, kp_r.secret_key);

	// Compute a namespace for peer handshake.
	const uint8_t ids[] = { 0 }; // PEER_HANDSHAKE
	uint8_t ns[32];
	NoiseIK::compute_namespace("hyperswarm/dht", ids, 1, ns);

	// Initiator handshake.
	NoiseIK initiator(true, kp_i, ns, kp_r.public_key);
	NoiseIK responder(false, kp_r, ns, nullptr);

	// Initiator sends Message 1.
	uint8_t payload1[] = "hello from initiator";
	auto msg1 = initiator.send(payload1, sizeof(payload1));
	check(msg1.size() > 0, "Initiator msg1 produced");

	// Responder receives Message 1.
	auto pt1 = responder.recv(msg1.data(), msg1.size());
	check(pt1.size() == sizeof(payload1), "Responder decrypted msg1");
	check(memcmp(pt1.data(), payload1, sizeof(payload1)) == 0, "msg1 payload correct");

	// Responder sends Message 2.
	uint8_t payload2[] = "hello from responder";
	auto msg2 = responder.send(payload2, sizeof(payload2));
	check(msg2.size() > 0, "Responder msg2 produced");
	check(responder.is_complete(), "Responder handshake complete");

	// Initiator receives Message 2.
	auto pt2 = initiator.recv(msg2.data(), msg2.size());
	check(pt2.size() == sizeof(payload2), "Initiator decrypted msg2");
	check(memcmp(pt2.data(), payload2, sizeof(payload2)) == 0, "msg2 payload correct");
	check(initiator.is_complete(), "Initiator handshake complete");

	// Verify key consistency: initiator's TX = responder's RX and vice versa.
	check(memcmp(initiator.tx_key(), responder.rx_key(), 32) == 0,
		  "Initiator TX == Responder RX");
	check(memcmp(initiator.rx_key(), responder.tx_key(), 32) == 0,
		  "Initiator RX == Responder TX");

	// Verify remote public key authentication.
	check(memcmp(initiator.remote_public_key(), kp_r.public_key, 32) == 0,
		  "Initiator sees responder's public key");
	check(memcmp(responder.remote_public_key(), kp_i.public_key, 32) == 0,
		  "Responder sees initiator's public key");

	// Derive holepunch secrets — should match on both sides.
	uint8_t hp_secret_i[32], hp_secret_r[32];
	uint8_t hp_ns[32];
	const uint8_t hp_ids[] = { 1 }; // PEER_HOLEPUNCH
	NoiseIK::compute_namespace("hyperswarm/dht", hp_ids, 1, hp_ns);

	initiator.derive_holepunch_secret(hp_ns, hp_secret_i);
	responder.derive_holepunch_secret(hp_ns, hp_secret_r);
	check(memcmp(hp_secret_i, hp_secret_r, 32) == 0,
		  "Holepunch secrets match");

	// Test that the holepunch secret can encrypt/decrypt.
	uint8_t test_data[] = "holepunch test payload";
	auto encrypted = dht_msg::secure_payload_encrypt(test_data, sizeof(test_data), hp_secret_i);
	std::vector<uint8_t> decrypted;
	bool ok = dht_msg::secure_payload_decrypt(encrypted.data(), encrypted.size(), hp_secret_r, decrypted);
	check(ok, "SecurePayload with IK-derived secret works");
	check(memcmp(decrypted.data(), test_data, sizeof(test_data)) == 0,
		  "SecurePayload content matches");
}

// ---- RemotePeerInfo / Holepuncher update ----

static void test_holepuncher_remote_update() {
	printf("\n--- Holepuncher Remote Update ---\n");

	Holepuncher hp;
	hp.start();

	RemotePeerInfo rinfo;
	memset(&rinfo, 0, sizeof(rinfo));
	rinfo.firewall = FIREWALL_OPEN;
	rinfo.address_count = 2;
	rinfo.addresses[0].ip[0] = 10; rinfo.addresses[0].ip[1] = 0;
	rinfo.addresses[0].ip[2] = 0; rinfo.addresses[0].ip[3] = 1;
	rinfo.addresses[0].port = 8000;
	rinfo.addresses[1].ip[0] = 192; rinfo.addresses[1].ip[1] = 168;
	rinfo.addresses[1].ip[2] = 0; rinfo.addresses[1].ip[3] = 1;
	rinfo.addresses[1].port = 8001;
	rinfo.has_verified = true;
	rinfo.verified_address = rinfo.addresses[0];

	hp.update_remote(rinfo);
	check(true, "Remote update doesn't crash");

	// Update again with different info.
	rinfo.firewall = FIREWALL_CONSISTENT;
	rinfo.address_count = 1;
	hp.update_remote(rinfo);
	check(true, "Second remote update doesn't crash");
}

// ---- Main ----

int main() {
	printf("=== Phase 4 Tests ===\n");
	printf("Holepuncher, SecurePayload, NoisePayload, DhtConnector\n");

	if (sodium_init() < 0) {
		printf("FATAL: sodium_init failed\n");
		return 1;
	}

	// Holepuncher tests
	test_holepuncher_init();
	test_holepuncher_start_stop();
	test_holepuncher_probe_received();
	test_holepuncher_ignores_unknown_source();
	test_holepuncher_timeout();
	test_holepuncher_remote_update();

	// Message/encryption tests
	test_secure_payload_roundtrip();
	test_secure_payload_wrong_key();
	test_secure_payload_tampered();
	test_noise_payload_roundtrip();
	test_holepunch_message_roundtrip();

	// DhtConnector basic test
	test_dht_connector_alloc();

	// Noise IK + holepunch secret consistency
	test_noise_ik_key_consistency();

	printf("\n=== Summary: %d/%d passed", tests_passed, tests_run);
	if (tests_failed > 0) printf(", %d FAILED", tests_failed);
	printf(" ===\n\n");

	return tests_failed > 0 ? 1 : 0;
}