Initial commit: Naut-Torrent — from-scratch 10 GbE BitTorrent client
A maintainable, extensible BitTorrent client (C11, Linux/io_uring) targeting 10 GbE saturation. All torrent functionality is built from scratch; liburing is the only linked third-party dependency on the data path. Implements Phases 1-7 of the roadmap: - core: page-aligned buffer pool, MPMC/Treiber queues, bitfields, worker pool - crypto: SHA-1/256 (SHA-NI + scalar), Merkle (BEP-52), RC4 (MSE) - bencode/metainfo: zero-copy parser, v1/v2/hybrid .torrent + magnet - peer: sans-IO wire codec, MSE/PE handshake state machine, BEP-10, ut_metadata, PEX - piece/storage: block-level multi-peer engine, rarest-first + endgame, per-file completion events + single-file relocate (move-as-you-finish) - tracker/dht: HTTP + UDP (BEP-15) trackers, BEP-5 KRPC iterative lookup - platform: io_uring reactor (SQPOLL, registered buffers, SEND_ZC) - surface: versioned RPC, native plugin ABI, sandboxed Lua scripting, nautd/nautctl Verified against libtorrent (single/multi/hybrid, MSE, magnet-via-DHT, swarm); unit + interop tests green; ASan/UBSan/TSan clean. Scripting reference in docs/scripting.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
2178d6a70c
121 changed files with 12644 additions and 0 deletions
162
src/tracker/fetch.c
Normal file
162
src/tracker/fetch.c
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
#include "naut/tracker.h"
|
||||
#include "naut/log.h"
|
||||
|
||||
#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)
|
||||
|
||||
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 for now (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[2048];
|
||||
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 success = len >= 12 && memcmp(buf, "HTTP/", 5) == 0 &&
|
||||
buf[9] == '2';
|
||||
naut_err e = success && body
|
||||
? naut_tracker_parse_http(body, blen, out)
|
||||
: NAUT_ERR_PROTO;
|
||||
free(buf);
|
||||
return e;
|
||||
}
|
||||
|
||||
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[98], resp[1500];
|
||||
naut_udp_build_connect(pkt, txid);
|
||||
if (write(fd, pkt, 16) != 16) { close(fd); return NAUT_ERR_IO; }
|
||||
ssize_t r = read(fd, resp, sizeof resp);
|
||||
uint64_t cid;
|
||||
if (r < 0 || naut_udp_parse_connect(resp, (size_t)r, txid, &cid) != NAUT_OK) {
|
||||
close(fd); return NAUT_ERR_IO;
|
||||
}
|
||||
txid++;
|
||||
naut_udp_build_announce(pkt, cid, txid, req);
|
||||
if (write(fd, pkt, 98) != 98) { close(fd); return NAUT_ERR_IO; }
|
||||
r = read(fd, resp, sizeof resp);
|
||||
naut_err e = (r < 0) ? NAUT_ERR_IO
|
||||
: naut_udp_parse_announce(resp, (size_t)r, txid, out);
|
||||
close(fd);
|
||||
return e;
|
||||
}
|
||||
120
src/tracker/tracker.c
Normal file
120
src/tracker/tracker.c
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#include "naut/tracker.h"
|
||||
#include "naut/bencode.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void naut_tracker_response_free(naut_tracker_response *r) {
|
||||
free(r->peers); r->peers = NULL; r->num_peers = 0;
|
||||
free(r->failure); r->failure = NULL;
|
||||
}
|
||||
|
||||
/* percent-encode raw bytes per RFC 3986 (unreserved chars pass through) */
|
||||
static size_t pct_encode(const uint8_t *in, size_t n, char *out, size_t outsz) {
|
||||
static const char hx[] = "0123456789ABCDEF";
|
||||
size_t o = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint8_t c = in[i];
|
||||
bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '-' || c == '_' ||
|
||||
c == '.' || c == '~';
|
||||
if (unreserved) {
|
||||
if (o + 1 >= outsz) return 0;
|
||||
out[o++] = (char)c;
|
||||
} else {
|
||||
if (o + 3 >= outsz) return 0;
|
||||
out[o++] = '%'; out[o++] = hx[c >> 4]; out[o++] = hx[c & 15];
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
size_t naut_tracker_http_url(const char *base, const naut_announce_req *req,
|
||||
char *out, size_t outsz) {
|
||||
static const char *ev[] = { "", "completed", "started", "stopped" };
|
||||
if (req->event < NAUT_TEV_NONE || req->event > NAUT_TEV_STOPPED) return 0;
|
||||
char ih[61], pid[61]; /* 20*3 = 60 worst case + NUL */
|
||||
size_t ihn = pct_encode(req->info_hash, 20, ih, sizeof ih);
|
||||
size_t pidn = pct_encode(req->peer_id, 20, pid, sizeof pid);
|
||||
if (!ihn || !pidn) return 0;
|
||||
ih[ihn] = 0; pid[pidn] = 0;
|
||||
|
||||
const char *sep = strchr(base, '?') ? "&" : "?";
|
||||
int n = snprintf(out, outsz,
|
||||
"%s%sinfo_hash=%s&peer_id=%s&port=%u&uploaded=%llu&downloaded=%llu"
|
||||
"&left=%llu&compact=1&numwant=%d%s%s&key=%u",
|
||||
base, sep, ih, pid, req->port,
|
||||
(unsigned long long)req->uploaded, (unsigned long long)req->downloaded,
|
||||
(unsigned long long)req->left, req->numwant < 0 ? 50 : req->numwant,
|
||||
req->event ? "&event=" : "", ev[req->event], req->key);
|
||||
if (n < 0 || (size_t)n >= outsz) return 0;
|
||||
return (size_t)n;
|
||||
}
|
||||
|
||||
static naut_err parse_peers(const naut_bc *peers, naut_tracker_response *out) {
|
||||
const uint8_t *p; size_t n;
|
||||
if (naut_bc_get_str(peers, &p, &n)) { /* compact: 6 bytes each */
|
||||
if (n % 6 != 0) return NAUT_ERR_PROTO;
|
||||
out->num_peers = n / 6;
|
||||
out->peers = calloc(out->num_peers ? out->num_peers : 1, sizeof(naut_peer_addr));
|
||||
if (!out->peers) return NAUT_ERR_NOMEM;
|
||||
for (size_t i = 0; i < out->num_peers; i++) {
|
||||
memcpy(out->peers[i].ip, p + i*6, 4);
|
||||
out->peers[i].port = ((uint16_t)p[i*6+4] << 8) | p[i*6+5];
|
||||
}
|
||||
return NAUT_OK;
|
||||
}
|
||||
if (peers && peers->type == NAUT_BC_LIST) { /* dict form */
|
||||
out->peers = calloc(peers->v.list.count ? peers->v.list.count : 1, sizeof(naut_peer_addr));
|
||||
if (!out->peers) return NAUT_ERR_NOMEM;
|
||||
for (size_t i = 0; i < peers->v.list.count; i++) {
|
||||
const naut_bc *pe = naut_bc_list_at(peers, i);
|
||||
const uint8_t *ips; size_t ipn; int64_t port;
|
||||
if (!naut_bc_get_str(naut_bc_dict_get(pe, "ip"), &ips, &ipn)) continue;
|
||||
if (!naut_bc_get_int(naut_bc_dict_get(pe, "port"), &port)) continue;
|
||||
unsigned a, b, c, dd;
|
||||
char tmp[64];
|
||||
if (ipn >= sizeof tmp) continue;
|
||||
memcpy(tmp, ips, ipn); tmp[ipn] = 0;
|
||||
if (sscanf(tmp, "%u.%u.%u.%u", &a, &b, &c, &dd) != 4) continue;
|
||||
if (a > 255 || b > 255 || c > 255 || dd > 255 ||
|
||||
port <= 0 || port > UINT16_MAX) continue;
|
||||
naut_peer_addr *pa = &out->peers[out->num_peers++];
|
||||
pa->ip[0]=(uint8_t)a; pa->ip[1]=(uint8_t)b; pa->ip[2]=(uint8_t)c; pa->ip[3]=(uint8_t)dd;
|
||||
pa->port = (uint16_t)port;
|
||||
}
|
||||
return NAUT_OK;
|
||||
}
|
||||
return NAUT_ERR_PROTO;
|
||||
}
|
||||
|
||||
naut_err naut_tracker_parse_http(const uint8_t *body, size_t len,
|
||||
naut_tracker_response *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->seeders = out->leechers = -1;
|
||||
naut_bc_doc *doc = NULL;
|
||||
naut_err e = naut_bc_parse(body, len, &doc);
|
||||
if (e != NAUT_OK) return e;
|
||||
const naut_bc *root = naut_bc_root(doc);
|
||||
|
||||
const uint8_t *fp; size_t fn;
|
||||
if (naut_bc_get_str(naut_bc_dict_get(root, "failure reason"), &fp, &fn)) {
|
||||
out->failure = malloc(fn + 1);
|
||||
if (out->failure) { memcpy(out->failure, fp, fn); out->failure[fn] = 0; }
|
||||
naut_bc_free(doc);
|
||||
return NAUT_ERR_PROTO; /* tracker reported failure */
|
||||
}
|
||||
|
||||
int64_t iv = 0;
|
||||
naut_bc_get_int(naut_bc_dict_get(root, "interval"), &iv);
|
||||
out->interval = (int32_t)iv;
|
||||
int64_t sc;
|
||||
if (naut_bc_get_int(naut_bc_dict_get(root, "complete"), &sc)) out->seeders = (int32_t)sc;
|
||||
if (naut_bc_get_int(naut_bc_dict_get(root, "incomplete"), &sc)) out->leechers = (int32_t)sc;
|
||||
|
||||
e = parse_peers(naut_bc_dict_get(root, "peers"), out);
|
||||
naut_bc_free(doc);
|
||||
if (e != NAUT_OK) { naut_tracker_response_free(out); return e; }
|
||||
return NAUT_OK;
|
||||
}
|
||||
81
src/tracker/udp.c
Normal file
81
src/tracker/udp.c
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#include "naut/tracker.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define UDP_PROTOCOL_ID 0x41727101980ULL /* BEP-15 magic */
|
||||
#define ACTION_CONNECT 0
|
||||
#define ACTION_ANNOUNCE 1
|
||||
#define ACTION_ERROR 3
|
||||
|
||||
static void wr16(uint8_t *p, uint16_t v) { p[0]=(uint8_t)(v>>8); p[1]=(uint8_t)v; }
|
||||
static void wr32(uint8_t *p, uint32_t v) {
|
||||
p[0]=(uint8_t)(v>>24); p[1]=(uint8_t)(v>>16); p[2]=(uint8_t)(v>>8); p[3]=(uint8_t)v;
|
||||
}
|
||||
static void wr64(uint8_t *p, uint64_t v) { wr32(p, (uint32_t)(v>>32)); wr32(p+4, (uint32_t)v); }
|
||||
static uint32_t rd32(const uint8_t *p) {
|
||||
return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|((uint32_t)p[2]<<8)|p[3];
|
||||
}
|
||||
static uint64_t rd64(const uint8_t *p) { return ((uint64_t)rd32(p)<<32) | rd32(p+4); }
|
||||
|
||||
void naut_udp_build_connect(uint8_t out[16], uint32_t txid) {
|
||||
wr64(out, UDP_PROTOCOL_ID);
|
||||
wr32(out + 8, ACTION_CONNECT);
|
||||
wr32(out + 12, txid);
|
||||
}
|
||||
|
||||
naut_err naut_udp_parse_connect(const uint8_t *in, size_t len, uint32_t txid,
|
||||
uint64_t *connection_id) {
|
||||
if (len < 16) return NAUT_ERR_PROTO;
|
||||
if (rd32(in) != ACTION_CONNECT) return NAUT_ERR_PROTO;
|
||||
if (rd32(in + 4) != txid) return NAUT_ERR_PROTO;
|
||||
*connection_id = rd64(in + 8);
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
void naut_udp_build_announce(uint8_t out[98], uint64_t connection_id,
|
||||
uint32_t txid, const naut_announce_req *req) {
|
||||
wr64(out + 0, connection_id);
|
||||
wr32(out + 8, ACTION_ANNOUNCE);
|
||||
wr32(out + 12, txid);
|
||||
memcpy(out + 16, req->info_hash, 20);
|
||||
memcpy(out + 36, req->peer_id, 20);
|
||||
wr64(out + 56, req->downloaded);
|
||||
wr64(out + 64, req->left);
|
||||
wr64(out + 72, req->uploaded);
|
||||
wr32(out + 80, (uint32_t)req->event);
|
||||
wr32(out + 84, 0); /* IP: 0 = source */
|
||||
wr32(out + 88, req->key);
|
||||
wr32(out + 92, (uint32_t)(req->numwant < 0 ? 50 : req->numwant));
|
||||
wr16(out + 96, req->port);
|
||||
}
|
||||
|
||||
naut_err naut_udp_parse_announce(const uint8_t *in, size_t len, uint32_t txid,
|
||||
naut_tracker_response *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->seeders = out->leechers = -1;
|
||||
if (len < 8) return NAUT_ERR_PROTO;
|
||||
uint32_t action = rd32(in);
|
||||
if (rd32(in + 4) != txid) return NAUT_ERR_PROTO;
|
||||
if (action == ACTION_ERROR) {
|
||||
size_t mn = len - 8;
|
||||
out->failure = malloc(mn + 1);
|
||||
if (out->failure) { memcpy(out->failure, in + 8, mn); out->failure[mn] = 0; }
|
||||
return NAUT_ERR_PROTO;
|
||||
}
|
||||
if (action != ACTION_ANNOUNCE || len < 20 || (len - 20) % 6 != 0)
|
||||
return NAUT_ERR_PROTO;
|
||||
out->interval = (int32_t)rd32(in + 8);
|
||||
out->leechers = (int32_t)rd32(in + 12);
|
||||
out->seeders = (int32_t)rd32(in + 16);
|
||||
|
||||
size_t avail = (len - 20) / 6;
|
||||
out->peers = calloc(avail ? avail : 1, sizeof(naut_peer_addr));
|
||||
if (!out->peers) return NAUT_ERR_NOMEM;
|
||||
for (size_t i = 0; i < avail; i++) {
|
||||
const uint8_t *p = in + 20 + i*6;
|
||||
memcpy(out->peers[i].ip, p, 4);
|
||||
out->peers[i].port = ((uint16_t)p[4] << 8) | p[5];
|
||||
}
|
||||
out->num_peers = avail;
|
||||
return NAUT_OK;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue