OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / src / dht_messages.h

527 lines · c ref:
#ifndef HYPERSWARM_DHT_MESSAGES_H
#define HYPERSWARM_DHT_MESSAGES_H

// HyperDHT message codecs — wire-compatible with hyperdht/lib/messages.js.
// Header-only. Uses compact_encoding.h for wire encoding.

#include "compact_encoding.h"

#include <cstdint>
#include <cstring>

// ---- HyperDHT command numbers (constants.js) ----

static constexpr uint32_t HYPERDHT_CMD_PEER_HANDSHAKE = 0;
static constexpr uint32_t HYPERDHT_CMD_PEER_HOLEPUNCH = 1;
static constexpr uint32_t HYPERDHT_CMD_FIND_PEER      = 2;
static constexpr uint32_t HYPERDHT_CMD_LOOKUP          = 3;
static constexpr uint32_t HYPERDHT_CMD_ANNOUNCE         = 4;
static constexpr uint32_t HYPERDHT_CMD_UNANNOUNCE       = 5;
static constexpr uint32_t HYPERDHT_CMD_MUTABLE_PUT      = 6;
static constexpr uint32_t HYPERDHT_CMD_MUTABLE_GET      = 7;
static constexpr uint32_t HYPERDHT_CMD_IMMUTABLE_PUT    = 8;
static constexpr uint32_t HYPERDHT_CMD_IMMUTABLE_GET    = 9;

// ---- Firewall types ----

static constexpr uint32_t FIREWALL_UNKNOWN    = 0;
static constexpr uint32_t FIREWALL_OPEN       = 1;
static constexpr uint32_t FIREWALL_CONSISTENT = 2;
static constexpr uint32_t FIREWALL_RANDOM     = 3;

// ---- Handshake/holepunch routing modes (router.js) ----
// These match the JS constants exactly.

static constexpr uint32_t HS_MODE_FROM_CLIENT       = 0;
static constexpr uint32_t HS_MODE_FROM_SERVER       = 1;
static constexpr uint32_t HS_MODE_FROM_RELAY        = 2;
static constexpr uint32_t HS_MODE_FROM_SECOND_RELAY = 3;
static constexpr uint32_t HS_MODE_REPLY             = 4;

// ---- Handshake message ----
// Wraps a Noise IK message with mode and optional addresses.

struct HandshakeMessage {
	uint32_t mode;
	const uint8_t *noise;
	size_t noise_len;
	IPv4Address peer_address;
	bool has_peer_address;
	IPv4Address relay_address;
	bool has_relay_address;
};

namespace dht_msg {

inline void handshake_preencode(EncodeState &s, const HandshakeMessage &m) {
	s.end += 1 + 1; // flags + mode
	if (m.has_peer_address) s.end += 6;
	if (m.has_relay_address) s.end += 6;
	cenc::buffer_preencode(s, m.noise, m.noise_len);
}

inline void handshake_encode(EncodeState &s, const HandshakeMessage &m) {
	uint8_t flags = 0;
	if (m.has_peer_address) flags |= 1;
	if (m.has_relay_address) flags |= 2;

	cenc::uint_encode(s, flags);
	cenc::uint_encode(s, m.mode);
	cenc::buffer_encode(s, m.noise, m.noise_len);

	if (m.has_peer_address) cenc::ipv4_address_encode(s, m.peer_address);
	if (m.has_relay_address) cenc::ipv4_address_encode(s, m.relay_address);
}

inline bool handshake_decode(EncodeState &s, HandshakeMessage &m) {
	memset(&m, 0, sizeof(m));
	uint8_t flags = static_cast<uint8_t>(cenc::uint_decode(s));
	m.mode = static_cast<uint32_t>(cenc::uint_decode(s));

	BufView noise = cenc::buffer_decode(s);
	m.noise = noise.data;
	m.noise_len = noise.len;

	if (flags & 1) {
		m.peer_address = cenc::ipv4_address_decode(s);
		m.has_peer_address = true;
	}
	if (flags & 2) {
		m.relay_address = cenc::ipv4_address_decode(s);
		m.has_relay_address = true;
	}
	return true;
}

// ---- Noise Payload (inside Noise IK encrypted handshake) ----

struct UdxInfo {
	uint32_t version;
	bool reusable_socket;
	uint32_t id;    // Stream ID.
	uint32_t seq;   // Sequence number.
};

struct NoisePayload {
	uint32_t version;  // Always 1.
	uint32_t error;
	uint32_t firewall; // FIREWALL_* constants.

	// Holepunch info (flag bit 0).
	bool has_holepunch;
	uint32_t holepunch_id;

	// IPv4 addresses (flag bit 1).
	IPv4Address addresses4[8];
	int addresses4_count;

	// UDX info (flag bit 3).
	bool has_udx;
	UdxInfo udx;

	// Secret stream (flag bit 4).
	bool has_secret_stream;

	// Relay through (flag bit 5).
	bool has_relay_through;
	uint8_t relay_public_key[32];
	uint8_t relay_token[32];
};

inline void noise_payload_preencode(EncodeState &s, const NoisePayload &m) {
	s.end += 4; // version + flags + error + firewall
	if (m.has_holepunch) {
		cenc::uint_preencode(s, m.holepunch_id);
		s.end += 1; // empty relays array (varint 0)
	}
	if (m.addresses4_count > 0) {
		cenc::ipv4_array_preencode(s, m.addresses4, m.addresses4_count);
	}
	if (m.has_udx) {
		s.end += 2; // version + features
		cenc::uint_preencode(s, m.udx.id);
		cenc::uint_preencode(s, m.udx.seq);
	}
	if (m.has_secret_stream) {
		s.end += 1; // version
	}
	if (m.has_relay_through) {
		s.end += 2; // version + flags
		s.end += 64; // publicKey + token
	}
}

inline void noise_payload_encode(EncodeState &s, const NoisePayload &m) {
	uint8_t flags = 0;
	if (m.has_holepunch) flags |= 1;
	if (m.addresses4_count > 0) flags |= 2;
	if (m.has_udx) flags |= 8;
	if (m.has_secret_stream) flags |= 16;
	if (m.has_relay_through) flags |= 32;

	cenc::uint_encode(s, 1); // version
	cenc::uint_encode(s, flags);
	cenc::uint_encode(s, m.error);
	cenc::uint_encode(s, m.firewall);

	if (m.has_holepunch) {
		cenc::uint_encode(s, m.holepunch_id);
		cenc::uint_encode(s, 0); // empty relays array
	}
	if (m.addresses4_count > 0) {
		cenc::ipv4_array_encode(s, m.addresses4, m.addresses4_count);
	}
	if (m.has_udx) {
		cenc::uint_encode(s, 1); // version
		cenc::uint_encode(s, m.udx.reusable_socket ? 1u : 0u);
		cenc::uint_encode(s, m.udx.id);
		cenc::uint_encode(s, m.udx.seq);
	}
	if (m.has_secret_stream) {
		cenc::uint_encode(s, 1); // version
	}
	if (m.has_relay_through) {
		cenc::uint_encode(s, 1); // version
		cenc::uint_encode(s, 0); // flags
		cenc::fixed32_encode(s, m.relay_public_key);
		cenc::fixed32_encode(s, m.relay_token);
	}
}

inline bool noise_payload_decode(EncodeState &s, NoisePayload &m) {
	memset(&m, 0, sizeof(m));
	m.version = static_cast<uint32_t>(cenc::uint_decode(s));
	if (m.version != 1) return true; // Unknown version — return defaults.

	uint8_t flags = static_cast<uint8_t>(cenc::uint_decode(s));
	m.error = static_cast<uint32_t>(cenc::uint_decode(s));
	m.firewall = static_cast<uint32_t>(cenc::uint_decode(s));

	if (flags & 1) {
		m.has_holepunch = true;
		m.holepunch_id = static_cast<uint32_t>(cenc::uint_decode(s));
		// Skip relays array.
		uint64_t relay_count = cenc::uint_decode(s);
		s.start += static_cast<size_t>(relay_count) * 12; // Each relay is 12 bytes (2x ipv4Address).
	}
	if (flags & 2) {
		m.addresses4_count = cenc::ipv4_array_decode(s, m.addresses4, 8);
	}
	// Skip IPv6 (flag bit 2) — we don't handle it.
	if (flags & 4) {
		uint64_t count = cenc::uint_decode(s);
		s.start += static_cast<size_t>(count) * 18; // ipv6Address = 16 + 2 bytes.
	}
	if (flags & 8) {
		m.has_udx = true;
		m.udx.version = static_cast<uint32_t>(cenc::uint_decode(s));
		uint32_t features = static_cast<uint32_t>(cenc::uint_decode(s));
		m.udx.reusable_socket = (features & 1) != 0;
		m.udx.id = static_cast<uint32_t>(cenc::uint_decode(s));
		m.udx.seq = static_cast<uint32_t>(cenc::uint_decode(s));
	}
	if (flags & 16) {
		m.has_secret_stream = true;
		cenc::uint_decode(s); // version, ignore.
	}
	if (flags & 32) {
		m.has_relay_through = true;
		cenc::uint_decode(s); // version.
		cenc::uint_decode(s); // flags.
		const uint8_t *pk = cenc::fixed32_decode(s);
		if (!pk) return false;
		memcpy(m.relay_public_key, pk, 32);
		const uint8_t *tok = cenc::fixed32_decode(s);
		if (!tok) return false;
		memcpy(m.relay_token, tok, 32);
	}
	return true;
}

// ---- Holepunch message (outer wrapper) ----

struct HolepunchMessage {
	uint32_t mode;
	uint32_t id;
	const uint8_t *payload; // Encrypted by SecurePayload.
	size_t payload_len;
	IPv4Address peer_address;
	bool has_peer_address;
};

inline void holepunch_msg_preencode(EncodeState &s, const HolepunchMessage &m) {
	s.end += 2; // flags + mode
	cenc::uint_preencode(s, m.id);
	cenc::buffer_preencode(s, m.payload, m.payload_len);
	if (m.has_peer_address) s.end += 6;
}

inline void holepunch_msg_encode(EncodeState &s, const HolepunchMessage &m) {
	uint8_t flags = m.has_peer_address ? 1 : 0;
	cenc::uint_encode(s, flags);
	cenc::uint_encode(s, m.mode);
	cenc::uint_encode(s, m.id);
	cenc::buffer_encode(s, m.payload, m.payload_len);
	if (m.has_peer_address) cenc::ipv4_address_encode(s, m.peer_address);
}

inline bool holepunch_msg_decode(EncodeState &s, HolepunchMessage &m) {
	memset(&m, 0, sizeof(m));
	uint8_t flags = static_cast<uint8_t>(cenc::uint_decode(s));
	m.mode = static_cast<uint32_t>(cenc::uint_decode(s));
	m.id = static_cast<uint32_t>(cenc::uint_decode(s));
	BufView pl = cenc::buffer_decode(s);
	m.payload = pl.data;
	m.payload_len = pl.len;
	if (flags & 1) {
		m.peer_address = cenc::ipv4_address_decode(s);
		m.has_peer_address = true;
	}
	return true;
}

// ---- Holepunch payload (encrypted inside HolepunchMessage) ----

struct HolepunchPayload {
	uint32_t error;
	uint32_t firewall;
	uint32_t round;
	bool connected;
	bool punching;
	IPv4Address addresses[8];
	int addresses_count;
	IPv4Address remote_address;
	bool has_remote_address;
	uint8_t token[32];
	bool has_token;
	uint8_t remote_token[32];
	bool has_remote_token;
};

inline void holepunch_payload_preencode(EncodeState &s, const HolepunchPayload &m) {
	s.end += 4; // flags + error + firewall + round
	if (m.addresses_count > 0) cenc::ipv4_array_preencode(s, m.addresses, m.addresses_count);
	if (m.has_remote_address) s.end += 6;
	if (m.has_token) s.end += 32;
	if (m.has_remote_token) s.end += 32;
}

inline void holepunch_payload_encode(EncodeState &s, const HolepunchPayload &m) {
	uint8_t flags = 0;
	if (m.connected) flags |= 1;
	if (m.punching) flags |= 2;
	if (m.addresses_count > 0) flags |= 4;
	if (m.has_remote_address) flags |= 8;
	if (m.has_token) flags |= 16;
	if (m.has_remote_token) flags |= 32;

	cenc::uint_encode(s, flags);
	cenc::uint_encode(s, m.error);
	cenc::uint_encode(s, m.firewall);
	cenc::uint_encode(s, m.round);

	if (m.addresses_count > 0) cenc::ipv4_array_encode(s, m.addresses, m.addresses_count);
	if (m.has_remote_address) cenc::ipv4_address_encode(s, m.remote_address);
	if (m.has_token) cenc::fixed32_encode(s, m.token);
	if (m.has_remote_token) cenc::fixed32_encode(s, m.remote_token);
}

inline bool holepunch_payload_decode(EncodeState &s, HolepunchPayload &m) {
	memset(&m, 0, sizeof(m));
	uint8_t flags = static_cast<uint8_t>(cenc::uint_decode(s));
	m.error = static_cast<uint32_t>(cenc::uint_decode(s));
	m.firewall = static_cast<uint32_t>(cenc::uint_decode(s));
	m.round = static_cast<uint32_t>(cenc::uint_decode(s));

	m.connected = (flags & 1) != 0;
	m.punching = (flags & 2) != 0;
	if (flags & 4) {
		m.addresses_count = cenc::ipv4_array_decode(s, m.addresses, 8);
	}
	if (flags & 8) {
		m.remote_address = cenc::ipv4_address_decode(s);
		m.has_remote_address = true;
	}
	if (flags & 16) {
		const uint8_t *tok = cenc::fixed32_decode(s);
		if (!tok) return false;
		memcpy(m.token, tok, 32);
		m.has_token = true;
	}
	if (flags & 32) {
		const uint8_t *tok = cenc::fixed32_decode(s);
		if (!tok) return false;
		memcpy(m.remote_token, tok, 32);
		m.has_remote_token = true;
	}
	return true;
}

// ---- Peer record (for announce/lookup responses) ----

struct PeerRecord {
	uint8_t public_key[32];
	IPv4Address relay_addresses[8];
	int relay_count;
};

inline void peer_record_preencode(EncodeState &s, const PeerRecord &m) {
	s.end += 32; // publicKey
	cenc::ipv4_array_preencode(s, m.relay_addresses, m.relay_count);
}

inline void peer_record_encode(EncodeState &s, const PeerRecord &m) {
	cenc::fixed32_encode(s, m.public_key);
	cenc::ipv4_array_encode(s, m.relay_addresses, m.relay_count);
}

inline bool peer_record_decode(EncodeState &s, PeerRecord &m) {
	memset(&m, 0, sizeof(m));
	const uint8_t *pk = cenc::fixed32_decode(s);
	if (!pk) return false;
	memcpy(m.public_key, pk, 32);
	m.relay_count = cenc::ipv4_array_decode(s, m.relay_addresses, 8);
	return true;
}

// ---- Announce message ----

struct AnnounceMessage {
	PeerRecord peer;
	bool has_peer;
	uint8_t refresh[32];
	bool has_refresh;
	uint8_t signature[64];
	bool has_signature;
	uint32_t bump;
	bool has_bump;
};

inline void announce_preencode(EncodeState &s, const AnnounceMessage &m) {
	s.end += 1; // flags
	if (m.has_peer) peer_record_preencode(s, m.peer);
	if (m.has_refresh) s.end += 32;
	if (m.has_signature) s.end += 64;
	if (m.has_bump) cenc::uint_preencode(s, m.bump);
}

inline void announce_encode(EncodeState &s, const AnnounceMessage &m) {
	uint8_t flags = 0;
	if (m.has_peer) flags |= 1;
	if (m.has_refresh) flags |= 2;
	if (m.has_signature) flags |= 4;
	if (m.has_bump) flags |= 8;

	cenc::uint_encode(s, flags);
	if (m.has_peer) peer_record_encode(s, m.peer);
	if (m.has_refresh) cenc::fixed32_encode(s, m.refresh);
	if (m.has_signature) cenc::fixed64_encode(s, m.signature);
	if (m.has_bump) cenc::uint_encode(s, m.bump);
}

inline bool announce_decode(EncodeState &s, AnnounceMessage &m) {
	memset(&m, 0, sizeof(m));
	uint8_t flags = static_cast<uint8_t>(cenc::uint_decode(s));

	if (flags & 1) {
		peer_record_decode(s, m.peer);
		m.has_peer = true;
	}
	if (flags & 2) {
		const uint8_t *r = cenc::fixed32_decode(s);
		if (!r) return false;
		memcpy(m.refresh, r, 32);
		m.has_refresh = true;
	}
	if (flags & 4) {
		const uint8_t *sig = cenc::fixed64_decode(s);
		if (!sig) return false;
		memcpy(m.signature, sig, 64);
		m.has_signature = true;
	}
	if (flags & 8) {
		m.bump = static_cast<uint32_t>(cenc::uint_decode(s));
		m.has_bump = true;
	}
	return true;
}

// ---- Lookup raw reply ----

struct LookupReply {
	PeerRecord peers[16];
	int peer_count;
	uint32_t bump;
};

inline bool lookup_reply_decode(EncodeState &s, LookupReply &m) {
	memset(&m, 0, sizeof(m));
	// peers array
	uint64_t count = cenc::uint_decode(s);
	m.peer_count = (count > 16) ? 16 : static_cast<int>(count);
	for (int i = 0; i < m.peer_count; i++) {
		if (!peer_record_decode(s, m.peers[i])) {
			m.peer_count = i;
			return true;
		}
	}
	// Skip excess
	for (uint64_t i = m.peer_count; i < count; i++) {
		if (s.start + 32 > s.end) { s.start = s.end; break; }
		s.start += 32; // publicKey
		uint64_t rc = cenc::uint_decode(s);
		size_t skip = static_cast<size_t>(rc) * 6;
		if (s.start + skip > s.end) { s.start = s.end; break; }
		s.start += skip; // relay addresses
	}
	// bump (optional)
	if (s.start < s.end) {
		m.bump = static_cast<uint32_t>(cenc::uint_decode(s));
	}
	return true;
}

// ---- SecurePayload: XSalsa20-Poly1305 encrypt/decrypt ----
// Used for holepunch payloads encrypted with holepunch_secret.

#include <sodium.h>

static constexpr size_t SECRETBOX_NONCE = 24;
static constexpr size_t SECRETBOX_MAC = 16;

inline std::vector<uint8_t> secure_payload_encrypt(
		const uint8_t *plaintext, size_t plaintext_len,
		const uint8_t *key_32) {
	std::vector<uint8_t> out(SECRETBOX_NONCE + plaintext_len + SECRETBOX_MAC);

	// Random nonce.
	randombytes_buf(out.data(), SECRETBOX_NONCE);

	// Encrypt: nonce || ciphertext+mac.
	crypto_secretbox_easy(
		out.data() + SECRETBOX_NONCE,
		plaintext, plaintext_len,
		out.data(), key_32);

	return out;
}

inline bool secure_payload_decrypt(
		const uint8_t *data, size_t data_len,
		const uint8_t *key_32,
		std::vector<uint8_t> &out) {
	if (data_len < SECRETBOX_NONCE + SECRETBOX_MAC) return false;

	size_t ct_len = data_len - SECRETBOX_NONCE;
	out.resize(ct_len - SECRETBOX_MAC);

	return crypto_secretbox_open_easy(
		out.data(),
		data + SECRETBOX_NONCE, ct_len,
		data, key_32) == 0;
}

} // namespace dht_msg

#endif // HYPERSWARM_DHT_MESSAGES_H