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
227
src/dht/dht.c
Normal file
227
src/dht/dht.c
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
#include "naut/dht.h"
|
||||
#include "naut/bencode.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static naut_err finish(naut_bc_writer *w, uint8_t **out, size_t *out_len) {
|
||||
if (w->err != NAUT_OK) {
|
||||
naut_err e = w->err;
|
||||
naut_bc_w_free(w);
|
||||
return e;
|
||||
}
|
||||
*out = w->buf;
|
||||
*out_len = w->len;
|
||||
w->buf = NULL;
|
||||
naut_bc_w_free(w);
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
static bool valid_common(const uint8_t *tx, size_t tx_len,
|
||||
const uint8_t id[20], uint8_t **out, size_t *out_len) {
|
||||
return tx && tx_len > 0 && tx_len <= 8 && id && out && out_len;
|
||||
}
|
||||
|
||||
naut_err naut_dht_build_ping(const uint8_t *tx, size_t tx_len,
|
||||
const uint8_t id[20],
|
||||
uint8_t **out, size_t *out_len) {
|
||||
if (!valid_common(tx, tx_len, id, out, out_len)) return NAUT_ERR_INVAL;
|
||||
naut_bc_writer w; naut_bc_w_init(&w);
|
||||
naut_bc_w_dict_begin(&w);
|
||||
naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w);
|
||||
naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20);
|
||||
naut_bc_w_end(&w);
|
||||
naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, "ping");
|
||||
naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len);
|
||||
naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q");
|
||||
naut_bc_w_end(&w);
|
||||
return finish(&w, out, out_len);
|
||||
}
|
||||
|
||||
static naut_err build_target_query(const char *query, const char *target_key,
|
||||
const uint8_t *tx, size_t tx_len,
|
||||
const uint8_t id[20],
|
||||
const uint8_t target[20],
|
||||
uint8_t **out, size_t *out_len) {
|
||||
if (!query || !target_key || !target ||
|
||||
!valid_common(tx, tx_len, id, out, out_len)) return NAUT_ERR_INVAL;
|
||||
naut_bc_writer w; naut_bc_w_init(&w);
|
||||
naut_bc_w_dict_begin(&w);
|
||||
naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w);
|
||||
naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20);
|
||||
naut_bc_w_cstr(&w, target_key); naut_bc_w_bytes(&w, target, 20);
|
||||
naut_bc_w_end(&w);
|
||||
naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, query);
|
||||
naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len);
|
||||
naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q");
|
||||
naut_bc_w_end(&w);
|
||||
return finish(&w, out, out_len);
|
||||
}
|
||||
|
||||
naut_err naut_dht_build_find_node(const uint8_t *tx, size_t tx_len,
|
||||
const uint8_t id[20],
|
||||
const uint8_t target[20],
|
||||
uint8_t **out, size_t *out_len) {
|
||||
return build_target_query("find_node", "target", tx, tx_len, id, target,
|
||||
out, out_len);
|
||||
}
|
||||
|
||||
naut_err naut_dht_build_get_peers(const uint8_t *tx, size_t tx_len,
|
||||
const uint8_t id[20],
|
||||
const uint8_t info_hash[20],
|
||||
uint8_t **out, size_t *out_len) {
|
||||
return build_target_query("get_peers", "info_hash", tx, tx_len, id,
|
||||
info_hash, out, out_len);
|
||||
}
|
||||
|
||||
naut_err naut_dht_build_announce_peer(const uint8_t *tx, size_t tx_len,
|
||||
const uint8_t id[20],
|
||||
const uint8_t info_hash[20],
|
||||
uint16_t port, bool implied_port,
|
||||
const void *token, size_t token_len,
|
||||
uint8_t **out, size_t *out_len) {
|
||||
if (!valid_common(tx, tx_len, id, out, out_len) || !info_hash ||
|
||||
!token || token_len == 0 || token_len > 64 || (!implied_port && port == 0))
|
||||
return NAUT_ERR_INVAL;
|
||||
naut_bc_writer w; naut_bc_w_init(&w);
|
||||
naut_bc_w_dict_begin(&w);
|
||||
naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w);
|
||||
naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20);
|
||||
naut_bc_w_cstr(&w, "implied_port"); naut_bc_w_int(&w, implied_port ? 1 : 0);
|
||||
naut_bc_w_cstr(&w, "info_hash"); naut_bc_w_bytes(&w, info_hash, 20);
|
||||
naut_bc_w_cstr(&w, "port"); naut_bc_w_int(&w, port);
|
||||
naut_bc_w_cstr(&w, "token"); naut_bc_w_bytes(&w, token, token_len);
|
||||
naut_bc_w_end(&w);
|
||||
naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, "announce_peer");
|
||||
naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len);
|
||||
naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q");
|
||||
naut_bc_w_end(&w);
|
||||
return finish(&w, out, out_len);
|
||||
}
|
||||
|
||||
static naut_err parse_nodes(const uint8_t *p, size_t n,
|
||||
naut_dht_node **out, size_t *count) {
|
||||
if (n % 26 != 0 || n / 26 > NAUT_DHT_MAX_NODES) return NAUT_ERR_PROTO;
|
||||
size_t num = n / 26;
|
||||
naut_dht_node *nodes = calloc(num ? num : 1, sizeof(*nodes));
|
||||
if (!nodes) return NAUT_ERR_NOMEM;
|
||||
for (size_t i = 0; i < num; i++) {
|
||||
const uint8_t *entry = p + i * 26;
|
||||
memcpy(nodes[i].id, entry, 20);
|
||||
memcpy(nodes[i].ip, entry + 20, 4);
|
||||
nodes[i].port = ((uint16_t)entry[24] << 8) | entry[25];
|
||||
if (nodes[i].port == 0) {
|
||||
free(nodes);
|
||||
return NAUT_ERR_PROTO;
|
||||
}
|
||||
}
|
||||
*out = nodes;
|
||||
*count = num;
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
static bool peer_duplicate(const naut_peer_addr *peers, size_t n,
|
||||
const naut_peer_addr *candidate) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (peers[i].port == candidate->port &&
|
||||
memcmp(peers[i].ip, candidate->ip, 4) == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static naut_err parse_values(const naut_bc *values,
|
||||
naut_peer_addr **out, size_t *count) {
|
||||
if (!values || values->type != NAUT_BC_LIST ||
|
||||
values->v.list.count > NAUT_DHT_MAX_PEERS) return NAUT_ERR_PROTO;
|
||||
naut_peer_addr *peers = calloc(values->v.list.count ? values->v.list.count : 1,
|
||||
sizeof(*peers));
|
||||
if (!peers) return NAUT_ERR_NOMEM;
|
||||
size_t num = 0;
|
||||
for (size_t i = 0; i < values->v.list.count; i++) {
|
||||
const uint8_t *p; size_t n;
|
||||
if (!naut_bc_get_str(naut_bc_list_at(values, i), &p, &n) || n != 6) {
|
||||
free(peers);
|
||||
return NAUT_ERR_PROTO;
|
||||
}
|
||||
naut_peer_addr peer;
|
||||
memcpy(peer.ip, p, 4);
|
||||
peer.port = ((uint16_t)p[4] << 8) | p[5];
|
||||
if (peer.port && !peer_duplicate(peers, num, &peer))
|
||||
peers[num++] = peer;
|
||||
}
|
||||
*out = peers;
|
||||
*count = num;
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
naut_err naut_dht_parse_response(const uint8_t *data, size_t len,
|
||||
naut_dht_response *out) {
|
||||
if (!data || !out) return NAUT_ERR_INVAL;
|
||||
memset(out, 0, sizeof(*out));
|
||||
naut_bc_doc *doc = NULL;
|
||||
naut_err err = naut_bc_parse(data, len, &doc);
|
||||
if (err != NAUT_OK) return err;
|
||||
const naut_bc *root = naut_bc_root(doc);
|
||||
const uint8_t *p; size_t n;
|
||||
if (!root || root->type != NAUT_BC_DICT ||
|
||||
!naut_bc_get_str(naut_bc_dict_get(root, "t"), &p, &n) ||
|
||||
n == 0 || n > sizeof out->transaction) {
|
||||
err = NAUT_ERR_PROTO;
|
||||
goto done;
|
||||
}
|
||||
memcpy(out->transaction, p, n);
|
||||
out->transaction_len = n;
|
||||
const naut_bc *y = naut_bc_dict_get(root, "y");
|
||||
if (naut_bc_str_eq(y, "e")) {
|
||||
const naut_bc *e = naut_bc_dict_get(root, "e");
|
||||
int64_t code;
|
||||
if (!e || e->type != NAUT_BC_LIST || e->v.list.count < 1 ||
|
||||
!naut_bc_get_int(naut_bc_list_at(e, 0), &code)) {
|
||||
err = NAUT_ERR_PROTO;
|
||||
goto done;
|
||||
}
|
||||
out->type = NAUT_DHT_ERROR;
|
||||
out->error_code = (int)code;
|
||||
goto done;
|
||||
}
|
||||
if (!naut_bc_str_eq(y, "r")) {
|
||||
err = NAUT_ERR_PROTO;
|
||||
goto done;
|
||||
}
|
||||
out->type = NAUT_DHT_RESPONSE;
|
||||
const naut_bc *r = naut_bc_dict_get(root, "r");
|
||||
if (!r || r->type != NAUT_BC_DICT) {
|
||||
err = NAUT_ERR_PROTO;
|
||||
goto done;
|
||||
}
|
||||
if (naut_bc_get_str(naut_bc_dict_get(r, "id"), &p, &n)) {
|
||||
if (n != 20) { err = NAUT_ERR_PROTO; goto done; }
|
||||
memcpy(out->id, p, 20);
|
||||
out->has_id = true;
|
||||
}
|
||||
if (naut_bc_get_str(naut_bc_dict_get(r, "token"), &p, &n)) {
|
||||
if (n == 0 || n > sizeof out->token) { err = NAUT_ERR_PROTO; goto done; }
|
||||
memcpy(out->token, p, n);
|
||||
out->token_len = n;
|
||||
}
|
||||
if (naut_bc_get_str(naut_bc_dict_get(r, "nodes"), &p, &n)) {
|
||||
err = parse_nodes(p, n, &out->nodes, &out->num_nodes);
|
||||
if (err != NAUT_OK) goto done;
|
||||
}
|
||||
const naut_bc *values = naut_bc_dict_get(r, "values");
|
||||
if (values) {
|
||||
err = parse_values(values, &out->peers, &out->num_peers);
|
||||
if (err != NAUT_OK) goto done;
|
||||
}
|
||||
done:
|
||||
naut_bc_free(doc);
|
||||
if (err != NAUT_OK) naut_dht_response_free(out);
|
||||
return err;
|
||||
}
|
||||
|
||||
void naut_dht_response_free(naut_dht_response *response) {
|
||||
if (!response) return;
|
||||
free(response->nodes);
|
||||
free(response->peers);
|
||||
memset(response, 0, sizeof(*response));
|
||||
}
|
||||
154
src/dht/fetch.c
Normal file
154
src/dht/fetch.c
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#include "naut/dht.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
#include <poll.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
struct sockaddr_in addr;
|
||||
bool queried;
|
||||
} candidate;
|
||||
|
||||
static bool parse_endpoint(const char *text, struct sockaddr_in *out) {
|
||||
const char *colon = strrchr(text, ':');
|
||||
if (!colon || colon == text) return false;
|
||||
char host[256], port[16];
|
||||
size_t host_len = (size_t)(colon - text);
|
||||
size_t port_len = strlen(colon + 1);
|
||||
if (host_len >= sizeof host || port_len == 0 || port_len >= sizeof port)
|
||||
return false;
|
||||
memcpy(host, text, host_len); host[host_len] = 0;
|
||||
memcpy(port, colon + 1, port_len + 1);
|
||||
struct addrinfo hints, *result = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
if (getaddrinfo(host, port, &hints, &result) != 0) return false;
|
||||
memcpy(out, result->ai_addr, sizeof(*out));
|
||||
freeaddrinfo(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool same_addr(const struct sockaddr_in *a, const struct sockaddr_in *b) {
|
||||
return a->sin_port == b->sin_port && a->sin_addr.s_addr == b->sin_addr.s_addr;
|
||||
}
|
||||
|
||||
static bool add_candidate(candidate *v, size_t *n, const struct sockaddr_in *addr) {
|
||||
if (addr->sin_port == 0) return true;
|
||||
for (size_t i = 0; i < *n; i++)
|
||||
if (same_addr(&v[i].addr, addr)) return true;
|
||||
if (*n == NAUT_DHT_MAX_NODES) return false;
|
||||
v[*n].addr = *addr;
|
||||
v[*n].queried = false;
|
||||
(*n)++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool add_peer(naut_peer_addr *v, size_t *n, const naut_peer_addr *peer) {
|
||||
for (size_t i = 0; i < *n; i++)
|
||||
if (v[i].port == peer->port && memcmp(v[i].ip, peer->ip, 4) == 0)
|
||||
return true;
|
||||
if (*n == NAUT_DHT_MAX_PEERS) return false;
|
||||
v[(*n)++] = *peer;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void node_id(uint8_t id[20]) {
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
size_t done = 0;
|
||||
while (done < 20) {
|
||||
ssize_t n = read(fd, id + done, 20 - done);
|
||||
if (n <= 0) break;
|
||||
done += (size_t)n;
|
||||
}
|
||||
close(fd);
|
||||
if (done == 20) return;
|
||||
}
|
||||
for (size_t i = 0; i < 20; i++) id[i] = (uint8_t)rand();
|
||||
}
|
||||
|
||||
naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap,
|
||||
const uint8_t info_hash[20],
|
||||
naut_peer_addr **peers, size_t *num_peers) {
|
||||
if (!bootstrap || num_bootstrap == 0 || !info_hash || !peers || !num_peers)
|
||||
return NAUT_ERR_INVAL;
|
||||
*peers = NULL; *num_peers = 0;
|
||||
candidate nodes[NAUT_DHT_MAX_NODES];
|
||||
size_t node_count = 0;
|
||||
for (size_t i = 0; i < num_bootstrap; i++) {
|
||||
struct sockaddr_in addr;
|
||||
if (parse_endpoint(bootstrap[i], &addr))
|
||||
add_candidate(nodes, &node_count, &addr);
|
||||
}
|
||||
if (node_count == 0) return NAUT_ERR_INVAL;
|
||||
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0) return NAUT_ERR_IO;
|
||||
naut_peer_addr found[NAUT_DHT_MAX_PEERS];
|
||||
size_t found_count = 0;
|
||||
uint8_t id[20];
|
||||
node_id(id);
|
||||
uint16_t tx_counter = 1;
|
||||
size_t queries = 0;
|
||||
|
||||
while (queries < 64 && found_count < NAUT_DHT_MAX_PEERS) {
|
||||
size_t index = SIZE_MAX;
|
||||
for (size_t i = 0; i < node_count; i++)
|
||||
if (!nodes[i].queried) { index = i; break; }
|
||||
if (index == SIZE_MAX) break;
|
||||
nodes[index].queried = true;
|
||||
queries++;
|
||||
uint8_t tx[2] = { (uint8_t)(tx_counter >> 8), (uint8_t)tx_counter };
|
||||
tx_counter++;
|
||||
uint8_t *query = NULL; size_t query_len = 0;
|
||||
if (naut_dht_build_get_peers(tx, sizeof tx, id, info_hash,
|
||||
&query, &query_len) != NAUT_OK)
|
||||
continue;
|
||||
ssize_t sent = sendto(fd, query, query_len, 0,
|
||||
(struct sockaddr *)&nodes[index].addr,
|
||||
sizeof(nodes[index].addr));
|
||||
free(query);
|
||||
if (sent < 0) continue;
|
||||
|
||||
struct pollfd pfd = { .fd = fd, .events = POLLIN };
|
||||
if (poll(&pfd, 1, 1000) <= 0) continue;
|
||||
uint8_t packet[65536];
|
||||
ssize_t received = recv(fd, packet, sizeof packet, 0);
|
||||
if (received <= 0) continue;
|
||||
naut_dht_response response;
|
||||
if (naut_dht_parse_response(packet, (size_t)received, &response) != NAUT_OK)
|
||||
continue;
|
||||
if (response.transaction_len != sizeof tx ||
|
||||
memcmp(response.transaction, tx, sizeof tx) != 0 ||
|
||||
response.type != NAUT_DHT_RESPONSE) {
|
||||
naut_dht_response_free(&response);
|
||||
continue;
|
||||
}
|
||||
for (size_t i = 0; i < response.num_peers; i++)
|
||||
add_peer(found, &found_count, &response.peers[i]);
|
||||
for (size_t i = 0; i < response.num_nodes; i++) {
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof addr);
|
||||
addr.sin_family = AF_INET;
|
||||
memcpy(&addr.sin_addr, response.nodes[i].ip, 4);
|
||||
addr.sin_port = htons(response.nodes[i].port);
|
||||
add_candidate(nodes, &node_count, &addr);
|
||||
}
|
||||
naut_dht_response_free(&response);
|
||||
}
|
||||
close(fd);
|
||||
if (found_count == 0) return NAUT_ERR_EMPTY;
|
||||
naut_peer_addr *result = malloc(found_count * sizeof(*result));
|
||||
if (!result) return NAUT_ERR_NOMEM;
|
||||
memcpy(result, found, found_count * sizeof(*result));
|
||||
*peers = result;
|
||||
*num_peers = found_count;
|
||||
return NAUT_OK;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue