Naut/src/discovery/tracker_client.c
ookami125 096535292d swarm: re-announce promptly when the swarm is starved
The peer top-up loop re-announced to trackers only at the full advertised
interval (~30 min), so a torrent that dropped to a handful of peers would
sit there for up to half an hour with only the 5-minute DHT refresh to
help — looking like the re-announce system was dead.

Now, when fewer than LOW_PEER_THRESHOLD (10) peers are connected, the next
tracker announce is scheduled at the tracker's min_interval floor (never
below 60s) instead of the full interval, so a thin swarm actually tries to
recover. Once peers recover the full interval is used again. Capture the
tracker's min_interval (was being dropped) to stay announce-compliant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:04:24 -04:00

258 lines
9.1 KiB
C

/* tracker_client.c — HTTP/UDP tracker announce client.
*
* The wire codec (query building, bencode/UDP packet encode+decode) comes from
* the sibling `torrent-tracker` library; this file owns only the socket glue and
* the conversion between Naut's announce types and torrent-tracker's. */
#include "naut/tracker.h"
#include "naut/log.h"
#include "tracker.h" /* torrent-tracker public ABI */
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/time.h>
#define TRACKER_RESPONSE_MAX (16u << 20)
void naut_tracker_response_free(naut_tracker_response *r) {
if (!r) return;
free(r->peers);
free(r->failure);
r->peers = NULL;
r->failure = NULL;
r->num_peers = 0;
}
/* naut_announce_req -> torrent-tracker request (compact IPv4 announce). */
static void to_tracker_request(const naut_announce_req *req,
tracker_announce_request *out) {
memset(out, 0, sizeof *out);
memcpy(out->info_hash, req->info_hash, 20);
memcpy(out->peer_id, req->peer_id, 20);
out->port = req->port;
out->uploaded = req->uploaded;
out->downloaded = req->downloaded;
out->left = req->left;
out->numwant = req->numwant;
out->key = req->key;
out->has_key = 1;
out->compact = 1;
out->event = (tracker_event)req->event; /* codes match BEP-15 */
}
/* Copy torrent-tracker IPv4 peers into a freshly malloc'd naut_peer_addr array. */
static naut_err collect_peers(const tracker_peer *peers, size_t count,
const tracker_announce_response *resp,
naut_tracker_response *out) {
out->interval = (int32_t)resp->interval;
out->min_interval = (int32_t)resp->min_interval;
out->seeders = (int32_t)resp->complete;
out->leechers = (int32_t)resp->incomplete;
out->peers = NULL;
out->num_peers = 0;
if (count == 0) return NAUT_OK;
naut_peer_addr *v = malloc(count * sizeof *v);
if (!v) return NAUT_ERR_NOMEM;
size_t n = 0;
for (size_t i = 0; i < count; i++) {
if (peers[i].family != TRACKER_ADDR_IPV4) continue; /* IPv4 only */
memcpy(v[n].ip, peers[i].addr, 4);
v[n].port = peers[i].port;
n++;
}
out->peers = v;
out->num_peers = n;
return NAUT_OK;
}
size_t naut_tracker_http_url(const char *base, const naut_announce_req *req,
char *out, size_t outsz) {
tracker_announce_request treq;
to_tracker_request(req, &treq);
char query[2048];
size_t qlen = 0;
if (tracker_http_write_announce_query(&treq, query, sizeof query, &qlen) !=
TRACKER_OK)
return 0;
const char sep = strchr(base, '?') ? '&' : '?';
int n = snprintf(out, outsz, "%s%c%.*s", base, sep, (int)qlen, query);
if (n < 0 || (size_t)n >= outsz) return 0;
return (size_t)n;
}
/* --- HTTP --------------------------------------------------------------- */
static int dial(const char *host, const char *port, int socktype) {
struct addrinfo hints, *res = NULL, *ai;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET; /* IPv4 (compact peers are v4) */
hints.ai_socktype = socktype;
if (getaddrinfo(host, port, &hints, &res) != 0) return -1;
int fd = -1;
for (ai = res; ai; ai = ai->ai_next) {
fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd < 0) continue;
struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
close(fd); fd = -1;
}
freeaddrinfo(res);
return fd;
}
/* split "http://host[:port]/path" */
static bool parse_http_url(const char *url, char *host, size_t hostsz,
char *port, size_t portsz, const char **path) {
if (strncmp(url, "http://", 7) != 0) return false;
const char *h = url + 7;
const char *slash = strchr(h, '/');
const char *hostend = slash ? slash : h + strlen(h);
const char *colon = memchr(h, ':', (size_t)(hostend - h));
size_t hlen = colon ? (size_t)(colon - h) : (size_t)(hostend - h);
if (hlen >= hostsz) return false;
memcpy(host, h, hlen); host[hlen] = 0;
if (colon) {
size_t plen = (size_t)(hostend - colon - 1);
if (plen >= portsz) return false;
memcpy(port, colon + 1, plen); port[plen] = 0;
} else { snprintf(port, portsz, "80"); }
*path = slash ? slash : "/";
return true;
}
static bool write_all(int fd, const void *data, size_t len) {
const uint8_t *p = data;
while (len) {
ssize_t n = write(fd, p, len);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
p += (size_t)n;
len -= (size_t)n;
}
return true;
}
naut_err naut_tracker_announce_http(const char *url, naut_tracker_response *out) {
char host[256], port[16]; const char *path;
if (!parse_http_url(url, host, sizeof host, port, sizeof port, &path))
return NAUT_ERR_INVAL;
int fd = dial(host, port, SOCK_STREAM);
if (fd < 0) { NAUT_WARN("tracker connect %s:%s failed", host, port); return NAUT_ERR_IO; }
char req[4096];
int rn = snprintf(req, sizeof req,
"GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: Naut/0.1\r\nAccept: */*\r\n\r\n",
path, host);
if (rn < 0 || (size_t)rn >= sizeof req ||
!write_all(fd, req, (size_t)rn)) {
close(fd);
return NAUT_ERR_IO;
}
/* read whole response (server closes on HTTP/1.0) */
size_t cap = 1 << 16, len = 0;
uint8_t *buf = malloc(cap);
if (!buf) { close(fd); return NAUT_ERR_NOMEM; }
naut_err read_error = NAUT_OK;
for (;;) {
if (len == cap) {
if (cap == TRACKER_RESPONSE_MAX) { read_error = NAUT_ERR_FULL; break; }
size_t next_cap = NAUT_MIN(cap * 2, (size_t)TRACKER_RESPONSE_MAX);
uint8_t *next = realloc(buf, next_cap);
if (!next) { read_error = NAUT_ERR_NOMEM; break; }
buf = next;
cap = next_cap;
}
ssize_t r = read(fd, buf + len, cap - len);
if (r < 0) {
if (errno == EINTR) continue;
read_error = NAUT_ERR_IO;
break;
}
if (r == 0) break;
len += (size_t)r;
}
close(fd);
if (read_error != NAUT_OK) { free(buf); return read_error; }
/* find body after CRLFCRLF */
uint8_t *body = NULL; size_t blen = 0;
for (size_t i = 0; i + 3 < len; i++)
if (buf[i]=='\r'&&buf[i+1]=='\n'&&buf[i+2]=='\r'&&buf[i+3]=='\n') {
body = buf + i + 4; blen = len - (i + 4); break;
}
bool ok = len >= 12 && memcmp(buf, "HTTP/", 5) == 0 && buf[9] == '2';
if (!ok || !body) { free(buf); return NAUT_ERR_PROTO; }
tracker_peer peers[TRACKER_MAX_PEERS];
tracker_announce_response resp;
memset(&resp, 0, sizeof resp);
naut_err e = NAUT_ERR_PROTO;
if (tracker_http_parse_announce_response(body, blen, peers,
TRACKER_MAX_PEERS, &resp) ==
TRACKER_OK)
e = collect_peers(resp.peers, resp.peer_count, &resp, out);
free(buf);
return e;
}
/* --- UDP (BEP-15) ------------------------------------------------------- */
naut_err naut_tracker_announce_udp(const char *host, uint16_t port,
const naut_announce_req *req,
naut_tracker_response *out) {
char portstr[16]; snprintf(portstr, sizeof portstr, "%u", port);
int fd = dial(host, portstr, SOCK_DGRAM);
if (fd < 0) return NAUT_ERR_IO;
srand((unsigned)time(NULL) ^ (unsigned)getpid());
uint32_t txid = (uint32_t)rand();
uint8_t pkt[128], resp[2048];
size_t written = 0;
if (tracker_udp_write_connect_request(txid, pkt, sizeof pkt, &written) !=
TRACKER_OK ||
!write_all(fd, pkt, written)) {
close(fd); return NAUT_ERR_IO;
}
ssize_t r = read(fd, resp, sizeof resp);
uint64_t cid = 0;
if (r < 0 ||
tracker_udp_parse_connect_response(resp, (size_t)r, txid, &cid) !=
TRACKER_OK) {
close(fd); return NAUT_ERR_IO;
}
txid++;
tracker_announce_request treq;
to_tracker_request(req, &treq);
if (tracker_udp_write_announce_request(cid, txid, &treq, pkt, sizeof pkt,
&written) != TRACKER_OK ||
!write_all(fd, pkt, written)) {
close(fd); return NAUT_ERR_IO;
}
r = read(fd, resp, sizeof resp);
naut_err e = NAUT_ERR_IO;
if (r >= 0) {
tracker_peer peers[TRACKER_MAX_PEERS];
tracker_announce_response tresp;
memset(&tresp, 0, sizeof tresp);
e = NAUT_ERR_PROTO;
if (tracker_udp_parse_announce_response(resp, (size_t)r, txid,
TRACKER_ADDR_IPV4, peers,
TRACKER_MAX_PEERS, &tresp) ==
TRACKER_OK)
e = collect_peers(tresp.peers, tresp.peer_count, &tresp, out);
}
close(fd);
return e;
}