Naut/src/peer/mse.c
ookami125 2178d6a70c 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>
2026-06-15 12:12:00 -04:00

466 lines
16 KiB
C

#include "naut/mse.h"
#include "naut/hash.h"
#include <errno.h>
#include <openssl/bn.h>
#include <openssl/rand.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#define MSE_PAD_MAX 512
#define MSE_CRYPTO_RC4 2u
static const char DH_PRIME_HEX[] =
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC"
"74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF2"
"5F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A3621000000"
"0000090563";
static void wr16(uint8_t *p, uint16_t value) {
p[0] = (uint8_t)(value >> 8);
p[1] = (uint8_t)value;
}
static void wr32(uint8_t *p, uint32_t value) {
p[0] = (uint8_t)(value >> 24);
p[1] = (uint8_t)(value >> 16);
p[2] = (uint8_t)(value >> 8);
p[3] = (uint8_t)value;
}
static uint16_t rd16(const uint8_t *p) {
return ((uint16_t)p[0] << 8) | p[1];
}
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 void hash_parts(const char label[4],
const uint8_t *first, size_t first_len,
const uint8_t *second, size_t second_len,
uint8_t out[20]) {
naut_sha1_ctx sha;
naut_sha1_init(&sha);
naut_sha1_update(&sha, label, 4);
naut_sha1_update(&sha, first, first_len);
if (second && second_len) naut_sha1_update(&sha, second, second_len);
naut_sha1_final(&sha, out);
}
static void init_rc4(const uint8_t secret[NAUT_MSE_DH_LEN],
const uint8_t info_hash[20],
naut_mse_stream *stream) {
uint8_t key_a[20], key_b[20];
hash_parts("keyA", secret, NAUT_MSE_DH_LEN, info_hash, 20, key_a);
hash_parts("keyB", secret, NAUT_MSE_DH_LEN, info_hash, 20, key_b);
naut_rc4_init(&stream->send, key_a, sizeof key_a, 1024);
naut_rc4_init(&stream->recv, key_b, sizeof key_b, 1024);
memset(key_a, 0, sizeof key_a);
memset(key_b, 0, sizeof key_b);
}
/* ---- sans-IO handshake state machine ------------------------------------- */
enum {
PH_RECV_PUBKEY, /* waiting for the peer's 96-byte DH public key */
PH_SYNC_VC, /* scanning past PadB for the encrypted VC */
PH_RECV_SELECT, /* crypto_select + len(PadD) */
PH_RECV_PAD, /* PadD bytes (discarded) */
PH_RECV_HS, /* the peer's encrypted BitTorrent handshake */
};
struct naut_mse_handshake {
int phase;
naut_err err;
bool done;
uint8_t info_hash[20];
uint8_t peer_id[NAUT_PEERID_LEN];
uint64_t reserved;
/* DH state retained until the shared secret is computed. */
BN_CTX *ctx;
BIGNUM *prime;
BIGNUM *priv;
naut_mse_stream stream;
uint8_t expected_vc[8];
size_t vc_scanned;
size_t pad_remaining;
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN];
uint8_t out[256];
size_t out_len, out_off;
uint8_t in[1024];
size_t in_len;
};
static void dh_free(naut_mse_handshake *h) {
BN_CTX_free(h->ctx); h->ctx = NULL;
BN_free(h->prime); h->prime = NULL;
BN_clear_free(h->priv); h->priv = NULL;
}
/* Generate our private key and public value, writing the 96-byte public key
* into the outgoing buffer. Retains prime/priv/ctx for dh_complete(). */
static naut_err dh_begin(naut_mse_handshake *h) {
naut_err result = NAUT_ERR_IO;
BIGNUM *generator = BN_new();
BIGNUM *local = BN_new();
h->ctx = BN_CTX_new();
h->priv = BN_new();
if (!generator || !local || !h->ctx || !h->priv ||
!BN_hex2bn(&h->prime, DH_PRIME_HEX) || !BN_set_word(generator, 2))
goto done;
do {
if (!BN_rand_range(h->priv, h->prime)) goto done;
} while (BN_cmp(h->priv, generator) < 0);
if (!BN_mod_exp(local, generator, h->priv, h->prime, h->ctx) ||
BN_bn2binpad(local, h->out, NAUT_MSE_DH_LEN) != NAUT_MSE_DH_LEN)
goto done;
h->out_len = NAUT_MSE_DH_LEN;
h->out_off = 0;
result = NAUT_OK;
done:
BN_free(generator);
BN_free(local);
if (result != NAUT_OK) dh_free(h);
return result;
}
/* Validate the peer's public key and derive the shared secret. */
static naut_err dh_complete(naut_mse_handshake *h, const uint8_t remote_bytes[96],
uint8_t secret[NAUT_MSE_DH_LEN]) {
naut_err result = NAUT_ERR_IO;
BIGNUM *remote = BN_new();
BIGNUM *shared = BN_new();
BIGNUM *limit = BN_new();
BIGNUM *two = BN_new();
if (!remote || !shared || !limit || !two ||
!BN_bin2bn(remote_bytes, NAUT_MSE_DH_LEN, remote) ||
!BN_set_word(two, 2) || !BN_copy(limit, h->prime) ||
!BN_sub_word(limit, 1))
goto done;
if (BN_cmp(remote, two) < 0 || BN_cmp(remote, limit) >= 0) {
result = NAUT_ERR_PROTO;
goto done;
}
if (!BN_mod_exp(shared, remote, h->priv, h->prime, h->ctx) ||
BN_bn2binpad(shared, secret, NAUT_MSE_DH_LEN) != NAUT_MSE_DH_LEN)
goto done;
result = NAUT_OK;
done:
BN_free(remote);
BN_clear_free(shared);
BN_free(limit);
BN_free(two);
return result;
}
naut_mse_handshake *naut_mse_handshake_begin(
const uint8_t info_hash[20],
const uint8_t peer_id[NAUT_PEERID_LEN],
uint64_t reserved) {
if (!info_hash || !peer_id) return NULL;
naut_mse_handshake *h = calloc(1, sizeof(*h));
if (!h) return NULL;
memcpy(h->info_hash, info_hash, 20);
memcpy(h->peer_id, peer_id, NAUT_PEERID_LEN);
h->reserved = reserved;
h->phase = PH_RECV_PUBKEY;
if (dh_begin(h) != NAUT_OK) {
naut_mse_handshake_free(h);
return NULL;
}
return h;
}
void naut_mse_handshake_free(naut_mse_handshake *h) {
if (!h) return;
dh_free(h);
/* keystream state is sensitive; scrub before release */
memset(h, 0, sizeof(*h));
free(h);
}
static void consume(naut_mse_handshake *h, size_t n) {
memmove(h->in, h->in + n, h->in_len - n);
h->in_len -= n;
}
/* Build req1/req2 + encrypted offer (VC, crypto_provide, PadC, IA) into out. */
static void build_request(naut_mse_handshake *h, const uint8_t secret[96]) {
uint8_t req1[20], req2[20], req3[20];
hash_parts("req1", secret, NAUT_MSE_DH_LEN, NULL, 0, req1);
hash_parts("req2", h->info_hash, 20, NULL, 0, req2);
hash_parts("req3", secret, NAUT_MSE_DH_LEN, NULL, 0, req3);
for (size_t i = 0; i < sizeof req2; i++) req2[i] ^= req3[i];
init_rc4(secret, h->info_hash, &h->stream);
uint8_t *p = h->out;
memcpy(p, req1, 20);
memcpy(p + 20, req2, 20);
p += 40;
uint8_t *offer = p; /* VC(8) crypto_provide(4) padlen(2) ialen(2) IA */
memset(offer, 0, 8);
wr32(offer + 8, MSE_CRYPTO_RC4);
wr16(offer + 12, 0);
wr16(offer + 14, NAUT_HANDSHAKE_LEN);
naut_peer_handshake_build(offer + 16, h->info_hash, h->peer_id, h->reserved);
size_t offer_len = 16 + NAUT_HANDSHAKE_LEN;
naut_rc4_xor(&h->stream.send, offer, offer_len);
h->out_len = 40 + offer_len;
h->out_off = 0;
/* expected_vc = our recv keystream applied to 8 zero bytes at position 0,
* without advancing the real recv state (we resync on it). */
naut_rc4 probe = h->stream.recv;
uint8_t vc[8] = {0};
naut_rc4_xor(&probe, vc, sizeof vc);
memcpy(h->expected_vc, vc, sizeof vc);
h->vc_scanned = 0;
}
static void advance(naut_mse_handshake *h) {
for (;;) {
switch (h->phase) {
case PH_RECV_PUBKEY: {
if (h->in_len < NAUT_MSE_DH_LEN) return;
uint8_t secret[NAUT_MSE_DH_LEN];
naut_err e = dh_complete(h, h->in, secret);
if (e != NAUT_OK) { h->err = e; return; }
consume(h, NAUT_MSE_DH_LEN);
dh_free(h); /* DH no longer needed */
build_request(h, secret);
memset(secret, 0, sizeof secret);
h->phase = PH_SYNC_VC;
return; /* out now holds req+offer: NEED_WRITE */
}
case PH_SYNC_VC: {
while (h->in_len >= sizeof h->expected_vc) {
if (memcmp(h->in, h->expected_vc, sizeof h->expected_vc) == 0) {
uint8_t vc[8];
memcpy(vc, h->in, sizeof vc);
naut_rc4_xor(&h->stream.recv, vc, sizeof vc);
static const uint8_t zero8[8] = {0};
if (memcmp(vc, zero8, sizeof vc) != 0) {
h->err = NAUT_ERR_PROTO;
return;
}
consume(h, sizeof vc);
h->phase = PH_RECV_SELECT;
break;
}
consume(h, 1);
if (++h->vc_scanned > MSE_PAD_MAX) {
h->err = NAUT_ERR_PROTO;
return;
}
}
if (h->phase == PH_SYNC_VC) return; /* need more bytes */
continue;
}
case PH_RECV_SELECT: {
if (h->in_len < 6) return;
uint8_t hdr[6];
memcpy(hdr, h->in, sizeof hdr);
naut_rc4_xor(&h->stream.recv, hdr, sizeof hdr);
consume(h, sizeof hdr);
if (rd32(hdr) != MSE_CRYPTO_RC4) { h->err = NAUT_ERR_PROTO; return; }
h->pad_remaining = rd16(hdr + 4);
if (h->pad_remaining > MSE_PAD_MAX) { h->err = NAUT_ERR_PROTO; return; }
h->phase = PH_RECV_PAD;
continue;
}
case PH_RECV_PAD: {
if (h->pad_remaining > 0) {
size_t n = h->pad_remaining < h->in_len ? h->pad_remaining
: h->in_len;
if (n == 0) return;
naut_rc4_xor(&h->stream.recv, h->in, n); /* advance keystream */
consume(h, n);
h->pad_remaining -= n;
if (h->pad_remaining > 0) return;
}
h->phase = PH_RECV_HS;
continue;
}
case PH_RECV_HS: {
if (h->in_len < NAUT_HANDSHAKE_LEN) return;
memcpy(h->remote_handshake, h->in, NAUT_HANDSHAKE_LEN);
naut_rc4_xor(&h->stream.recv, h->remote_handshake, NAUT_HANDSHAKE_LEN);
consume(h, NAUT_HANDSHAKE_LEN);
uint8_t remote_hash[20], remote_id[20];
if (!naut_peer_handshake_parse(h->remote_handshake, remote_hash,
remote_id, NULL) ||
memcmp(remote_hash, h->info_hash, 20) != 0) {
h->err = NAUT_ERR_PROTO;
return;
}
h->stream.active = true;
h->done = true;
return;
}
default:
h->err = NAUT_ERR_PROTO;
return;
}
}
}
naut_mse_hs_status naut_mse_handshake_status(const naut_mse_handshake *h) {
if (!h || h->err != NAUT_OK) return NAUT_MSE_HS_ERROR;
if (h->done) return NAUT_MSE_HS_DONE;
if (h->out_off < h->out_len) return NAUT_MSE_HS_NEED_WRITE;
return NAUT_MSE_HS_NEED_READ;
}
size_t naut_mse_handshake_pull(naut_mse_handshake *h, uint8_t *buf, size_t cap) {
if (!h || !buf) return 0;
size_t avail = h->out_len - h->out_off;
size_t n = avail < cap ? avail : cap;
if (n) {
memcpy(buf, h->out + h->out_off, n);
h->out_off += n;
if (h->out_off == h->out_len) h->out_len = h->out_off = 0;
}
return n;
}
naut_mse_hs_status naut_mse_handshake_feed(naut_mse_handshake *h,
const uint8_t *data, size_t len,
size_t *consumed) {
if (consumed) *consumed = 0;
if (!h) return NAUT_MSE_HS_ERROR;
if (h->err == NAUT_OK && !h->done && data && len) {
size_t space = sizeof h->in - h->in_len;
size_t take = len < space ? len : space;
memcpy(h->in + h->in_len, data, take);
h->in_len += take;
if (consumed) *consumed = take;
advance(h);
}
return naut_mse_handshake_status(h);
}
naut_err naut_mse_handshake_finish(naut_mse_handshake *h,
naut_mse_stream *stream,
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]) {
if (!h || !stream || !remote_handshake) return NAUT_ERR_INVAL;
if (h->err != NAUT_OK) return h->err;
if (!h->done) return NAUT_ERR_AGAIN;
*stream = h->stream;
memcpy(remote_handshake, h->remote_handshake, NAUT_HANDSHAKE_LEN);
return NAUT_OK;
}
/* ---- blocking I/O helpers + convenience wrapper -------------------------- */
static bool raw_send_all(int fd, const void *data, size_t len) {
const uint8_t *p = data;
while (len) {
ssize_t n = send(fd, p, len, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
if (n == 0) return false;
p += n;
len -= (size_t)n;
}
return true;
}
static bool raw_recv_exact(int fd, void *data, size_t len) {
uint8_t *p = data;
while (len) {
ssize_t n = recv(fd, p, len, 0);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
if (n == 0) return false;
p += n;
len -= (size_t)n;
}
return true;
}
naut_err naut_mse_client_handshake(
int fd,
const uint8_t info_hash[20],
const uint8_t peer_id[NAUT_PEERID_LEN],
uint64_t reserved,
naut_mse_stream *stream,
uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]) {
if (fd < 0 || !info_hash || !peer_id || !stream || !remote_handshake)
return NAUT_ERR_INVAL;
memset(stream, 0, sizeof(*stream));
naut_mse_handshake *h =
naut_mse_handshake_begin(info_hash, peer_id, reserved);
if (!h) return NAUT_ERR_NOMEM;
naut_err rc = NAUT_ERR_PROTO;
for (;;) {
naut_mse_hs_status st = naut_mse_handshake_status(h);
if (st == NAUT_MSE_HS_NEED_WRITE) {
uint8_t buf[256];
size_t n;
bool ok = true;
while ((n = naut_mse_handshake_pull(h, buf, sizeof buf)) > 0)
if (!raw_send_all(fd, buf, n)) { ok = false; break; }
if (!ok) { rc = NAUT_ERR_IO; break; }
} else if (st == NAUT_MSE_HS_NEED_READ) {
/* One byte at a time: the handshake is tiny and one-shot, and this
* keeps the wrapper from over-reading into the payload stream. */
uint8_t byte;
if (!raw_recv_exact(fd, &byte, 1)) { rc = NAUT_ERR_IO; break; }
naut_mse_handshake_feed(h, &byte, 1, NULL);
} else if (st == NAUT_MSE_HS_DONE) {
rc = naut_mse_handshake_finish(h, stream, remote_handshake);
break;
} else {
rc = h->err != NAUT_OK ? h->err : NAUT_ERR_PROTO;
break;
}
}
naut_mse_handshake_free(h);
return rc;
}
/* ---- post-handshake stream I/O ------------------------------------------- */
bool naut_mse_send_all(int fd, naut_mse_stream *stream,
const void *data, size_t len) {
if (!stream || !stream->active) return raw_send_all(fd, data, len);
const uint8_t *p = data;
uint8_t block[16 * 1024];
while (len) {
size_t n = len < sizeof block ? len : sizeof block;
memcpy(block, p, n);
naut_rc4_xor(&stream->send, block, n);
if (!raw_send_all(fd, block, n)) return false;
p += n;
len -= n;
}
return true;
}
ssize_t naut_mse_recv(int fd, naut_mse_stream *stream,
void *data, size_t len) {
ssize_t n;
do {
n = recv(fd, data, len, 0);
} while (n < 0 && errno == EINTR);
if (n > 0 && stream && stream->active)
naut_rc4_xor(&stream->recv, data, (size_t)n);
return n;
}