OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_handshake_pool.cpp

168 lines · cpp ref:
// Gate: a responder must still answer after it has already handled many
// handshakes. hyper-games #72's wall, reduced to one process.
//
//   cross-machine, both peers fresh      WORKS
//   cross-machine, one peer up minutes   FAILS  <- this
//
// Mechanism under test: HyperDht keeps a FIXED pool of 16 DhtHandshake slots.
// _alloc_handshake() returns nullptr once _handshake_count hits 16, and
// _handle_handshake() answers that with a bare `return` -- no reply, no log.
// _free_handshake() has exactly ONE call site (the Noise-recv failure path), so
// a handshake that SUCCEEDS is never reclaimed and there is no timeout sweep.
// JS reclaims on stream close and on a clearing timeout (server.js _clearLater).
//
// What is counted: on_handshake_request, which HyperDht fires only AFTER it has
// allocated a slot and decrypted the payload, and immediately before it builds
// the reply. It is upstream of the send, so "callback fired" and "reply built"
// are the same event -- that is why counting it does not need a socket.

#include <cstdio>
#include <cstring>
#include <vector>

#include <sodium.h>

#include "dht_messages.h"
#include "dht_rpc.h"
#include "hyperdht.h"
#include "noise_ik.h"

extern "C" {
#include <udx.h>
#include <uv.h>
}

// How many handshakes to drive through one responder. Must exceed the pool.
static const int ATTEMPTS = 20;
static const int POOL = 16;

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

	uv_loop_t loop;
	uv_loop_init(&loop);
	udx_t udx;
	udx_init(&loop, &udx, nullptr);
	udx_socket_t sock;
	udx_socket_init(&udx, &sock, nullptr);
	struct sockaddr_in addr;
	uv_ip4_addr("127.0.0.1", 0, &addr);
	udx_socket_bind(&sock, (struct sockaddr *)&addr, 0);

	DhtRpc rpc;
	rpc.init(&sock, &loop);

	Ed25519KeyPair server_kp;
	crypto_sign_keypair(server_kp.public_key, server_kp.secret_key);

	HyperDht dht;
	dht.init(&rpc, server_kp);
	dht.listen();

	int accepted = 0;
	dht.on_handshake_request = [&accepted](uint32_t, const uint8_t *,
										   const dht_msg::NoisePayload &) {
		accepted++;
	};

	// The namespace the peer handshake is bound to. Derived the same way the
	// responder derives it, so the client's IK message is one it will accept.
	uint8_t ns[32];
	{
		uint8_t ns_buf[5 * 32];
		const char *name = "hyperswarm/dht";
		uint8_t ids[5] = { 4, 5, 6, 0, 1 };
		uint8_t base[32];
		crypto_generichash(base, 32, (const uint8_t *)name, strlen(name), nullptr, 0);
		for (int i = 0; i < 5; i++) {
			crypto_generichash_state st;
			crypto_generichash_init(&st, nullptr, 0, 32);
			crypto_generichash_update(&st, base, 32);
			crypto_generichash_update(&st, &ids[i], 1);
			crypto_generichash_final(&st, ns_buf + i * 32, 32);
		}
		memcpy(ns, ns_buf + 3 * 32, 32); // PEER_HANDSHAKE = index 3 in that list
	}

	printf("driving %d handshakes into one responder (pool is %d)\n", ATTEMPTS, POOL);

	// A daemon that has been up for a while: each handshake arrives a minute
	// after the last, and the node ticks in between. That is the reported
	// failure ("cross-machine, one peer up minutes"), not a burst — 16
	// CONCURRENT handshakes is a capacity limit and a different question.
	uint64_t now_ms = 1000;

	for (int i = 0; i < ATTEMPTS; i++) {
		now_ms += 60000;
		dht.tick(now_ms);

		// A DISTINCT client each time. Reusing one keypair would make every
		// handshake identical and could be deduplicated for reasons unrelated
		// to the pool, which would test the wrong thing.
		Ed25519KeyPair client_kp;
		crypto_sign_keypair(client_kp.public_key, client_kp.secret_key);

		NoiseIK ik(true, client_kp, ns, server_kp.public_key);

		dht_msg::NoisePayload payload;
		memset(&payload, 0, sizeof(payload));
		payload.version = 1;
		payload.firewall = 1; // FIREWALL_OPEN
		payload.has_udx = true;
		payload.udx.id = 1000 + i;
		payload.has_secret_stream = true;

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

		auto noise_msg = ik.send(pbuf.data(), pbuf.size());
		if (noise_msg.empty()) { printf("  client %d: IK send failed\n", i); break; }

		HandshakeMessage hmsg;
		memset(&hmsg, 0, sizeof(hmsg));
		hmsg.mode = HS_MODE_FROM_CLIENT;
		hmsg.noise = noise_msg.data();
		hmsg.noise_len = noise_msg.size();

		EncodeState hs_s = { 0, 0, nullptr };
		dht_msg::handshake_preencode(hs_s, hmsg);
		std::vector<uint8_t> hbuf(hs_s.end);
		hs_s.buffer = hbuf.data();
		hs_s.start = 0;
		dht_msg::handshake_encode(hs_s, hmsg);

		DhtRequest req;
		memset(&req, 0, sizeof(req));
		req.command = HYPERDHT_CMD_PEER_HANDSHAKE;
		req.from.ip[0] = 127; req.from.ip[1] = 0; req.from.ip[2] = 0; req.from.ip[3] = 1;
		req.from.port = (uint16_t)(40000 + i);
		req.value = hbuf.data();
		req.value_len = hbuf.size();

		int before = accepted;
		if (rpc.on_request) rpc.on_request(req);
		if (accepted == before) printf("  handshake %2d: NO REPLY\n", i + 1);
	}

	printf("\naccepted %d of %d\n", accepted, ATTEMPTS);

	const bool ok = (accepted == ATTEMPTS);
	if (!ok) {
		printf("FAIL: the responder stopped answering after %d handshakes.\n", accepted);
		printf("      A daemon that has served this many goes permanently silent:\n");
		printf("      _alloc_handshake returns nullptr and _handle_handshake returns\n");
		printf("      without replying or logging. This is #72's wall.\n");
	} else {
		printf("PASS: the responder answered every handshake.\n");
	}

	udx_socket_close(&sock);
	uv_run(&loop, UV_RUN_NOWAIT);
	uv_loop_close(&loop);
	return ok ? 0 : 1;
}