Initial commit: multi-peer torrent download engine
Reactor/loop-pool engine with TCP/µTP/MSE transports, per-connection pipelining, priority-driven piece selection with endgame, and the Python FFI test harness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
d8208685a2
55 changed files with 9989 additions and 0 deletions
385
src/proto.c
Normal file
385
src/proto.c
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* proto.c - BitTorrent peer wire protocol: handshake, message framing, and the
|
||||
* incremental push-model parser.
|
||||
*
|
||||
* The loop thread does the recv() and feeds the bytes here via proto_feed();
|
||||
* the parser is a resumable state machine that never blocks and never calls
|
||||
* recv() itself. A piece's payload is memcpy'd out of the fed buffer straight
|
||||
* into its destination arena slot, so the only copy on the hot path is that one
|
||||
* (unavoidable, since the bytes already live in the loop's recv staging buffer).
|
||||
*
|
||||
* Completed blocks are pushed to the owning loop's ready_ring and the engine's
|
||||
* shared ready eventfd is signalled so the single consumer wakes.
|
||||
*/
|
||||
#include "engine_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* pstrlen (19) followed by "BitTorrent protocol"; exactly 20 bytes, no NUL. */
|
||||
static const uint8_t BT_PROTOCOL[20] = {
|
||||
19, 'B','i','t','T','o','r','r','e','n','t',' ','p','r','o','t','o','c','o','l'
|
||||
};
|
||||
|
||||
static inline uint32_t be32(const uint8_t *p) {
|
||||
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
|
||||
((uint32_t)p[2] << 8) | (uint32_t)p[3];
|
||||
}
|
||||
static inline void put_be32(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;
|
||||
}
|
||||
|
||||
/* ---- outgoing message construction --------------------------------- */
|
||||
|
||||
void proto_queue_handshake(conn *c) {
|
||||
uint8_t hs[HANDSHAKE_LEN];
|
||||
memcpy(hs, BT_PROTOCOL, 20);
|
||||
memset(hs + 20, 0, 8); /* reserved */
|
||||
hs[25] |= 0x10; /* BEP-10 extension protocol */
|
||||
hs[27] |= 0x04; /* BEP-6 Fast Extension */
|
||||
memcpy(hs + 28, c->tor->info_hash, 20);
|
||||
memcpy(hs + 48, c->tor->peer_id, 20);
|
||||
outbuf_append(&c->out, hs, sizeof hs);
|
||||
}
|
||||
|
||||
void proto_queue_msg(conn *c, uint8_t id) {
|
||||
uint8_t m[5];
|
||||
put_be32(m, 1);
|
||||
m[4] = id;
|
||||
outbuf_append(&c->out, m, sizeof m);
|
||||
}
|
||||
|
||||
void proto_queue_request(conn *c, uint32_t piece, uint32_t begin, uint32_t length) {
|
||||
uint8_t m[17];
|
||||
put_be32(m, 13);
|
||||
m[4] = MSG_REQUEST;
|
||||
put_be32(m + 5, piece);
|
||||
put_be32(m + 9, begin);
|
||||
put_be32(m + 13, length);
|
||||
outbuf_append(&c->out, m, sizeof m);
|
||||
}
|
||||
|
||||
void proto_queue_cancel(conn *c, uint32_t piece, uint32_t begin, uint32_t length) {
|
||||
uint8_t m[17];
|
||||
put_be32(m, 13);
|
||||
m[4] = MSG_CANCEL;
|
||||
put_be32(m + 5, piece);
|
||||
put_be32(m + 9, begin);
|
||||
put_be32(m + 13, length);
|
||||
outbuf_append(&c->out, m, sizeof m);
|
||||
}
|
||||
|
||||
static void proto_queue_have_none(conn *c) {
|
||||
proto_queue_msg(c, MSG_HAVE_NONE);
|
||||
}
|
||||
|
||||
static void proto_queue_ext_handshake(conn *c) {
|
||||
char payload[160];
|
||||
int n = snprintf(payload, sizeof payload,
|
||||
"d1:md11:lt_donthavei%uee1:reqqi%ue1:v14:torrent-peer/0ee",
|
||||
EXT_LT_DONTHAVE, c->lp->eng->cfg.max_pipeline);
|
||||
if (n <= 0 || (size_t)n >= sizeof payload) return;
|
||||
|
||||
uint8_t hdr[6];
|
||||
put_be32(hdr, (uint32_t)n + 2);
|
||||
hdr[4] = MSG_EXTENDED;
|
||||
hdr[5] = 0; /* extended handshake */
|
||||
outbuf_append(&c->out, hdr, sizeof hdr);
|
||||
outbuf_append(&c->out, payload, (size_t)n);
|
||||
}
|
||||
|
||||
/* ---- fed-buffer reader --------------------------------------------- */
|
||||
|
||||
/* Copy up to (need - *got) bytes from the feed cursor [*p, end) into dst.
|
||||
* Advances the cursor. Returns 1 once *got reaches need, else 0 (need more). */
|
||||
static inline int feed_take(const uint8_t **p, const uint8_t *end, void *dst,
|
||||
size_t need, size_t *got) {
|
||||
size_t avail = (size_t)(end - *p);
|
||||
size_t n = need - *got;
|
||||
if (n > avail) n = avail;
|
||||
memcpy((uint8_t *)dst + *got, *p, n);
|
||||
*p += n;
|
||||
*got += n;
|
||||
return *got >= need;
|
||||
}
|
||||
|
||||
static void signal_ready(conn *c) {
|
||||
uint64_t one = 1;
|
||||
ssize_t w = write(c->lp->eng->ready_efd, &one, sizeof one);
|
||||
(void)w; /* eventfd write only fails on overflow; harmless */
|
||||
}
|
||||
|
||||
/* Endgame races a block across several peers; once one copy lands, cancel the
|
||||
* others so we don't pay to receive the same block twice. No-op outside endgame
|
||||
* (no two connections share an in-flight block then), so the hot path is clean. */
|
||||
static void cancel_redundant_copies(conn *src, uint32_t piece, uint32_t begin,
|
||||
uint32_t len) {
|
||||
torrent *t = src->tor;
|
||||
if (!t->endgame) return;
|
||||
uint64_t key = block_key(t, piece, begin);
|
||||
for (conn *o = src->lp->conns; o; o = o->next) {
|
||||
if (o == src || o->tor != t || o->dead) continue;
|
||||
req_entry e;
|
||||
if (reqtab_take(&o->inflight, key, &e)) {
|
||||
proto_queue_cancel(o, piece, begin, len);
|
||||
atomic_fetch_sub_explicit(&o->outstanding, 1, memory_order_relaxed);
|
||||
o->lp->outstanding--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void drop_inflight_piece(conn *c, uint32_t piece) {
|
||||
uint32_t removed = reqtab_take_piece(&c->inflight, piece);
|
||||
if (removed == 0) return;
|
||||
atomic_fetch_sub_explicit(&c->outstanding, removed, memory_order_relaxed);
|
||||
c->lp->outstanding -= removed;
|
||||
c->tor->requested[piece] = 0;
|
||||
if (c->have_cur_piece && c->cur_piece == piece) c->have_cur_piece = 0;
|
||||
}
|
||||
|
||||
static void handle_reject(conn *c, const uint8_t *payload, size_t len) {
|
||||
if (!c->fast_enabled || len < 12) {
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t piece = be32(payload);
|
||||
uint32_t begin = be32(payload + 4);
|
||||
if (piece >= c->tor->num_pieces) {
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
return;
|
||||
}
|
||||
|
||||
req_entry e;
|
||||
if (reqtab_take(&c->inflight, block_key(c->tor, piece, begin), &e)) {
|
||||
atomic_fetch_sub_explicit(&c->outstanding, 1, memory_order_relaxed);
|
||||
c->lp->outstanding--;
|
||||
c->tor->requested[piece] = 0;
|
||||
if (c->have_cur_piece && c->cur_piece == piece) c->have_cur_piece = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_extended(conn *c, const uint8_t *payload, size_t len) {
|
||||
if (!c->ext_enabled || len < 1) return;
|
||||
|
||||
uint8_t ext_id = payload[0];
|
||||
if (ext_id == 0) return; /* handshake: no fields needed */
|
||||
if (ext_id != EXT_LT_DONTHAVE || len < 5) return;
|
||||
|
||||
uint32_t piece = be32(payload + 1);
|
||||
if (piece >= c->tor->num_pieces) return;
|
||||
clear_have_bit(c->have_bits, piece);
|
||||
clear_have_bit(c->allowed_fast_bits, piece);
|
||||
drop_inflight_piece(c, piece);
|
||||
}
|
||||
|
||||
/* A fully-received non-piece control message (payload after the id). have_bits
|
||||
* is loop-thread-only, so the selector reads it from the same thread. */
|
||||
static void handle_control(conn *c, uint8_t id, const uint8_t *payload, size_t len) {
|
||||
switch (id) {
|
||||
case MSG_UNCHOKE:
|
||||
c->unchoked = 1;
|
||||
atomic_store_explicit(&c->astate, PEER_STATE_RUNNING, memory_order_relaxed);
|
||||
break;
|
||||
case MSG_CHOKE:
|
||||
c->unchoked = 0;
|
||||
atomic_store_explicit(&c->astate, PEER_STATE_CHOKED, memory_order_relaxed);
|
||||
break;
|
||||
case MSG_BITFIELD: {
|
||||
size_t n = len < c->tor->bf_bytes ? len : c->tor->bf_bytes;
|
||||
memcpy(c->have_bits, payload, n);
|
||||
break;
|
||||
}
|
||||
case MSG_HAVE:
|
||||
if (len >= 4) {
|
||||
uint32_t idx = be32(payload);
|
||||
if (idx < c->tor->num_pieces) set_have_bit(c->have_bits, idx);
|
||||
}
|
||||
break;
|
||||
case MSG_SUGGEST:
|
||||
if (!c->fast_enabled || len < 4) conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
break;
|
||||
case MSG_HAVE_ALL:
|
||||
if (!c->fast_enabled) {
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
break;
|
||||
}
|
||||
set_all_have_bits(c->have_bits, c->tor->num_pieces);
|
||||
break;
|
||||
case MSG_HAVE_NONE:
|
||||
if (!c->fast_enabled) {
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
break;
|
||||
}
|
||||
memset(c->have_bits, 0, c->tor->bf_bytes);
|
||||
memset(c->allowed_fast_bits, 0, c->tor->bf_bytes);
|
||||
break;
|
||||
case MSG_REJECT:
|
||||
handle_reject(c, payload, len);
|
||||
break;
|
||||
case MSG_ALLOWED_FAST:
|
||||
if (!c->fast_enabled || len < 4) {
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
break;
|
||||
}
|
||||
{
|
||||
uint32_t idx = be32(payload);
|
||||
if (idx < c->tor->num_pieces) set_have_bit(c->allowed_fast_bits, idx);
|
||||
}
|
||||
break;
|
||||
case MSG_EXTENDED:
|
||||
handle_extended(c, payload, len);
|
||||
break;
|
||||
default:
|
||||
/* not-interested/port/fast-extension/etc: ignored by a leech. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void conn_set_error(conn *c, peer_error e) {
|
||||
atomic_store_explicit(&c->aerror, (int)e, memory_order_relaxed);
|
||||
atomic_store_explicit(&c->astate, PEER_STATE_ERROR, memory_order_relaxed);
|
||||
}
|
||||
|
||||
/* ---- the parser ----------------------------------------------------- */
|
||||
|
||||
int proto_feed(conn *c, const uint8_t *data, size_t len) {
|
||||
reader *r = &c->rd;
|
||||
torrent *tor = c->tor;
|
||||
loop *lp = c->lp;
|
||||
const uint8_t *p = data;
|
||||
const uint8_t *end = data + len;
|
||||
int pushed = 0;
|
||||
|
||||
while (p < end) {
|
||||
switch (r->state) {
|
||||
case RS_HANDSHAKE:
|
||||
if (!feed_take(&p, end, r->hs, HANDSHAKE_LEN, &r->hs_got)) goto out;
|
||||
if (memcmp(r->hs, BT_PROTOCOL, 20) != 0 ||
|
||||
memcmp(r->hs + 28, tor->info_hash, 20) != 0) {
|
||||
conn_set_error(c, PEER_ERR_HANDSHAKE);
|
||||
return -1;
|
||||
}
|
||||
c->fast_enabled = (r->hs[27] & 0x04) != 0;
|
||||
c->ext_enabled = (r->hs[25] & 0x10) != 0;
|
||||
if (c->fast_enabled) proto_queue_have_none(c);
|
||||
if (c->ext_enabled) proto_queue_ext_handshake(c);
|
||||
/* Express interest; wait for unchoke or Allowed Fast. */
|
||||
c->bt_established = 1;
|
||||
proto_queue_msg(c, MSG_INTERESTED);
|
||||
atomic_store_explicit(&c->astate, PEER_STATE_CHOKED, memory_order_relaxed);
|
||||
r->state = RS_LEN;
|
||||
r->len_got = 0;
|
||||
break;
|
||||
|
||||
case RS_LEN:
|
||||
if (!feed_take(&p, end, r->lenb, 4, &r->len_got)) goto out;
|
||||
r->msg_len = be32(r->lenb);
|
||||
r->len_got = 0;
|
||||
if (r->msg_len == 0) break; /* keep-alive */
|
||||
r->id_got = 0;
|
||||
r->state = RS_ID;
|
||||
break;
|
||||
|
||||
case RS_ID:
|
||||
if (!feed_take(&p, end, &r->msg_id, 1, &r->id_got)) goto out;
|
||||
if (r->msg_id == MSG_PIECE) {
|
||||
if (r->msg_len < 1 + PIECE_HDR_LEN ||
|
||||
r->msg_len - 1 - PIECE_HDR_LEN > PEER_BLOCK_SIZE) {
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
return -1;
|
||||
}
|
||||
r->body_len = r->msg_len - 1 - PIECE_HDR_LEN;
|
||||
r->phdr_got = 0;
|
||||
r->state = RS_PIECE_HDR;
|
||||
} else {
|
||||
uint32_t payload = r->msg_len - 1;
|
||||
if (payload == 0) {
|
||||
handle_control(c, r->msg_id, NULL, 0);
|
||||
if (atomic_load_explicit(&c->aerror, memory_order_relaxed) != PEER_OK)
|
||||
return -1;
|
||||
r->state = RS_LEN;
|
||||
} else {
|
||||
if (payload > r->other_cap) {
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
return -1;
|
||||
}
|
||||
r->other_total = payload;
|
||||
r->other_got = 0;
|
||||
r->state = RS_OTHER;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case RS_PIECE_HDR: {
|
||||
if (!feed_take(&p, end, r->phdr, PIECE_HDR_LEN, &r->phdr_got)) goto out;
|
||||
r->cur_piece = be32(r->phdr);
|
||||
r->cur_begin = be32(r->phdr + 4);
|
||||
/* C2: only accept a block we actually requested; otherwise drain and
|
||||
* drop it without consuming a slot or disturbing credit. */
|
||||
req_entry e;
|
||||
if (!reqtab_take(&c->inflight, block_key(tor, r->cur_piece, r->cur_begin), &e)) {
|
||||
r->other_total = r->body_len; /* <= block size <= other_cap */
|
||||
r->other_got = 0;
|
||||
r->state = RS_PIECE_DROP;
|
||||
break;
|
||||
}
|
||||
uint64_t rtt = peer_now_ns() - e.issue_ns;
|
||||
if (c->rtt_min_ns == 0 || rtt < c->rtt_min_ns) c->rtt_min_ns = rtt;
|
||||
atomic_fetch_sub_explicit(&c->outstanding, 1, memory_order_relaxed);
|
||||
lp->outstanding--;
|
||||
|
||||
if (!slot_ring_pop(&lp->free_ring, &r->cur_slot)) {
|
||||
/* Flow control guarantees a free slot here. */
|
||||
conn_set_error(c, PEER_ERR_PROTOCOL);
|
||||
return -1;
|
||||
}
|
||||
r->cur_slot_ptr = lp->arena + (uint64_t)r->cur_slot * PEER_BLOCK_SIZE;
|
||||
r->body_got = 0;
|
||||
r->state = RS_PIECE_BODY;
|
||||
break;
|
||||
}
|
||||
|
||||
case RS_PIECE_BODY: {
|
||||
size_t avail = (size_t)(end - p);
|
||||
size_t n = r->body_len - r->body_got;
|
||||
if (n > avail) n = avail;
|
||||
memcpy(r->cur_slot_ptr + r->body_got, p, n);
|
||||
p += n;
|
||||
r->body_got += n;
|
||||
if (r->body_got < r->body_len) goto out;
|
||||
engine_block d = { tor->id, r->cur_piece, r->cur_begin, r->body_len,
|
||||
(uint32_t)lp->index, r->cur_slot };
|
||||
/* ready_ring capacity >= num_slots, so this cannot fail. */
|
||||
desc_ring_push(&lp->ready_ring, d);
|
||||
torrent_mark_received(tor, r->cur_piece, r->cur_begin);
|
||||
cancel_redundant_copies(c, r->cur_piece, r->cur_begin, r->body_len);
|
||||
atomic_fetch_add_explicit(&c->bytes_received, r->body_len,
|
||||
memory_order_relaxed);
|
||||
atomic_fetch_add_explicit(&c->blocks_received, 1, memory_order_relaxed);
|
||||
pushed = 1;
|
||||
r->state = RS_LEN;
|
||||
break;
|
||||
}
|
||||
|
||||
case RS_PIECE_DROP:
|
||||
if (!feed_take(&p, end, r->other, r->other_total, &r->other_got)) goto out;
|
||||
r->state = RS_LEN;
|
||||
break;
|
||||
|
||||
case RS_OTHER:
|
||||
if (!feed_take(&p, end, r->other, r->other_total, &r->other_got)) goto out;
|
||||
handle_control(c, r->msg_id, r->other, r->other_total);
|
||||
if (atomic_load_explicit(&c->aerror, memory_order_relaxed) != PEER_OK)
|
||||
return -1;
|
||||
r->state = RS_LEN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
if (pushed) signal_ready(c);
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue