#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