1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! **The DHT rendezvous client** (REMOTE §13.2, bl-df31) — a pure client of
//! the BitTorrent mainline DHT: outbound-UDP iterative lookups and BEP 44
//! signed mutable get/put, and never a node. No listener, no routing answers,
//! no storage offered — nothing for the commons to dial back, which is REMOTE
//! §13's ruling 2 applied to the rendezvous itself.
//!
//! The shape the punched-wire chain consumes is this module root: a caller
//! hands [`Dht::new`] a [`Transport`] (the std UDP socket [`Udp`], or a
//! stand-in), the bootstrap addresses and a [`Config`], then asks
//! [`Dht::lookup`] for the nodes nearest an id, [`Dht::get`] for the newest
//! item under a key and salt, or [`Dht::put`] to store one a [`Keypair`]
//! signed. Everything the commons answers is untrusted until it verifies:
//! an item without a good signature under the asked-for key is not an item.
//!
//! Four files under this root, one concern each: `bencode` the encoding,
//! `krpc` the datagram shapes, `mutable` the signed item, `transport` the
//! socket seam; `lookup` is the walk and `items` the two BEP 44 verbs over
//! it. Synchronous throughout — `std::net` with socket timeouts, no tokio
//! (AGENTS.md rule 8) — and every duration is a [`Config`] field a test can
//! shorten, so the fake DHT the suite runs on loopback UDP answers in
//! milliseconds where the commons answers in seconds.
pub mod bencode;
mod items;
pub mod krpc;
mod lookup;
pub mod mutable;
pub mod transport;
pub use krpc::{Node, NodeId};
pub use mutable::{Keypair, Mutable, target_of};
pub use transport::{Transport, Udp};
use bencode::{Dict, bytes, entry};
use ring::rand::SecureRandom;
use std::net::SocketAddr;
use std::time::Duration;
/// The walk's parameters — stated so a test can shrink them and a caller
/// can widen them; the defaults are BEP 5's.
#[derive(Clone, Debug)]
pub struct Config {
/// Queries in flight per round.
pub alpha: usize,
/// How many closest nodes a walk converges on and a `put` writes to.
pub k: usize,
/// How long one round waits for its answers.
pub round: Duration,
/// The most queries one walk may send, however the commons answers.
pub max_queries: usize,
}
impl Default for Config {
fn default() -> Config {
Config {
alpha: 3,
k: 8,
round: Duration::from_secs(2),
max_queries: 64,
}
}
}
/// One client: a transport, the nodes it starts from, and the id it queries
/// as. The id is random per client — a client is not a node and nobody
/// routes by it, so nothing is lost by minting a fresh one every run.
pub struct Dht {
transport: Box<dyn Transport>,
bootstrap: Vec<SocketAddr>,
config: Config,
id: NodeId,
tid: u16,
}
impl Dht {
/// A client over `transport`, starting every walk from `bootstrap`.
pub fn new(
transport: Box<dyn Transport>,
bootstrap: Vec<SocketAddr>,
config: Config,
) -> Result<Dht, String> {
let mut id = [0u8; 20];
random(&mut id)?;
Ok(Dht {
transport,
bootstrap,
config,
id: NodeId(id),
tid: 0,
})
}
/// The id this client queries as.
pub fn id(&self) -> NodeId {
self.id
}
/// The nodes nearest `target`, closest first — BEP 5's `find_node` walk.
pub fn lookup(&mut self, target: NodeId) -> Result<Vec<Node>, String> {
let out = self.search(
target,
"find_node",
Dict::from([entry("target", bytes(&target.0))]),
)?;
Ok(out
.replies
.into_iter()
.map(|(n, _)| n)
.take(self.config.k)
.collect())
}
/// The next transaction id: two bytes, wrapping, never reused within a
/// walk (a walk sends at most `max_queries`, far under 65 536).
pub(crate) fn next_tid(&mut self) -> Vec<u8> {
self.tid = self.tid.wrapping_add(1);
self.tid.to_be_bytes().to_vec()
}
}
/// Fill `buf` from the system's randomness.
pub(crate) fn random(buf: &mut [u8]) -> Result<(), String> {
ring::rand::SystemRandom::new()
.fill(buf)
.map_err(|_| "the system offered no randomness".to_string())
}
#[cfg(test)]
pub(crate) mod tests;