Add NodeDiscovery: multi-hop peer crawl with UDP-ping preference

- Implement NodeDiscovery engine: known-peer table (DynArr) with a
  per-tick seed/ping/query/connect state machine driven by the node
  maintenance thread; bounded multi-hop crawl (FANOUT peers per node,
  hop-capped) that connects to reachable peers lowest-ping-first
- Add GET_PEERS/PEERS TCP opcodes for peer-list exchange, handled on
  both inbound and outbound connections
- Measure UDP round-trip time and pass it to the on_pong callback
  (previously the send timestamp was only used for retries)
- Advertise each node's listen port in HELLO/ACK_HELLO and store it
  per-connection, so inbound-only peers and non-default ports are
  discoverable (length-guarded parse; wire-compatible with old peers)
- Wire a udp_node_t + node_discovery_t into net_node_t: init/start in
  Node_Create, tick in the maintenance loop, teardown in Node_Destroy
  (stop UDP before destroying discovery to avoid callback races)
- Add Node_ConnListenEndpoint / Node_GetPeerEndpoints helpers to derive
  peers' listen endpoints (outbound: dialed port; inbound: advertised),
  with IPv4-mapped-IPv6 normalization and IP+port dedup
- Match ping pong/timeout callbacks by peer address (the UDP layer owns
  the nonce), fixing discovered peers stuck UNREACHABLE
- Ping the peer's listen port instead of the ephemeral TCP source port
  (fixes the original stub so pongs actually return)
- Add `peers` CLI command to dump the discovery table (endpoint/hop/
  state/ping)
- Add discovery tunables to constants.h (fanout, max hops, target
  connections, timeouts, caps)
This commit is contained in:
2026-07-22 23:18:57 +02:00
parent 1345ff7fa6
commit 21fe73fb01
12 changed files with 1234 additions and 10 deletions
+379
View File
@@ -0,0 +1,379 @@
#include <udpd/udpnode.h>
#include <udpd/udppackettype.h>
#include <utils.h>
#include <numgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <errno.h>
typedef struct {
udp_node_t* node;
int sockFd;
} udprecv_thread_args_t;
// Send a raw PING packet (nonce already chosen) to dest.
static void UdpNode_SendRawPing(udp_node_t* node, uint64_t nonce, const struct sockaddr_storage* dest) {
unsigned char buf[UDP_PING_WIRE_SIZE];
buf[0] = (unsigned char)UDP_PACKET_TYPE_PING;
memcpy(buf + 1, &nonce, sizeof(nonce));
int sock = -1;
socklen_t addrLen = 0;
if (dest->ss_family == AF_INET6 && node->sockFd >= 0) {
sock = node->sockFd;
addrLen = sizeof(struct sockaddr_in6);
} else if (dest->ss_family == AF_INET && node->sockFdV4 >= 0) {
sock = node->sockFdV4;
addrLen = sizeof(struct sockaddr_in);
}
if (sock < 0) {
return;
}
sendto(sock, buf, sizeof(buf), 0, (const struct sockaddr*)dest, addrLen);
}
static void UdpNode_HandlePacket(udp_node_t* node, int fromSock,
const unsigned char* buf, ssize_t n,
const struct sockaddr_storage* from) {
if (n < 1) {
return;
}
udp_packet_type_t type = (udp_packet_type_t)buf[0];
switch (type) {
case UDP_PACKET_TYPE_PING: {
if (n < UDP_PING_WIRE_SIZE) {
return;
}
uint64_t nonce;
memcpy(&nonce, buf + 1, sizeof(nonce));
// Build and send PONG
unsigned char reply[UDP_PONG_WIRE_SIZE];
reply[0] = (unsigned char)UDP_PACKET_TYPE_PONG;
memcpy(reply + 1, &nonce, sizeof(nonce));
int32_t protoVer = (int32_t)PROTO_VERSION;
memcpy(reply + 1 + sizeof(nonce), &protoVer, sizeof(protoVer));
socklen_t addrLen = (from->ss_family == AF_INET6)
? sizeof(struct sockaddr_in6)
: sizeof(struct sockaddr_in);
sendto(fromSock, reply, sizeof(reply), 0, (const struct sockaddr*)from, addrLen);
break;
}
case UDP_PACKET_TYPE_PONG: {
if (n < UDP_PONG_WIRE_SIZE) {
return;
}
uint64_t nonce;
int32_t protoVer;
memcpy(&nonce, buf + 1, sizeof(nonce));
memcpy(&protoVer, buf + 1 + sizeof(nonce), sizeof(protoVer));
bool found = false;
uint64_t rttMs = 0;
pthread_mutex_lock(&node->pingsMutex);
for (int i = 0; i < UDP_MAX_PENDING_PINGS; i++) {
if (node->pendingPings[i].active && node->pendingPings[i].nonce == nonce) {
uint64_t nowMs = get_current_time_ms();
rttMs = (nowMs >= node->pendingPings[i].lastSentMs)
? (nowMs - node->pendingPings[i].lastSentMs)
: 0;
node->pendingPings[i].active = false;
found = true;
break;
}
}
pthread_mutex_unlock(&node->pingsMutex);
if (found && node->on_pong) {
node->on_pong(node, from, nonce, (int)protoVer, rttMs, node->callbackUser);
}
break;
}
default:
break;
}
}
static void* UdpNode_RecvThreadProc(void* arg) {
udprecv_thread_args_t* args = (udprecv_thread_args_t*)arg;
udp_node_t* node = args->node;
int sock = args->sockFd;
free(args);
unsigned char buf[1500];
while (node->isRunning) {
struct sockaddr_storage from;
socklen_t fromLen = sizeof(from);
ssize_t n = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr*)&from, &fromLen);
if (n < 1) {
if (!node->isRunning) {
break;
}
// Transient error — keep going
continue;
}
UdpNode_HandlePacket(node, sock, buf, n, &from);
}
return NULL;
}
static void* UdpNode_RetryThreadProc(void* arg) {
udp_node_t* node = (udp_node_t*)arg;
struct {
uint64_t nonce;
struct sockaddr_storage dest;
} timedOut[UDP_MAX_PENDING_PINGS];
while (node->isRunning) {
sleep_for_milliseconds(100);
int timedOutCount = 0;
pthread_mutex_lock(&node->pingsMutex);
uint64_t now = get_current_time_ms();
for (int i = 0; i < UDP_MAX_PENDING_PINGS; i++) {
pending_ping_t* p = &node->pendingPings[i];
if (!p->active) {
continue;
}
if (now - p->lastSentMs < UDP_PING_RETRY_INTERVAL_MS) {
continue;
}
if (p->retries >= UDP_PING_MAX_RETRIES) {
timedOut[timedOutCount].nonce = p->nonce;
timedOut[timedOutCount].dest = p->dest;
timedOutCount++;
p->active = false;
} else {
UdpNode_SendRawPing(node, p->nonce, &p->dest);
p->retries++;
p->lastSentMs = now;
}
}
pthread_mutex_unlock(&node->pingsMutex);
for (int i = 0; i < timedOutCount; i++) {
if (node->on_ping_timeout) {
node->on_ping_timeout(node, &timedOut[i].dest,
timedOut[i].nonce, node->callbackUser);
}
}
}
return NULL;
}
int UdpNode_Init(udp_node_t* node, uint16_t port) {
if (!node) {
return -1;
}
memset(node, 0, sizeof(*node));
node->sockFd = -1;
node->sockFdV4 = -1;
int opt = 1;
// IPv6 (pure, not dual-stack)
int fd6 = socket(AF_INET6, SOCK_DGRAM, 0);
if (fd6 >= 0) {
setsockopt(fd6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
int v6only = 1;
setsockopt(fd6, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
struct sockaddr_in6 a6;
memset(&a6, 0, sizeof(a6));
a6.sin6_family = AF_INET6;
a6.sin6_port = htons(port);
a6.sin6_addr = in6addr_any;
if (bind(fd6, (struct sockaddr*)&a6, sizeof(a6)) == 0) {
node->sockFd = fd6;
} else {
close(fd6);
}
}
// IPv4
int fd4 = socket(AF_INET, SOCK_DGRAM, 0);
if (fd4 >= 0) {
setsockopt(fd4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in a4;
memset(&a4, 0, sizeof(a4));
a4.sin_family = AF_INET;
a4.sin_port = htons(port);
a4.sin_addr.s_addr = INADDR_ANY;
if (bind(fd4, (struct sockaddr*)&a4, sizeof(a4)) == 0) {
node->sockFdV4 = fd4;
} else {
close(fd4);
}
}
if (node->sockFd < 0 && node->sockFdV4 < 0) {
return -1;
}
pthread_mutex_init(&node->pingsMutex, NULL);
return 0;
}
void UdpNode_SetCallbacks(udp_node_t* node,
void (*on_pong)(udp_node_t*, const struct sockaddr_storage*, uint64_t, int, uint64_t, void*),
void (*on_ping_timeout)(udp_node_t*, const struct sockaddr_storage*, uint64_t, void*),
void* user) {
if (!node) {
return;
}
node->on_pong = on_pong;
node->on_ping_timeout = on_ping_timeout;
node->callbackUser = user;
}
int UdpNode_Start(udp_node_t* node) {
if (!node || node->isRunning) {
return -1;
}
if (node->sockFd < 0 && node->sockFdV4 < 0) {
return -1;
}
node->isRunning = 1;
int anyStarted = 0;
if (node->sockFd >= 0) {
udprecv_thread_args_t* args = (udprecv_thread_args_t*)malloc(sizeof(*args));
if (args) {
args->node = node;
args->sockFd = node->sockFd;
if (pthread_create(&node->recvThreadV6, NULL, UdpNode_RecvThreadProc, args) == 0) {
anyStarted = 1;
} else {
free(args);
}
}
}
if (node->sockFdV4 >= 0) {
udprecv_thread_args_t* args = (udprecv_thread_args_t*)malloc(sizeof(*args));
if (args) {
args->node = node;
args->sockFd = node->sockFdV4;
if (pthread_create(&node->recvThreadV4, NULL, UdpNode_RecvThreadProc, args) == 0) {
anyStarted = 1;
} else {
free(args);
}
}
}
if (pthread_create(&node->retryThread, NULL, UdpNode_RetryThreadProc, node) == 0) {
anyStarted = 1;
}
if (!anyStarted) {
node->isRunning = 0;
return -1;
}
return 0;
}
void UdpNode_Stop(udp_node_t* node) {
if (!node || !node->isRunning) {
return;
}
node->isRunning = 0;
// Close sockets to unblock recvfrom in receive threads
if (node->sockFd >= 0) {
int fd = node->sockFd;
node->sockFd = -1;
close(fd);
}
if (node->sockFdV4 >= 0) {
int fd = node->sockFdV4;
node->sockFdV4 = -1;
close(fd);
}
pthread_join(node->recvThreadV6, NULL);
pthread_join(node->recvThreadV4, NULL);
pthread_join(node->retryThread, NULL);
}
void UdpNode_Destroy(udp_node_t* node) {
if (!node) {
return;
}
if (node->sockFd >= 0) {
close(node->sockFd);
node->sockFd = -1;
}
if (node->sockFdV4 >= 0) {
close(node->sockFdV4);
node->sockFdV4 = -1;
}
pthread_mutex_destroy(&node->pingsMutex);
}
int UdpNode_SendPing(udp_node_t* node, const struct sockaddr_storage* dest) {
if (!node || !dest) {
return -1;
}
if (node->sockFd < 0 && node->sockFdV4 < 0) {
return -1;
}
uint64_t nonce = random_eight_byte();
uint64_t now = get_current_time_ms();
pthread_mutex_lock(&node->pingsMutex);
int slot = -1;
for (int i = 0; i < UDP_MAX_PENDING_PINGS; i++) {
if (!node->pendingPings[i].active) {
slot = i;
break;
}
}
if (slot < 0) {
pthread_mutex_unlock(&node->pingsMutex);
return -1;
}
node->pendingPings[slot].nonce = nonce;
node->pendingPings[slot].dest = *dest;
node->pendingPings[slot].lastSentMs = now;
node->pendingPings[slot].retries = 0;
node->pendingPings[slot].active = true;
pthread_mutex_unlock(&node->pingsMutex);
UdpNode_SendRawPing(node, nonce, dest);
return 0;
}