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