#include "holepuncher.h"
#include "debug_log.h"
#include <cstdlib>
// Single-byte holepunch probe payload (matches JS).
static const uint8_t HOLEPUNCH_PROBE[] = { 0x00 };
Holepuncher::Holepuncher()
: _socket(nullptr), _local_firewall(0), _stage(HP_IDLE),
_punching(false), _connected(false), _started(false),
_start_time(0), _last_probe_time(0), _probe_round(0),
_birthday_count(0), _birthday_active(false),
_birthday_probe_index(0), _birthday_iteration(0),
_birthday_low_ttl_round(true), _winning_socket(nullptr),
_random_iteration(0) {
memset(&_remote, 0, sizeof(_remote));
memset(_birthday_sockets, 0, sizeof(_birthday_sockets));
}
Holepuncher::~Holepuncher() {
if (_birthday_active) {
_cleanup_birthday_sockets();
}
if (_winning_socket) {
udx_socket_recv_stop(_winning_socket);
udx_socket_close(_winning_socket);
_winning_socket = nullptr;
}
}
void Holepuncher::init(udx_socket_t *socket, uint32_t local_firewall) {
_socket = socket;
_local_firewall = local_firewall;
}
void Holepuncher::update_remote(const RemotePeerInfo &info) {
_remote = info;
}
void Holepuncher::start() {
if (_started) return;
_started = true;
_punching = true;
_connected = false;
_probe_round = 0;
_last_probe_time = 0;
_random_iteration = 0;
_birthday_iteration = 0;
_birthday_probe_index = 0;
_birthday_low_ttl_round = true;
// If both firewalls are already known, decide strategy immediately.
if (_local_firewall != FIREWALL_UNKNOWN && _remote.firewall != FIREWALL_UNKNOWN) {
_decide_strategy();
} else {
_stage = HP_ANALYZE;
}
}
void Holepuncher::stop() {
bool was_active = _punching || _stage == HP_ANALYZE;
if (!was_active) return;
_punching = false;
_stage = HP_FAILED;
if (_birthday_active) _cleanup_birthday_sockets();
if (!_connected && on_abort) {
on_abort();
}
}
void Holepuncher::reset() {
// Full state reset for slot reuse. stop() only clears _punching/_stage,
// leaving _started/_connected stale — which causes tick() to immediately
// report is_connected()=true on reused slots.
if (_birthday_active) _cleanup_birthday_sockets();
if (_winning_socket) {
udx_socket_recv_stop(_winning_socket);
udx_socket_close(_winning_socket);
_winning_socket = nullptr;
}
_socket = nullptr;
_local_firewall = FIREWALL_UNKNOWN;
_stage = HP_IDLE;
_punching = false;
_connected = false;
_started = false;
_start_time = 0;
_last_probe_time = 0;
_probe_round = 0;
_birthday_count = 0;
_birthday_active = false;
_birthday_probe_index = 0;
_birthday_iteration = 0;
_birthday_low_ttl_round = true;
_random_iteration = 0;
memset(&_remote, 0, sizeof(_remote));
on_connect = nullptr;
on_abort = nullptr;
}
// ---- Strategy decision ----
// Matches JS holepuncher.js _punch() method decision tree.
void Holepuncher::_decide_strategy() {
uint32_t local = _local_firewall;
uint32_t remote = _remote.firewall;
if (local >= FIREWALL_RANDOM && remote >= FIREWALL_RANDOM) {
// Both random — impossible to hole-punch.
_stage = HP_FAILED;
_punching = false;
if (on_abort) on_abort();
return;
}
if (local <= FIREWALL_CONSISTENT && remote <= FIREWALL_CONSISTENT) {
// Both open/consistent — direct probing.
_stage = HP_PUNCH_CONSISTENT;
} else if (local <= FIREWALL_CONSISTENT && remote >= FIREWALL_RANDOM) {
// Local consistent, remote random — guess random ports.
_stage = HP_PUNCH_RANDOM;
} else {
// Local random, remote consistent — birthday attack.
_stage = HP_PUNCH_BIRTHDAY;
_start_birthday_attack();
}
}
// ---- Tick ----
bool Holepuncher::tick(uint64_t now_ms) {
if (!_started) return false;
if (_connected) return false;
if (_start_time == 0) _start_time = now_ms;
switch (_stage) {
case HP_ANALYZE:
if (_local_firewall != FIREWALL_UNKNOWN && _remote.firewall != FIREWALL_UNKNOWN) {
_decide_strategy();
} else if (now_ms - _start_time > HP_ANALYZE_TIMEOUT_MS) {
// Timeout — fall back to consistent probing.
_stage = HP_PUNCH_CONSISTENT;
}
break;
case HP_PUNCH_CONSISTENT:
_consistent_probe(now_ms);
break;
case HP_PUNCH_RANDOM:
_random_probe_tick(now_ms);
break;
case HP_PUNCH_BIRTHDAY:
_birthday_probe_tick(now_ms);
break;
case HP_CONNECTED:
case HP_FAILED:
case HP_IDLE:
return false;
}
return _punching;
}
// ---- Consistent probing (both OPEN/CONSISTENT) ----
// Matches JS _consistentProbe(): 10 rounds × 1s = ~10 seconds.
void Holepuncher::_consistent_probe(uint64_t now_ms) {
if (now_ms - _last_probe_time < (uint64_t)HP_CONSISTENT_PROBE_INTERVAL_MS) return;
_last_probe_time = now_ms;
if (_probe_round >= HP_CONSISTENT_PROBE_MAX_ROUNDS) {
_punching = false;
_stage = HP_FAILED;
if (on_abort) on_abort();
return;
}
// Send probes to all known remote addresses.
for (int i = 0; i < _remote.address_count; i++) {
// Prioritize verified address; probe unverified every 4th round.
bool is_verified = _remote.has_verified &&
memcmp(_remote.addresses[i].ip, _remote.verified_address.ip, 4) == 0 &&
_remote.addresses[i].port == _remote.verified_address.port;
if (!is_verified && (_probe_round & 3) != 0) continue;
send_probe(_remote.addresses[i], HP_TTL_NORMAL);
}
if (_remote.has_verified) {
send_probe(_remote.verified_address, HP_TTL_NORMAL);
}
_probe_round++;
}
// ---- Random port probing (local CONSISTENT → remote RANDOM) ----
// Matches JS _randomProbes(): 1750 iterations × 20ms = ~35 seconds.
// Sends probes to random ports on the remote's known IP.
void Holepuncher::_random_probe_tick(uint64_t now_ms) {
if (now_ms - _last_probe_time < (uint64_t)HP_RANDOM_PROBE_INTERVAL_MS) return;
_last_probe_time = now_ms;
if (_random_iteration >= HP_RANDOM_PROBE_MAX_ITERATIONS) {
_punching = false;
_stage = HP_FAILED;
if (on_abort) on_abort();
return;
}
// Generate random port in [1000, 65535] (matches JS randomPort()).
const IPv4Address &target = _remote_target();
IPv4Address random_addr;
memcpy(random_addr.ip, target.ip, 4);
random_addr.port = static_cast<uint16_t>(1000 + (rand() % 64536));
send_probe(random_addr, HP_TTL_NORMAL);
_random_iteration++;
}
// ---- Birthday attack probing (local RANDOM → remote CONSISTENT) ----
// Cycles through 256 birthday sockets at 20ms intervals.
// First full cycle uses low TTL (5), subsequent cycles use normal TTL (64).
// Matches JS _openBirthdaySockets() + _keepAliveRandomNat().
void Holepuncher::_birthday_probe_tick(uint64_t now_ms) {
if (!_birthday_active || _birthday_count == 0) return;
if (now_ms - _last_probe_time < (uint64_t)HP_RANDOM_PROBE_INTERVAL_MS) return;
_last_probe_time = now_ms;
if (_birthday_iteration >= HP_RANDOM_PROBE_MAX_ITERATIONS) {
_punching = false;
_stage = HP_FAILED;
_cleanup_birthday_sockets();
if (on_abort) on_abort();
return;
}
// Cycle through birthday sockets.
if (_birthday_probe_index >= _birthday_count) {
_birthday_probe_index = 0;
_birthday_low_ttl_round = false; // After first full round, use normal TTL.
}
udx_socket_t *sock = _birthday_sockets[_birthday_probe_index];
if (sock) {
int ttl = _birthday_low_ttl_round ? HP_TTL_LOW : HP_TTL_NORMAL;
_send_probe_from(sock, _remote_target(), ttl);
}
_birthday_probe_index++;
_birthday_iteration++;
}
// ---- Birthday socket allocation ----
// Creates 256 sockets bound to random ports, starts recv on each,
// and sends an initial low-TTL probe from each.
void Holepuncher::_start_birthday_attack() {
if (!_socket || _birthday_active) return;
udx_t *udx = _socket->udx;
if (!udx) return;
const IPv4Address &target = _remote_target();
if (target.port == 0) return;
struct sockaddr_in bind_addr;
memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = 0; // OS assigns random port.
bind_addr.sin_addr.s_addr = INADDR_ANY;
_birthday_count = 0;
for (int i = 0; i < HP_BIRTHDAY_SOCKET_COUNT; i++) {
udx_socket_t *sock = new udx_socket_t();
memset(sock, 0, sizeof(udx_socket_t));
int r = udx_socket_init(udx, sock, _on_birthday_close);
if (r != 0) {
delete sock;
break;
}
r = udx_socket_bind(sock, (struct sockaddr *)&bind_addr, 0);
if (r != 0) {
udx_socket_close(sock);
break;
}
// ⛔ `sock->data`, NOT `sock->uv_udp.data`.
//
// `uv_udp.data` belongs to libudx: udx_socket_init sets it to the
// udx_socket_t itself (udx.c:1960) and on_udx_socket_handle_close casts
// it back and writes through it (`socket->nrefs--`). Storing a
// Holepuncher* there made that callback decrement a field inside THIS
// object — silent heap corruption. Nulling it in cleanup to "prevent a
// callback crash" only traded that for a NULL deref, which is the SEGV
// test_phase6 has been dying on. `udx_socket_t::data` (udx.h:200) is the
// user slot; libudx never reads it.
sock->data = this;
udx_socket_recv_start(sock, _on_birthday_recv);
_birthday_sockets[_birthday_count++] = sock;
// Send initial low-TTL probe from this socket.
_send_probe_from(sock, target, HP_TTL_LOW);
}
_birthday_active = (_birthday_count > 0);
_birthday_probe_index = 0;
_birthday_iteration = 0;
_birthday_low_ttl_round = true;
DHT_LOG("[Holepuncher] Birthday attack: opened %d sockets\n", _birthday_count);
}
// ---- Birthday socket cleanup ----
// Closes all birthday sockets except the winner.
void Holepuncher::_cleanup_birthday_sockets() {
for (int i = 0; i < _birthday_count; i++) {
if (_birthday_sockets[i] && _birthday_sockets[i] != _winning_socket) {
// Detach us from the socket so a recv still in flight bails out.
// The socket's own libudx back-pointer is left alone — it is what
// the close callback needs to run.
_birthday_sockets[i]->data = nullptr;
udx_socket_recv_stop(_birthday_sockets[i]);
udx_socket_close(_birthday_sockets[i]);
}
_birthday_sockets[i] = nullptr;
}
_birthday_count = 0;
_birthday_active = false;
}
// ---- Birthday socket callbacks ----
void Holepuncher::_on_birthday_recv(udx_socket_t *socket, ssize_t read_len,
const uv_buf_t * /* buf */, const struct sockaddr *from) {
if (!socket->data) return; // Holepuncher gone.
if (read_len != 1) return; // Not a 1-byte holepunch probe.
Holepuncher *self = (Holepuncher *)socket->data;
const struct sockaddr_in *sin = (const struct sockaddr_in *)from;
IPv4Address addr;
memcpy(addr.ip, &sin->sin_addr.s_addr, 4);
addr.port = ntohs(sin->sin_port);
self->_on_birthday_probe(socket, addr);
}
void Holepuncher::_on_birthday_close(udx_socket_t *socket) {
delete socket;
}
void Holepuncher::_on_birthday_probe(udx_socket_t *winner, const IPv4Address &from) {
if (_connected) return;
// Validate source IP matches the remote peer.
if (!_is_remote_ip(from)) {
DHT_LOG("[Holepuncher] IGNORING birthday probe from non-remote IP %d.%d.%d.%d:%d\n",
from.ip[0], from.ip[1], from.ip[2], from.ip[3], from.port);
return;
}
_winning_socket = winner;
_connected = true;
_punching = false;
_stage = HP_CONNECTED;
// Close all other birthday sockets, keeping the winner.
_cleanup_birthday_sockets();
if (on_connect) on_connect(from);
}
// ---- Send probe ----
void Holepuncher::send_probe(const IPv4Address &to, int ttl) {
_send_probe_from(_socket, to, ttl);
}
void Holepuncher::_send_probe_from(udx_socket_t *sock, const IPv4Address &to, int ttl) {
if (!sock) return;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(to.port);
memcpy(&addr.sin_addr.s_addr, to.ip, 4);
uv_buf_t buf = uv_buf_init((char *)HOLEPUNCH_PROBE, 1);
udx_socket_send_t *req = (udx_socket_send_t *)malloc(sizeof(udx_socket_send_t));
udx_socket_send_ttl(req, sock, &buf, 1,
(const struct sockaddr *)&addr, ttl,
[](udx_socket_send_t *r, int /* status */) { free(r); });
}
// ---- Probe received (main socket or external dispatch) ----
void Holepuncher::on_probe_received(const IPv4Address &from) {
if (_connected) return;
if (!_punching) {
// Not actively punching — echo back (server behavior).
send_probe(from, HP_TTL_NORMAL);
return;
}
// Validate source IP matches the remote peer we're punching to.
// Without this, probes from local processes (e.g. keet daemon)
// would be accepted as successful punches.
if (!_is_remote_ip(from)) {
DHT_LOG("[Holepuncher] IGNORING probe from non-remote IP %d.%d.%d.%d:%d\n",
from.ip[0], from.ip[1], from.ip[2], from.ip[3], from.port);
return;
}
// Connection established!
_connected = true;
_punching = false;
_stage = HP_CONNECTED;
if (_birthday_active) {
_cleanup_birthday_sockets();
}
if (on_connect) {
on_connect(from);
}
}
// ---- Remote target address ----
const IPv4Address &Holepuncher::_remote_target() const {
if (_remote.has_verified) return _remote.verified_address;
if (_remote.address_count > 0) return _remote.addresses[0];
static IPv4Address empty = {};
return empty;
}
// ---- Source IP validation ----
// Only accept probes from IPs matching the remote peer's known addresses.
// Port is NOT checked — NAT may assign different ports.
bool Holepuncher::_is_remote_ip(const IPv4Address &from) const {
for (int i = 0; i < _remote.address_count; i++) {
if (memcmp(from.ip, _remote.addresses[i].ip, 4) == 0) return true;
}
if (_remote.has_verified && memcmp(from.ip, _remote.verified_address.ip, 4) == 0) {
return true;
}
return false;
}
// ---- Take winning socket (ownership transfer) ----
udx_socket_t *Holepuncher::take_winning_socket() {
udx_socket_t *s = _winning_socket;
_winning_socket = nullptr;
if (s) {
// Stop the birthday recv handler — UDX stream will handle data now.
// Also prevents use-after-free if packets arrive after puncher cleanup.
s->data = nullptr;
udx_socket_recv_stop(s);
// Remove from birthday array so cleanup() won't accidentally close it.
for (int i = 0; i < _birthday_count; i++) {
if (_birthday_sockets[i] == s) {
_birthday_sockets[i] = nullptr;
break;
}
}
}
return s;
}