diff --git a/AGENTS.md b/AGENTS.md index 9506c8b..b8d174e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,9 +45,11 @@ Keep these separate. Crossing them is the main thing to review for. is why the authenticated transport does too. - **A relay carries bytes, never packets.** A datagram passed on for two other peers goes link in, link out: it is never written to the middle's - interface, never decrypted there, and never relayed twice. Reachability - that decides a route is first-hand and volatile — a live announcement - over the control plane, never a signed record and never second-hand. + interface or decrypted there. Multihop routing has a bounded hop limit; + equal-cost next hops are chosen per flow. Reachability is first-hand and + volatile: each authenticated member advertises its own protocol-specific + links. Build routing tables on topology changes, never per packet. Transit + must not acquire a routing mutex or wait for a protocol/TUN reader. - **Plugins never learn reachability.** An `IpPlugin` is handed a `PacketLink` per peer and moves datagrams over it. Addresses, hole punching and relays belong to `crates/tsunagi/src/dataplane/transport/`. A plugin announcement says *who*, never diff --git a/Cargo.lock b/Cargo.lock index 30c1fc7..7814c86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1985,8 +1985,6 @@ checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" [[package]] name = "mainline" version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32eaee3dcba6e0bbbefe8bd896a8bd6039d5e74b199c0fe248e9feb547c2a26" dependencies = [ "crc", "document-features", @@ -3915,6 +3913,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" name = "tsunagi" version = "0.1.0" dependencies = [ + "arc-swap", "bytes", "caps", "data-encoding", diff --git a/Cargo.toml b/Cargo.toml index 112c717..62619cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,12 @@ [workspace] resolver = "3" members = ["crates/*"] +exclude = ["vendor/mainline"] + +# Mainline 8.0.0 treats Windows UDP read timeouts as socket failures. +# Keep the small receive-loop correction reproducible until an upstream fix. +[patch.crates-io] +mainline = { path = "vendor/mainline" } [workspace.package] edition = "2024" diff --git a/README.md b/README.md index 0192312..9c39887 100644 --- a/README.md +++ b/README.md @@ -563,26 +563,31 @@ explicitly. ### Through somebody in the middle -Everybody tries everybody first: the mesh is pairwise, and a relay is only -for the pair that cannot manage it. Two members can both reach a third and -not each other: a blocked path, a -relay that is unavailable, a network only reachable from inside somebody -else's building. When that happens the pair is routed through a member that -has both. +Tsunagi routes the overlay in userspace. A direct data link always wins; +otherwise the shortest available path can pass through several members. +Each authenticated member advertises only its own live, protocol-specific +transport links. The control plane collects these into a graph and replaces +the routing table when connectivity changes; announcements expire after +90 seconds without refresh. -Nothing is agreed and nothing is elected. Each member says only which peers -*it* has a live link with, first-hand, over the control plane and one hop -only; everyone picks their own way through from that, deterministically, and -drops it the moment a direct link exists. There is no routing protocol, no -second-hand claim to weigh, and a relayed datagram is never relayed again — -so a loop cannot form. +Transit goes transport → router → transport, without entering WireGuard or +the intermediate host's TUN. The payload stays encrypted between the original +endpoints. TUN connects only the local OS to Tsunagi; the kernel needs only +the overlay route through that interface, with no IP forwarding configuration. -The one in the middle carries **bytes it cannot read**: the tunnel stays end -to end between the two ends, and a relayed datagram never touches the middle -host's interface, so no forwarding, routing or firewall setting of that host -is involved. `status` says `via ` on a path that goes through -somebody, and counts what this device has carried for others — it is their -traffic on your uplink, and that should not be invisible. +A 16-hop limit bounds loops while topology updates converge. Equal shortest +paths use a stable flow hash derived before encryption, so packets of a TCP +or UDP flow keep their path while topology is unchanged. Physical link changes +do not replace end-to-end WireGuard tunnels or change the 1280-byte default MTU. +`status` reports the hop count and first next hop for relayed paths. + +The hot path reads an immutable routing snapshot and sends directly to a cached +transport handle. It takes no routing mutex, walks no graph and does not parse +the encrypted payload. See [routing.md](docs/routing.md) for the architecture, +limits and reproducible forwarding microbenchmark. + +This wire format requires all members to upgrade together (control ALPN 2, +data ALPN 4); saved identities, network names, secrets and addresses survive. ## How peers find each other diff --git a/crates/tsunagi-wg-quic/src/device.rs b/crates/tsunagi-wg-quic/src/device.rs index 9537dde..3e9416d 100644 --- a/crates/tsunagi-wg-quic/src/device.rs +++ b/crates/tsunagi-wg-quic/src/device.rs @@ -28,7 +28,7 @@ //! system level and signed by its holder, and that is what is compared //! against. -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::net::Ipv4Addr; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; @@ -39,6 +39,7 @@ use bytes::Bytes; use iroh::EndpointId; use tokio::task::JoinHandle; +use tsunagi::dataplane::routing::{FlowId, flow::ip_flow}; use tsunagi::dataplane::transport::{SharedLink, TransportError}; use tsunagi::dataplane::{PacketSink, PluginError}; use tsunagi::identity::NetworkId; @@ -54,6 +55,66 @@ const TIMER_INTERVAL: Duration = Duration::from_millis(250); /// Scratch space for one encapsulate or decapsulate call. const SCRATCH: usize = 4096; +/// Mirrors boringtun 0.7's bounded pending-packet FIFO with opaque flow tags. +/// All three operations share the same mutex as the cryptographic state. +struct FlowTunnel { + tunn: Tunn, + queued: VecDeque, +} + +fn is_data(result: &TunnResult<'_>) -> bool { + matches!(result, TunnResult::WriteToNetwork(bytes) if bytes.starts_with(&[4, 0, 0, 0])) +} + +impl FlowTunnel { + fn encapsulate<'a>( + &mut self, + packet: &[u8], + scratch: &'a mut [u8], + flow: FlowId, + ) -> (TunnResult<'a>, FlowId) { + let result = self.tunn.encapsulate(packet, scratch); + if is_data(&result) { + return (result, flow); + } + // MAX_QUEUE_DEPTH in boringtun 0.7: the newest packet is dropped at 256. + if self.queued.len() < 256 { + self.queued.push_back(flow); + } + (result, 0) + } + + fn decapsulate<'a>( + &mut self, + packet: &[u8], + scratch: &'a mut [u8], + ) -> (TunnResult<'a>, FlowId) { + let result = self.tunn.decapsulate(None, packet, scratch); + let mut flow = 0; + if packet.is_empty() { + if is_data(&result) { + flow = self.queued.pop_front().unwrap_or(0); + } else if let Some(pending) = self.queued.pop_front() { + // Without a session, draining pops the oldest IP packet and + // encapsulate queues it again at the back. + self.queued.push_back(pending); + } + } + (result, flow) + } + + fn update_timers<'a>(&mut self, scratch: &'a mut [u8]) -> TunnResult<'a> { + let result = self.tunn.update_timers(scratch); + if matches!( + result, + TunnResult::Err(boringtun::noise::errors::WireGuardError::ConnectionExpired) + ) { + self.queued.clear(); + } + result + } +} + /// Counters for one peer's tunnel. #[derive(Debug, Default)] struct PeerCounters { @@ -120,7 +181,7 @@ struct Peer { /// allocates it, the peer signs the claim, and every protocol carries /// traffic for the same address. overlay_v4: Mutex>, - tunn: Mutex, + tunn: Mutex, link: SharedLink, counters: Arc, task: Mutex>>, @@ -165,7 +226,7 @@ impl Peer { Err(poisoned) => poisoned.into_inner(), }; PeerHealth { - since_handshake: guard.time_since_last_handshake(), + since_handshake: guard.tunn.time_since_last_handshake(), } } } @@ -254,32 +315,34 @@ impl WireguardDevice { return false; }; - let mut scratch = vec![0u8; SCRATCH]; + let mut scratch = [0u8; SCRATCH]; + let flow = ip_flow(packet); // The encryption is the whole of what this protocol contributes, so // it happens here rather than anywhere the packet passes through. // The lock is released before the send: a slow link must not hold up // the tunnel's timers. - let len = { + let result = { let mut tunn = match peer.tunn.lock() { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; - match tunn.encapsulate(packet, &mut scratch) { - TunnResult::WriteToNetwork(out) => Some(out.len()), + let (result, selected_flow) = tunn.encapsulate(packet, &mut scratch, flow); + match result { + TunnResult::WriteToNetwork(out) => Some((out.len(), selected_flow)), // No session yet, so nothing to send. Counted as dropped // rather than reported: the handshake is in flight and the // next packet will go. _ => None, } }; - let Some(len) = len else { + let Some((len, selected_flow)) = result else { peer.counters .dropped_no_session .fetch_add(1, Ordering::Relaxed); return false; }; - send_to_peer(&peer, &scratch[..len]); + send_to_peer_flow(&peer, &scratch[..len], selected_flow); peer.counters.tx_packets.fetch_add(1, Ordering::Relaxed); peer.counters .tx_bytes @@ -320,7 +383,10 @@ impl WireguardDevice { endpoint_id, public_key, overlay_v4: Mutex::new(overlay_v4), - tunn: Mutex::new(tunn), + tunn: Mutex::new(FlowTunnel { + tunn, + queued: VecDeque::new(), + }), link, counters: Arc::new(PeerCounters::default()), task: Mutex::new(None), @@ -412,9 +478,7 @@ fn write_lock(lock: &RwLock) -> std::sync::RwLockWriteGuard<'_, T> { /// Asks boringtun for a handshake initiation and sends it. /// -/// Encapsulating an empty packet is how the protocol state machine is told -/// "there is something to say"; with no session yet it answers with the -/// handshake initiation. +/// This does not enqueue an empty IP packet in boringtun's pending queue. fn kick_handshake(peer: &Peer) { let mut scratch = vec![0u8; SCRATCH]; let len = { @@ -422,7 +486,7 @@ fn kick_handshake(peer: &Peer) { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; - match tunn.encapsulate(&[], &mut scratch) { + match tunn.tunn.format_handshake_initiation(&mut scratch, false) { TunnResult::WriteToNetwork(out) => Some(out.len()), _ => None, } @@ -434,7 +498,11 @@ fn kick_handshake(peer: &Peer) { /// Sends whatever boringtun produced, without holding the tunnel lock. fn send_to_peer(peer: &Peer, payload: &[u8]) { - match peer.link.send(Bytes::copy_from_slice(payload)) { + send_to_peer_flow(peer, payload, 0); +} + +fn send_to_peer_flow(peer: &Peer, payload: &[u8], flow: FlowId) { + match peer.link.send_flow(Bytes::copy_from_slice(payload), flow) { Ok(()) => {} Err(TransportError::TooLarge { .. }) => { peer.counters @@ -449,12 +517,12 @@ fn send_to_peer(peer: &Peer, payload: &[u8]) { } async fn read_from_link(inner: Arc, peer: Arc) { + let mut scratch = vec![0u8; SCRATCH]; loop { let Some(datagram) = peer.link.recv().await else { return; }; - let mut scratch = vec![0u8; SCRATCH]; // boringtun may need several passes: a handshake reply first, then // any packets that were queued while the session was coming up. let mut input: Option<&[u8]> = Some(&datagram); @@ -464,8 +532,9 @@ async fn read_from_link(inner: Arc, peer: Arc) { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; - match tunn.decapsulate(None, input.unwrap_or(&[]), &mut scratch) { - TunnResult::WriteToNetwork(out) => Outcome::ToNetwork(out.len()), + let (result, flow) = tunn.decapsulate(input.unwrap_or(&[]), &mut scratch); + match result { + TunnResult::WriteToNetwork(out) => Outcome::ToNetwork(out.len(), flow), // The source boringtun reports is not consulted here: // whether the peer may use it is checked where the // claims live. @@ -480,8 +549,8 @@ async fn read_from_link(inner: Arc, peer: Arc) { }; match outcome { - Outcome::ToNetwork(len) => { - send_to_peer(&peer, &scratch[..len]); + Outcome::ToNetwork(len, flow) => { + send_to_peer_flow(&peer, &scratch[..len], flow); // Keep draining with an empty datagram, as boringtun asks. input = None; continue; @@ -515,7 +584,7 @@ async fn read_from_link(inner: Arc, peer: Arc) { } enum Outcome { - ToNetwork(usize), + ToNetwork(usize, FlowId), ToTunnel(usize), Done, Failed, @@ -550,3 +619,73 @@ async fn drive_timers(inner: Arc) { } } } + +#[cfg(test)] +mod flow_tests { + #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use super::*; + + fn tunnel(ours: &WgSecretKey, theirs: &WgSecretKey) -> FlowTunnel { + FlowTunnel { + tunn: Tunn::new( + ours.to_static_secret(), + theirs.public().into_x25519(), + None, + None, + 1, + None, + ), + queued: VecDeque::new(), + } + } + fn wire(result: TunnResult<'_>) -> Vec { + match result { + TunnResult::WriteToNetwork(bytes) => bytes.to_vec(), + _ => panic!("expected WireGuard frame"), + } + } + + #[test] + fn flow_tags_survive_pending_queue_overflow_handshake_and_encryption() { + let ak = WgSecretKey::from_bytes(&[1; 32]); + let bk = WgSecretKey::from_bytes(&[2; 32]); + let mut a = tunnel(&ak, &bk); + let mut b = tunnel(&bk, &ak); + let mut scratch = [0u8; SCRATCH]; + let mut packet = [0u8; 40]; + packet[0] = 0x45; + packet[2..4].copy_from_slice(&40u16.to_be_bytes()); + packet[9] = 6; + packet[12..20].copy_from_slice(&[10, 0, 0, 1, 10, 0, 0, 2]); + let (result, flow) = a.encapsulate(&packet, &mut scratch, 100); + assert_eq!( + flow, 0, + "handshake does not masquerade as application traffic" + ); + let hello = wire(result); + for flow in 101..400 { + let _ = a.encapsulate(&packet, &mut scratch, flow); + } + assert_eq!(a.queued.len(), 256); + let response = wire(b.decapsulate(&hello, &mut scratch).0); + let keepalive = wire(a.decapsulate(&response, &mut scratch).0); + let _ = b.decapsulate(&keepalive, &mut scratch); + for expected in 100..356 { + let (result, flow) = a.decapsulate(&[], &mut scratch); + assert_eq!(flow, expected); + let ciphertext = wire(result); + match b.decapsulate(&ciphertext, &mut scratch).0 { + TunnResult::WriteToTunnelV4(bytes, _) => assert_eq!(bytes, packet), + _ => panic!("queued IP packet must decrypt"), + } + } + assert!(a.queued.is_empty()); + assert!(matches!( + a.decapsulate(&[], &mut scratch).0, + TunnResult::Done + )); + let (result, flow) = a.encapsulate(&packet, &mut scratch, 12345); + assert!(is_data(&result)); + assert_eq!(flow, 12345); + } +} diff --git a/crates/tsunagi-wg-quic/tests/wireguard.rs b/crates/tsunagi-wg-quic/tests/wireguard.rs index 2bd65a2..98010fd 100644 --- a/crates/tsunagi-wg-quic/tests/wireguard.rs +++ b/crates/tsunagi-wg-quic/tests/wireguard.rs @@ -1510,3 +1510,120 @@ async fn a_peer_with_no_direct_path_is_reached_through_one_that_has_both() { b.shutdown().await; middle.shutdown().await; } + +#[tokio::test] +async fn a_chain_routes_through_two_transit_peers_without_touching_their_tuns() { + let discovery = SharedMemoryDiscovery::new(); + let (name, secret) = network("wg-multihop"); + let mut agents = Vec::new(); + let mut blocked = Vec::new(); + for index in 0..4 { + let cuts = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())); + agents.push(WgAgent::spawn_cut_off(&discovery, &format!("mh{index}"), cuts.clone()).await); + blocked.push(cuts); + } + // The authenticated control plane stays connected. The only available + // data links are A--B--C--D; A and D need two transit routers. + for (i, cuts) in blocked.iter().enumerate() { + for (j, agent) in agents.iter().enumerate() { + if i.abs_diff(j) > 1 { + cuts.lock().unwrap().insert(agent.endpoint_id()); + } + } + } + let mut id = None; + for agent in &agents { + id = Some(agent.agent.join_network(&name, &secret).await.unwrap()); + } + let id = id.unwrap(); + for agent in &agents { + wait_for_peers(&agent.agent, id, 3).await; + agent.wait_for_tunnels(id, 3).await; + } + let a = &agents[0]; + let d = &agents[3]; + let a_addr = a.overlay(id).await; + let d_addr = d.overlay(id).await; + let a_tun = a.tun(id).await; + let d_tun = d.tun(id).await; + for (from, to, source, destination) in [ + (&a_tun, &d_tun, a_addr, d_addr), + (&d_tun, &a_tun, d_addr, a_addr), + ] { + let packet = tcp_packet(source, destination, 1280); + from.push_from_os(packet.clone()); + let received = tokio::time::timeout(tsunagi::testing::DEADLINE, to.pop_to_os()) + .await + .unwrap() + .unwrap(); + assert_eq!(received, packet); + } + for middle in &agents[1..3] { + assert!( + middle + .agent + .network_status(id) + .await + .unwrap() + .relay + .forwarded + > 0 + ); + assert!( + tokio::time::timeout(Duration::from_millis(100), middle.tun(id).await.pop_to_os()) + .await + .is_err(), + "transit ciphertext must never enter the intermediate host's TUN" + ); + } + // A direct shortcut wins immediately; losing it restores the chain using + // the same end-to-end tunnels and overlay addresses. + for allow_direct in [true, false] { + for (from, to) in [(0, 3), (3, 0)] { + if allow_direct { + blocked[from] + .lock() + .unwrap() + .remove(&agents[to].endpoint_id()); + } else { + blocked[from] + .lock() + .unwrap() + .insert(agents[to].endpoint_id()); + } + agents[from].agent.recheck_network(id).await.unwrap(); + } + for (from, to) in [(a, d), (d, a)] { + wait_until("route follows the changed topology", || async { + let view = from.plugin.overview(id)?; + let tunnel = view + .peers + .iter() + .find(|peer| peer.endpoint_id == to.endpoint_id())? + .tunnel + .as_ref()?; + let relayed = tunnel.path.contains("relay 3 hops"); + (tunnel.health.is_up() + && if allow_direct { + !tunnel.path.contains("relay") && !tunnel.path.contains("unreachable") + } else { + relayed + }) + .then_some(()) + }) + .await; + } + let packet = tcp_packet(a_addr, d_addr, 1280); + a_tun.push_from_os(packet.clone()); + assert_eq!( + tokio::time::timeout(tsunagi::testing::DEADLINE, d_tun.pop_to_os()) + .await + .unwrap() + .unwrap(), + packet + ); + } + for agent in agents { + agent.shutdown().await; + } +} diff --git a/crates/tsunagi/Cargo.toml b/crates/tsunagi/Cargo.toml index 75d4493..03b970d 100644 --- a/crates/tsunagi/Cargo.toml +++ b/crates/tsunagi/Cargo.toml @@ -33,6 +33,7 @@ dns-publish = ["dep:zbus"] testing = ["dep:tempfile", "dep:tracing-subscriber"] [dependencies] +arc-swap = "1.9" iroh.workspace = true # `net` for the local control interface: a Unix socket on Unix, a named pipe # on Windows. Both live behind the same `net` feature. diff --git a/crates/tsunagi/src/agent/network.rs b/crates/tsunagi/src/agent/network.rs index b40f36b..e2222b9 100644 --- a/crates/tsunagi/src/agent/network.rs +++ b/crates/tsunagi/src/agent/network.rs @@ -264,10 +264,16 @@ struct Runtime { /// else, so there is no hearsay to weigh and nothing to poison. Soft /// state — an entry that stops being refreshed expires, which is how a /// relay that has gone stops being chosen without anybody revoking it. - reachable: HashMap, std::time::Instant)>, + reachable: HashMap< + EndpointId, + ( + HashSet, + std::time::Instant, + ), + >, /// What this agent last told its peers it could reach, so it is only /// said again when it changed. - announced_reach: HashSet, + announced_reach: HashSet, /// Links currently being opened, so we do not start two. opening: HashSet<(EndpointId, String)>, link_results_tx: mpsc::Sender, @@ -328,7 +334,7 @@ impl Runtime { dial_results_tx, dial_results_rx, links: HashMap::new(), - hub: crate::dataplane::relay::RelayHub::new(network_id), + hub: crate::dataplane::relay::RelayHub::new(network_id, local_id), reachable: HashMap::new(), announced_reach: HashSet::new(), opening: HashSet::new(), @@ -360,6 +366,11 @@ impl Runtime { tokio::select! { biased; _ = self.shutdown.wait() => break, + _ = self.hub.changed() => { + self.ensure_links(); + self.update_paths(); + self.announce_reach(false); + } command = commands.recv() => match command { Some(command) => self.handle_command(command).await, None => break, @@ -387,6 +398,8 @@ impl Runtime { _ = ticker.tick() => { self.discovery_round().await; self.ensure_links(); + self.update_paths(); + self.announce_reach(false); } } } @@ -401,6 +414,7 @@ impl Runtime { // Stop accepting session events first: nothing is going to act on them // any more, and a sender blocked on a full queue would stall shutdown. self.session_events_rx.close(); + self.hub.close(); self.links.clear(); let peers: Vec = self.sessions.keys().copied().collect(); for peer in peers { @@ -1122,7 +1136,7 @@ impl Runtime { let mut dead: Vec<(EndpointId, String)> = Vec::new(); for (key, link) in &self.links { - if link.is_closed() { + if link.is_closed() || self.no_direct_data(key.0) { dead.push(key.clone()); } } @@ -1307,28 +1321,28 @@ impl Runtime { } } - /// Tells peers which peers this agent has a live link with, when that - /// has changed. - /// - /// Only what it can see for itself, and only to the peers it is - /// talking to: one hop, no flooding, nothing second-hand. A peer that - /// hears this is the only one that can use it, because it is also the - /// only one that could route through this agent. + /// Publishes only locally observed, protocol-specific transport edges. fn announce_reach(&mut self, force: bool) { - let live: HashSet = self + let live: HashSet<_> = self .links .iter() .filter(|(_, link)| !link.is_closed()) - .map(|((peer, _), _)| *peer) + .map( + |((peer, protocol), _)| crate::proto::message::ReachableLink { + peer: *peer.as_bytes(), + protocol: protocol.clone(), + }, + ) .collect(); if !force && live == self.announced_reach { return; } self.announced_reach = live.clone(); - let message = ControlMessage::Reachable { - peers: live.iter().map(|peer| *peer.as_bytes()).collect(), - }; - let peers: Vec = self.sessions.keys().copied().collect(); + let mut links: Vec<_> = live.into_iter().collect(); + links.sort_by(|a, b| (&a.protocol, a.peer).cmp(&(&b.protocol, b.peer))); + links.truncate(self.params.limits.max_state_records); + let message = ControlMessage::Reachable { links }; + let peers: Vec<_> = self.sessions.keys().copied().collect(); for peer in peers { if let Err(err) = self.send_to(peer, message.clone()) { tracing::debug!(%err, "could not queue a reachability announcement"); @@ -1336,99 +1350,72 @@ impl Runtime { } } - /// Chooses, for every peer with no direct link, somebody to go through. - /// - /// Local and deterministic: the lowest endpoint id among the peers - /// that both this agent has a link with and that say they have a link - /// with the destination. Nothing is agreed with anybody — two agents - /// may well pick differently, and traffic each way finds its own path. + /// Builds the graph off the packet path. Every remote row comes from that + /// member's authenticated control session; stale or incompatible rows never + /// enter the graph. The hub replaces an immutable table only on changes. fn update_paths(&mut self) { - self.reachable - .retain(|_, (_, heard)| heard.elapsed() < REACH_EXPIRY); - - let served = self.served_protocols(); - let live: HashSet = self - .links - .iter() - .filter(|(_, link)| !link.is_closed()) - .map(|((peer, _), _)| *peer) - .collect(); - - // Every peer this agent would carry traffic with: a session says - // who it is and what it speaks, which is also what a protocol - // needs before it can have a link at all. - let wanted: Vec<(EndpointId, String)> = self - .sessions - .values() - .flat_map(|session| { - let peer = session.peer; - session - .capabilities - .iter() - .filter(|capability| capability.enabled) - .map(move |capability| (peer, capability.protocol.clone(), capability.version)) - }) - .filter(|(_, protocol, version)| { - served - .iter() - .any(|(name, ours)| name == protocol && ours == version) - }) - .map(|(peer, protocol, _)| (peer, protocol)) - .collect(); - - for (peer, protocol) in wanted { - if live.contains(&peer) { - // There is a direct link; a hop is only for when there is - // not, and holding a stale one would keep a peer that has - // gone looking reachable. - if self.hub.has_link(peer, &protocol) { - self.hub.set_hop(peer, &protocol, None); - } - continue; - } - let hop = self + self.reachable.retain(|peer, (_, heard)| { + heard.elapsed() < REACH_EXPIRY && self.sessions.contains_key(peer) + }); + for (protocol, version) in self.served_protocols() { + let peers: Vec<_> = self + .sessions + .values() + .filter(|session| { + session.capabilities.iter().any(|capability| { + capability.enabled + && capability.protocol == protocol + && capability.version == version + }) + }) + .map(|session| session.peer) + .collect(); + let mut members: HashSet<_> = peers.iter().map(|peer| *peer.as_bytes()).collect(); + members.insert(*self.local_id.as_bytes()); + let graph = self .reachable .iter() - .filter(|(middle, (reaches, _))| { - live.contains(*middle) && reaches.contains(&peer) && **middle != peer + .filter(|(peer, _)| members.contains(peer.as_bytes())) + .map(|(peer, (links, _))| { + let mut neighbors: Vec<_> = links + .iter() + .filter(|link| { + link.protocol == protocol + && members.contains(&link.peer) + && link.peer != *peer.as_bytes() + }) + .map(|link| link.peer) + .collect(); + neighbors.sort_unstable(); + (*peer.as_bytes(), neighbors) }) - .map(|(middle, _)| *middle) - .min_by(|a, b| a.as_bytes().cmp(b.as_bytes())); - - let known = self.hub.has_link(peer, &protocol); - if !known && hop.is_none() { - // Nothing to reach it with and nothing to offer a - // protocol; inventing a link object here would only look - // like connectivity. - continue; + .collect(); + self.hub.set_topology(&protocol, graph, members); + for peer in peers { + if self.hub.has_link(peer, &protocol) || !self.hub.reachable(peer, &protocol) { + continue; + } + let Some(plugin) = self + .params + .plugins + .iter() + .find(|plugin| plugin.protocol_id() == protocol) + .cloned() + else { + continue; + }; + let link = self.hub.link(peer, &protocol); + let path = link.path_description(); + let max_datagram = link.max_datagram_size(); + plugin.on_peer_link(self.network_id, peer, link); + self.emit(Event::DataLinkUp { + network: self.network_id, + peer, + protocol: protocol.clone(), + path, + max_datagram, + }); } - self.hub.set_hop(peer, &protocol, hop); - if known { - continue; - } - // A peer reachable only through somebody still gets a link, so - // its tunnel can be built: the protocol is not told how its - // datagrams travel, only that this is the way to that peer. - let Some(plugin) = self - .params - .plugins - .iter() - .find(|plugin| plugin.protocol_id() == protocol) - .cloned() - else { - continue; - }; - let link = self.hub.link(peer, &protocol); - let path = link.path_description(); - let max_datagram = link.max_datagram_size(); - plugin.on_peer_link(self.network_id, peer, link); - self.emit(Event::DataLinkUp { - network: self.network_id, - peer, - protocol, - path, - max_datagram, - }); } } @@ -1718,6 +1705,8 @@ impl Runtime { } self.metrics.disconnects += 1; self.drop_links_for(peer); + self.update_paths(); + self.announce_reach(false); for plugin in &self.params.plugins { plugin.on_peer_gone(self.network_id, peer); } @@ -1748,6 +1737,7 @@ impl Runtime { } self.dispatch_capabilities(peer, &capabilities); self.ensure_links(); + self.update_paths(); } ControlMessage::Ping { seq, payload } => { let pong = ControlMessage::Pong { @@ -1772,11 +1762,8 @@ impl Runtime { } // What that peer can reach, from that peer. Kept with the time // it was heard, so it can go stale on its own. - ControlMessage::Reachable { peers } => { - let heard: HashSet = peers - .iter() - .filter_map(|raw| EndpointId::from_bytes(raw).ok()) - .collect(); + ControlMessage::Reachable { links } => { + let heard = links.iter().cloned().collect(); self.reachable .insert(peer, (heard, std::time::Instant::now())); self.update_paths(); diff --git a/crates/tsunagi/src/config.rs b/crates/tsunagi/src/config.rs index fa8de19..8baca3b 100644 --- a/crates/tsunagi/src/config.rs +++ b/crates/tsunagi/src/config.rs @@ -480,3 +480,5 @@ impl std::fmt::Debug for AgentConfig { .finish() } } +/// Maximum number of transport links a routed datagram may traverse. +pub const ROUTING_HOP_LIMIT: u8 = 16; diff --git a/crates/tsunagi/src/dataplane/mod.rs b/crates/tsunagi/src/dataplane/mod.rs index 4953a3c..2af8215 100644 --- a/crates/tsunagi/src/dataplane/mod.rs +++ b/crates/tsunagi/src/dataplane/mod.rs @@ -20,6 +20,7 @@ //! recorded and surfaced, the control plane keeps running. pub mod relay; +pub mod routing; pub mod transport; use std::sync::Arc; diff --git a/crates/tsunagi/src/dataplane/relay.rs b/crates/tsunagi/src/dataplane/relay.rs index 38ff06a..7369759 100644 --- a/crates/tsunagi/src/dataplane/relay.rs +++ b/crates/tsunagi/src/dataplane/relay.rs @@ -1,132 +1,45 @@ -//! Carrying a peer's packets through another peer. -//! -//! Two members of a network can both reach a third and yet not reach each -//! other: a blocked path, a relay that is unavailable, a network that is -//! only reachable from inside somebody else's building. Without a way -//! through the middle, that pair is simply lost to each other while both -//! sit in the same mesh. -//! -//! # What goes through the middle, and what does not -//! -//! The relay moves **bytes it cannot read**. A datagram is wrapped with the -//! peer it is for, handed to the peer in the middle, and unwrapped on the -//! other side; the protocol's own encryption is end to end between the two -//! ends of the tunnel, so the one in the middle carries an opaque payload. -//! -//! It never reaches the middle's operating system either: a relayed -//! datagram goes link in, link out. Nothing is written to its interface, -//! so no routing, forwarding or firewall setting of its host is involved — -//! which is both the fast path and the only one allowed here, since an -//! agent touches no system object it did not create. -//! -//! # What is deliberately not here -//! -//! No routing protocol and nothing second-hand. A peer says only which -//! peers *it* has a live link with, and that is used to pick one hop — -//! never a claim about somebody else's reachability. One hop means loops -//! are impossible by construction rather than by a counter, and a relayed -//! datagram is never relayed again. -//! -//! Fairness between the peers a relay carries for is not addressed yet: the -//! queues are bounded and the counters say how much went through, which is -//! what a limit would be built on. +//! Userspace forwarding. Topology updates publish immutable tables; the packet +//! path takes no routing mutex, walks no graph and never inspects plugin bytes. +//! Each transport reader forwards transit immediately, independently of the +//! destination protocol's receive task. Only local delivery enters its inbox. -use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex, RwLock, Weak}; +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; -use bytes::{BufMut, Bytes, BytesMut}; +use arc_swap::ArcSwap; +use bytes::Bytes; use iroh::EndpointId; -use tokio::sync::mpsc; +use tokio::sync::{Notify, mpsc, watch}; +use tokio::task::JoinHandle; +use super::routing::{FlowId, PeerId, RoutingTable, envelope, mix64}; +use super::transport::{PacketLink, SharedLink, TransportError}; use crate::BoxFuture; +use crate::config::{MAX_DATA_DATAGRAM, ROUTING_HOP_LIMIT}; use crate::identity::NetworkId; -use super::transport::{PacketLink, SharedLink, TransportError}; - -/// A datagram straight from the peer that sent it. -const TAG_DIRECT: u8 = 0; -/// A datagram for somebody else, to be passed on. -const TAG_TO_RELAY: u8 = 1; -/// A datagram that was passed on, naming who it came from. -const TAG_RELAYED: u8 = 2; - -/// The largest header any of the three shapes needs. -/// -/// Subtracted from every link's datagram size, relayed or not, so that the -/// size a protocol may use does not change when the path does. A tunnel -/// that had to renegotiate its packet size every time a path changed would -/// be worse than one that is a few bytes smaller than it could be. -pub const RELAY_OVERHEAD: usize = 1 + 32; - -/// How many datagrams may wait for a protocol to read them. -/// -/// Bounded, and the oldest is dropped rather than the newest kept waiting: -/// these are datagrams, loss is ordinary, and a queue that grows is worse -/// than one that spills. +/// Fixed routing header, subtracted from the logical transport MTU. +pub const RELAY_OVERHEAD: usize = envelope::HEADER; const INBOX: usize = 256; -/// What a datagram on a data link turned out to be. -#[derive(Debug, Clone, PartialEq, Eq)] -enum Frame { - /// From the peer at the other end of this link, for us. - Direct(Bytes), - /// From the peer at the other end, for somebody else. - ToRelay { to: EndpointId, payload: Bytes }, - /// Passed on by the peer at the other end, from somebody else. - Relayed { from: EndpointId, payload: Bytes }, -} - -fn wrap(tag: u8, id: Option, payload: &[u8]) -> Bytes { - let mut out = BytesMut::with_capacity(1 + 32 + payload.len()); - out.put_u8(tag); - if let Some(id) = id { - out.put_slice(id.as_bytes()); - } - out.put_slice(payload); - out.freeze() -} - -/// Reads a datagram, or `None` if it is not one of ours. -/// -/// Nothing here trusts a length: every field is checked before it is read, -/// because this is bytes off the network like any other. -fn unwrap(raw: Bytes) -> Option { - let tag = *raw.first()?; - match tag { - TAG_DIRECT => Some(Frame::Direct(raw.slice(1..))), - TAG_TO_RELAY | TAG_RELAYED => { - if raw.len() < 1 + 32 { - return None; - } - let mut id = [0u8; 32]; - id.copy_from_slice(&raw[1..33]); - let id = EndpointId::from_bytes(&id).ok()?; - let payload = raw.slice(33..); - Some(if tag == TAG_TO_RELAY { - Frame::ToRelay { to: id, payload } - } else { - Frame::Relayed { from: id, payload } - }) - } - _ => None, - } -} - -/// What has gone through the relay, from both sides of it. +/// Forwarding counters, shared by all protocols of one network. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct RelayCounters { - /// Datagrams this agent sent to a peer through somebody else. + /// Datagrams originated here and sent through another member. pub sent_via: u64, - /// Datagrams this agent passed on for two other peers. + /// Transit datagrams handed to the next transport link. pub forwarded: u64, - /// Datagrams that arrived for this agent through somebody else. + /// Local datagrams received through another member. pub received_via: u64, - /// Datagrams that could not be passed on: no link to the destination. + /// Missing or failed next hop. pub dropped_no_link: u64, - /// Datagrams that arrived relayed from a peer we have no link object - /// for, or that could not be read at all. + /// Malformed envelope or unknown member. pub dropped_unknown: u64, + /// Exhausted hop limit, including loops during topology convergence. + pub dropped_hop_limit: u64, + /// Full local protocol inbox. Transit does not use this queue. + pub dropped_congested: u64, } #[derive(Debug, Default)] @@ -136,6 +49,8 @@ struct Tally { received_via: AtomicU64, dropped_no_link: AtomicU64, dropped_unknown: AtomicU64, + dropped_hop_limit: AtomicU64, + dropped_congested: AtomicU64, } impl Tally { @@ -146,260 +61,365 @@ impl Tally { received_via: self.received_via.load(Ordering::Relaxed), dropped_no_link: self.dropped_no_link.load(Ordering::Relaxed), dropped_unknown: self.dropped_unknown.load(Ordering::Relaxed), + dropped_hop_limit: self.dropped_hop_limit.load(Ordering::Relaxed), + dropped_congested: self.dropped_congested.load(Ordering::Relaxed), } } } -/// Every data link of one network, and what it can reach through what. -/// -/// One per network runtime. It owns the raw links the transport produced -/// and hands protocols a [`PeerLink`] each, which is the thing that knows -/// whether a peer is reached directly or through somebody. #[derive(Debug)] -pub struct RelayHub { - network: NetworkId, - /// What a protocol holds, one per peer and protocol. Kept alive here so - /// a datagram can be injected into a link the protocol is reading. - links: Mutex>>, - /// The raw links, which is what a hop is: a way to reach the peer in - /// the middle, never wrapped again. - raw: Mutex>, +struct NextHop { + peer: PeerId, + link: SharedLink, +} + +#[derive(Debug)] +struct ForwardRoute { + hops: u8, + next: Box<[NextHop]>, +} + +impl ForwardRoute { + fn select(&self, source: &PeerId, flow: FlowId) -> Option<&NextHop> { + if self.next.len() == 1 { + return self.next.first().filter(|hop| !hop.link.is_closed()); + } + let seed = u64::from_le_bytes(source[..8].try_into().ok()?); + let start = mix64(flow ^ seed) as usize % self.next.len(); + // Stable fallback if a link closed just before its table was replaced. + (0..self.next.len()) + .map(|i| &self.next[(start + i) % self.next.len()]) + .find(|hop| !hop.link.is_closed()) + } +} + +#[derive(Debug, Default)] +struct ForwardingTable { + routes: HashMap, + inboxes: HashMap>, + members: HashSet, +} + +#[derive(Debug)] +struct Plane { + local: PeerId, + table: ArcSwap, tally: Arc, } +impl Plane { + /// Synchronous hot path: one snapshot, bounded header decode, table lookup, + /// and transport send. An exclusively owned buffer is modified in place. + fn receive(&self, incoming: PeerId, frame: Bytes) { + let Some(header) = envelope::decode(&frame) else { + self.tally.dropped_unknown.fetch_add(1, Ordering::Relaxed); + return; + }; + let table = self.table.load(); + if !table.members.contains(&header.source) || header.source == self.local { + self.tally.dropped_unknown.fetch_add(1, Ordering::Relaxed); + return; + } + if header.destination == self.local { + let Some(inbox) = table.inboxes.get(&header.source) else { + self.tally.dropped_unknown.fetch_add(1, Ordering::Relaxed); + return; + }; + if inbox.try_send(frame.slice(RELAY_OVERHEAD..)).is_err() { + self.tally.dropped_congested.fetch_add(1, Ordering::Relaxed); + } else if incoming != header.source { + self.tally.received_via.fetch_add(1, Ordering::Relaxed); + } + return; + } + if header.remaining <= 1 { + self.tally.dropped_hop_limit.fetch_add(1, Ordering::Relaxed); + return; + } + let hop = table + .routes + .get(&header.destination) + .and_then(|route| route.select(&header.source, header.flow)); + let Some(hop) = hop else { + self.tally.dropped_no_link.fetch_add(1, Ordering::Relaxed); + return; + }; + if hop.link.send(envelope::decrement(frame)).is_ok() { + self.tally.forwarded.fetch_add(1, Ordering::Relaxed); + } else { + self.tally.dropped_no_link.fetch_add(1, Ordering::Relaxed); + } + } +} + +#[derive(Debug)] +struct Protocol { + plane: Arc, + graph: HashMap>, + members: HashSet, + raw: HashMap, + readers: HashMap>, + links: HashMap>, +} + +impl Protocol { + fn publish(&self) { + let mut graph = self.graph.clone(); + graph.insert( + self.plane.local, + self.raw + .iter() + .filter(|(_, link)| !link.is_closed()) + .map(|(peer, _)| *peer) + .collect(), + ); + let routing = RoutingTable::build(self.plane.local, &graph, ROUTING_HOP_LIMIT); + let routes = routing + .iter() + .filter_map(|(peer, route)| { + let next: Box<[_]> = route + .next_hops + .iter() + .filter_map(|id| { + self.raw.get(id).map(|link| NextHop { + peer: *id, + link: link.clone(), + }) + }) + .collect(); + (!next.is_empty()).then_some(( + *peer, + ForwardRoute { + hops: route.hops, + next, + }, + )) + }) + .collect(); + let mut members = self.members.clone(); + members.extend(self.raw.keys().copied()); + self.plane.table.store(Arc::new(ForwardingTable { + routes, + members, + inboxes: self + .links + .iter() + .map(|(peer, link)| (*peer, link.inbox_tx.clone())) + .collect(), + })); + } +} + +impl Drop for Protocol { + fn drop(&mut self) { + for task in self.readers.values() { + task.abort(); + } + for link in self.links.values() { + link.close(); + } + self.plane.table.store(Arc::default()); + } +} + +/// One network's transport links and independently published protocol tables. +#[derive(Debug)] +pub struct RelayHub { + network: NetworkId, + local: PeerId, + protocols: Mutex>, + tally: Arc, + changed: Arc, +} + impl RelayHub { - /// A hub for one network. - pub fn new(network: NetworkId) -> Arc { + /// Creates the router at this network's local endpoint. + pub fn new(network: NetworkId, local: EndpointId) -> Arc { Arc::new(Self { network, - links: Mutex::new(HashMap::new()), - raw: Mutex::new(HashMap::new()), - tally: Arc::new(Tally::default()), + local: *local.as_bytes(), + protocols: Mutex::default(), + tally: Arc::default(), + changed: Arc::default(), }) } - /// What has gone through it. + fn protocols(&self) -> std::sync::MutexGuard<'_, HashMap> { + self.protocols + .lock() + .unwrap_or_else(|error| error.into_inner()) + } + + fn protocol<'a>( + &self, + protocols: &'a mut HashMap, + name: &str, + ) -> &'a mut Protocol { + protocols + .entry(name.to_owned()) + .or_insert_with(|| Protocol { + plane: Arc::new(Plane { + local: self.local, + table: ArcSwap::from_pointee(ForwardingTable::default()), + tally: self.tally.clone(), + }), + graph: HashMap::new(), + members: HashSet::new(), + raw: HashMap::new(), + readers: HashMap::new(), + links: HashMap::new(), + }) + } + + /// Wakes the control loop promptly when a transport reader ends. + pub async fn changed(&self) { + self.changed.notified().await; + } + + /// Current counters. pub fn counters(&self) -> RelayCounters { self.tally.snapshot() } - fn links(&self) -> std::sync::MutexGuard<'_, HashMap<(EndpointId, String), Arc>> { - match self.links.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), + /// Publishes authenticated topology. Refreshing identical state is cheap. + pub fn set_topology( + &self, + protocol: &str, + graph: HashMap>, + members: HashSet, + ) { + let mut protocols = self.protocols(); + let state = self.protocol(&mut protocols, protocol); + if state.graph != graph || state.members != members { + state.graph = graph; + state.members = members; + state.publish(); } } - fn raw(&self) -> std::sync::MutexGuard<'_, HashMap<(EndpointId, String), SharedLink>> { - match self.raw.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - } + /// Whether the current table has a path to this destination. + pub fn reachable(&self, peer: EndpointId, protocol: &str) -> bool { + self.protocols().get(protocol).is_some_and(|state| { + state + .plane + .table + .load() + .routes + .contains_key(peer.as_bytes()) + }) } - /// The link a protocol uses for a peer, created on first ask. - /// - /// The same object for the life of the peer, whatever happens to the - /// path underneath it: a protocol's tunnel survives a direct link - /// coming and going, and a change of hop, without noticing. - pub fn link(self: &Arc, peer: EndpointId, protocol: &str) -> Arc { - let key = (peer, protocol.to_string()); - let mut links = self.links(); - if let Some(existing) = links.get(&key) { - return Arc::clone(existing); + /// Stable logical link, preserved across topology changes. + pub fn link(&self, peer: EndpointId, protocol: &str) -> Arc { + let mut protocols = self.protocols(); + let state = self.protocol(&mut protocols, protocol); + if let Some(link) = state.links.get(peer.as_bytes()) { + return link.clone(); } let (inbox_tx, inbox_rx) = mpsc::channel(INBOX); + let (closed, _) = watch::channel(false); let link = Arc::new(PeerLink { network: self.network, peer, - protocol: protocol.to_string(), - direct: RwLock::new(None), - hop: RwLock::new(None), + plane: state.plane.clone(), inbox_tx, inbox_rx: tokio::sync::Mutex::new(inbox_rx), - changed: tokio::sync::Notify::new(), - closed: AtomicBool::new(false), - gone: tokio::sync::Notify::new(), - hub: Arc::downgrade(self), - tally: Arc::clone(&self.tally), + closed, }); - links.insert(key, Arc::clone(&link)); + state.links.insert(*peer.as_bytes(), link.clone()); + state.publish(); link } - /// Whether a protocol already holds a link for this peer. - /// - /// Asked before handing one out, because a protocol is given its link - /// once and keeps it: handing the same peer a second link would leave - /// it with two and a tunnel it is no longer reading for. + /// Whether the protocol already owns its logical link. pub fn has_link(&self, peer: EndpointId, protocol: &str) -> bool { - self.links().contains_key(&(peer, protocol.to_string())) + self.protocols() + .get(protocol) + .is_some_and(|state| state.links.contains_key(peer.as_bytes())) } - /// Records the direct link to a peer, replacing any previous one. - pub fn set_direct(self: &Arc, peer: EndpointId, protocol: &str, raw: SharedLink) { - self.raw().insert((peer, protocol.to_string()), raw.clone()); - let link = self.link(peer, protocol); - link.set_direct(Some(raw)); + /// Installs a raw transport link and starts its independent ingress reader. + pub fn set_direct(&self, peer: EndpointId, protocol: &str, raw: SharedLink) { + if raw.network() != self.network || raw.peer() != peer { + return; + } + let mut protocols = self.protocols(); + let state = self.protocol(&mut protocols, protocol); + let id = *peer.as_bytes(); + if let Some(old) = state.readers.remove(&id) { + old.abort(); + } + state.raw.insert(id, raw.clone()); + state.publish(); + let plane = state.plane.clone(); + let changed = self.changed.clone(); + state.readers.insert( + id, + tokio::spawn(async move { + let mut batch = 0; + while let Some(frame) = raw.recv().await { + plane.receive(id, frame); + batch += 1; + if batch == 64 { + tokio::task::yield_now().await; + batch = 0; + } + } + changed.notify_one(); + }), + ); } - /// Forgets the direct link to a peer. The protocol's link stays. - pub fn clear_direct(self: &Arc, peer: EndpointId, protocol: &str) { - self.raw().remove(&(peer, protocol.to_string())); - if let Some(link) = self.links().get(&(peer, protocol.to_string())) { - link.set_direct(None); + /// Removes a physical link without tearing down end-to-end protocol state. + pub fn clear_direct(&self, peer: EndpointId, protocol: &str) { + if let Some(state) = self.protocols().get_mut(protocol) { + state.raw.remove(peer.as_bytes()); + if let Some(task) = state.readers.remove(peer.as_bytes()) { + task.abort(); + } + state.publish(); } } - /// Routes a peer through another peer, or stops doing so. - /// - /// The hop must be a peer with a direct link of its own; anything else - /// is a request to relay through somebody unreachable. - pub fn set_hop(self: &Arc, peer: EndpointId, protocol: &str, hop: Option) { - let resolved = hop.and_then(|hop| { - self.raw() - .get(&(hop, protocol.to_string())) - .map(|link| (hop, Arc::clone(link))) - }); - let link = self.link(peer, protocol); - link.set_hop(resolved); - } - - /// Closes and forgets everything held for a peer. + /// Revokes all links and topology involving a departed authenticated peer. pub fn remove_peer(&self, peer: EndpointId) { - self.raw().retain(|(other, _), _| *other != peer); - let mut links = self.links(); - links.retain(|(other, _), link| { - if *other == peer { + for state in self.protocols().values_mut() { + state.raw.remove(peer.as_bytes()); + if let Some(task) = state.readers.remove(peer.as_bytes()) { + task.abort(); + } + if let Some(link) = state.links.remove(peer.as_bytes()) { link.close(); - false - } else { - true } - }); + state.members.remove(peer.as_bytes()); + state.graph.remove(peer.as_bytes()); + for neighbors in state.graph.values_mut() { + neighbors.retain(|id| id != peer.as_bytes()); + } + state.publish(); + } } - /// Closes everything. The network is going away. + /// Stops every reader and releases all transport handles. pub fn close(&self) { - self.raw().clear(); - for link in self.links().drain() { - link.1.close(); - } - } - - /// Passes a datagram on to the peer it is for. - /// - /// Only over a direct link: a relayed datagram is never relayed again, - /// which is what makes a loop impossible without counting hops. - fn forward(&self, to: EndpointId, protocol: &str, from: EndpointId, payload: Bytes) { - let link = self.raw().get(&(to, protocol.to_string())).cloned(); - let Some(link) = link else { - self.tally.dropped_no_link.fetch_add(1, Ordering::Relaxed); - return; - }; - match link.send(wrap(TAG_RELAYED, Some(from), &payload)) { - Ok(()) => { - self.tally.forwarded.fetch_add(1, Ordering::Relaxed); - } - Err(err) => { - tracing::debug!(%err, "cannot pass a datagram on"); - self.tally.dropped_no_link.fetch_add(1, Ordering::Relaxed); - } - } - } - - /// Hands a datagram that arrived through somebody to the peer's link. - fn inject(&self, from: EndpointId, protocol: &str, payload: Bytes) { - let link = self.links().get(&(from, protocol.to_string())).cloned(); - let Some(link) = link else { - // Nothing is reading for that peer: it is not one this agent - // carries traffic with, so the datagram has no owner. - self.tally.dropped_unknown.fetch_add(1, Ordering::Relaxed); - return; - }; - match link.inbox_tx.try_send(payload) { - Ok(()) => { - self.tally.received_via.fetch_add(1, Ordering::Relaxed); - } - Err(_) => { - self.tally.dropped_unknown.fetch_add(1, Ordering::Relaxed); - } - } + self.protocols().clear(); } } -/// A protocol's link to one peer, however that peer is reached. -/// -/// It outlives any particular path. A direct link that dies, a hop that -/// changes, a direct link that comes back: none of it is visible to the -/// protocol holding this, so its tunnel is not torn down and rebuilt every -/// time the way through changes. +/// A protocol's end-to-end channel, independent of its current next hop. #[derive(Debug)] pub struct PeerLink { network: NetworkId, peer: EndpointId, - protocol: String, - direct: RwLock>, - hop: RwLock>, + plane: Arc, inbox_tx: mpsc::Sender, inbox_rx: tokio::sync::Mutex>, - /// Woken when the path changes, so a reader parked on the old one - /// starts again on the new one. - changed: tokio::sync::Notify, - closed: AtomicBool, - gone: tokio::sync::Notify, - hub: Weak, - tally: Arc, + closed: watch::Sender, } impl PeerLink { - fn direct(&self) -> Option { - match self.direct.read() { - Ok(guard) => guard.clone(), - Err(poisoned) => poisoned.into_inner().clone(), - } - } - - fn hop(&self) -> Option<(EndpointId, SharedLink)> { - match self.hop.read() { - Ok(guard) => guard.clone(), - Err(poisoned) => poisoned.into_inner().clone(), - } - } - - fn set_direct(&self, link: Option) { - match self.direct.write() { - Ok(mut guard) => *guard = link, - Err(poisoned) => *poisoned.into_inner() = link, - } - self.changed.notify_waiters(); - } - - fn set_hop(&self, hop: Option<(EndpointId, SharedLink)>) { - match self.hop.write() { - Ok(mut guard) => *guard = hop, - Err(poisoned) => *poisoned.into_inner() = hop, - } - self.changed.notify_waiters(); - } - fn close(&self) { - self.closed.store(true, Ordering::Relaxed); - self.gone.notify_waiters(); - self.changed.notify_waiters(); - } - - /// The peer this link carries traffic for. - pub fn peer(&self) -> EndpointId { - self.peer - } - - /// Which peer it goes through, when it is not direct. - pub fn via(&self) -> Option { - if self.direct().is_some_and(|link| !link.is_closed()) { - return None; - } - self.hop().map(|(peer, _)| peer) + self.closed.send_replace(true); } } @@ -407,400 +427,80 @@ impl PacketLink for PeerLink { fn network(&self) -> NetworkId { self.network } - fn peer(&self) -> EndpointId { self.peer } - fn max_datagram_size(&self) -> usize { - // The same size whatever the path, so a protocol never has to - // resize because the way through changed. - let direct = self.direct().map(|link| link.max_datagram_size()); - let hop = self.hop().map(|(_, link)| link.max_datagram_size()); - let smallest = match (direct, hop) { - (Some(a), Some(b)) => a.min(b), - (Some(a), None) | (None, Some(a)) => a, - (None, None) => 0, - }; - smallest.saturating_sub(RELAY_OVERHEAD) + MAX_DATA_DATAGRAM - RELAY_OVERHEAD } - fn send(&self, payload: Bytes) -> Result<(), TransportError> { - if self.closed.load(Ordering::Relaxed) { + self.send_flow(payload, 0) + } + fn send_flow(&self, payload: Bytes, flow: FlowId) -> Result<(), TransportError> { + if self.is_closed() { return Err(TransportError::Closed); } - // Direct while there is one: a hop is what you use when there is - // nothing better, never a preference. - if let Some(direct) = self.direct() - && !direct.is_closed() - { - return direct.send(wrap(TAG_DIRECT, None, &payload)); + if payload.len() > self.max_datagram_size() { + return Err(TransportError::TooLarge { + size: payload.len(), + limit: self.max_datagram_size(), + }); } - if let Some((_, hop)) = self.hop() { - let out = hop.send(wrap(TAG_TO_RELAY, Some(self.peer), &payload)); - if out.is_ok() { - self.tally.sent_via.fetch_add(1, Ordering::Relaxed); - } - return out; + let table = self.plane.table.load(); + let route = table + .routes + .get(self.peer.as_bytes()) + .ok_or(TransportError::Closed)?; + let hop = route + .select(&self.plane.local, flow) + .ok_or(TransportError::Closed)?; + hop.link.send(envelope::encode( + self.plane.local, + *self.peer.as_bytes(), + flow, + &payload, + ))?; + if route.hops > 1 { + self.plane.tally.sent_via.fetch_add(1, Ordering::Relaxed); } - Err(TransportError::Unreachable(format!( - "no path to {}", - self.peer.fmt_short() - ))) + Ok(()) } - fn recv(&self) -> BoxFuture<'_, Option> { Box::pin(async move { - let mut inbox = self.inbox_rx.lock().await; - loop { - if self.closed.load(Ordering::Relaxed) { - return None; - } - let direct = self.direct(); - let changed = self.changed.notified(); - let gone = self.gone.notified(); - - let raw = tokio::select! { - // Whatever arrived through somebody else, already - // stripped of its wrapper by the link it came in on. - injected = inbox.recv() => return injected, - raw = async { - match &direct { - Some(link) => link.recv().await, - // Nothing direct: wait for a path or an injection. - None => std::future::pending().await, - } - } => raw, - // The path changed underneath: look again. - () = changed => continue, - () = gone => return None, - }; - - let Some(raw) = raw else { - // The direct link ended. The peer may still be - // reachable through somebody, so this link is not over. - self.set_direct(None); - continue; - }; - match unwrap(raw) { - Some(Frame::Direct(payload)) => return Some(payload), - // This agent is the one in the middle. - Some(Frame::ToRelay { to, payload }) => { - if let Some(hub) = self.hub.upgrade() { - hub.forward(to, &self.protocol, self.peer, payload); - } - } - // Somebody passed this on for a peer we talk to. - Some(Frame::Relayed { from, payload }) => { - if let Some(hub) = self.hub.upgrade() { - hub.inject(from, &self.protocol, payload); - } - } - None => { - self.tally.dropped_unknown.fetch_add(1, Ordering::Relaxed); - } - } + let mut closed = self.closed.subscribe(); + if *closed.borrow_and_update() { + return None; } + let mut inbox = self.inbox_rx.lock().await; + tokio::select! { biased; _ = closed.changed() => None, frame = inbox.recv() => frame } }) } - fn closed(&self) -> BoxFuture<'_, ()> { Box::pin(async move { - loop { - if self.closed.load(Ordering::Relaxed) { - return; - } - self.gone.notified().await; + let mut closed = self.closed.subscribe(); + if !*closed.borrow_and_update() { + let _ = closed.changed().await; } }) } - fn is_closed(&self) -> bool { - self.closed.load(Ordering::Relaxed) + *self.closed.borrow() } - fn path_description(&self) -> String { - if let Some(direct) = self.direct() - && !direct.is_closed() - { - return direct.path_description(); - } - match self.hop() { - Some((peer, link)) => { - format!("via {} ({})", peer.fmt_short(), link.path_description()) - } - None => "no path".to_string(), + let table = self.plane.table.load(); + match table.routes.get(self.peer.as_bytes()) { + Some(route) if route.hops == 1 => route.next[0].link.path_description(), + Some(route) => format!( + "relay {} hops via {} ({} equal paths)", + route.hops, + hex::encode(route.next[0].peer), + route.next.len() + ), + None => "unreachable".into(), } } } #[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - - use super::*; - use crate::identity::{NetworkKeys, NetworkName, NetworkSecret}; - - fn network() -> NetworkId { - NetworkKeys::derive( - &NetworkName::new("relay").unwrap(), - &NetworkSecret::from_bytes([3u8; 32]).unwrap(), - ) - .network_id() - } - - fn peer(seed: u8) -> EndpointId { - iroh::SecretKey::from_bytes(&[seed; 32]).public() - } - - /// A link that records what was sent and can be fed what arrives. - #[derive(Debug)] - struct Wire { - network: NetworkId, - peer: EndpointId, - sent: Mutex>, - inbound: tokio::sync::Mutex>, - feed: mpsc::Sender, - closed: AtomicBool, - } - - impl Wire { - fn new(peer: EndpointId) -> Arc { - let (feed, inbound) = mpsc::channel(32); - Arc::new(Self { - network: network(), - peer, - sent: Mutex::new(Vec::new()), - inbound: tokio::sync::Mutex::new(inbound), - feed, - closed: AtomicBool::new(false), - }) - } - - fn sent(&self) -> Vec { - self.sent.lock().unwrap().clone() - } - - async fn arrive(&self, raw: Bytes) { - self.feed.send(raw).await.unwrap(); - } - } - - impl PacketLink for Wire { - fn network(&self) -> NetworkId { - self.network - } - fn peer(&self) -> EndpointId { - self.peer - } - fn max_datagram_size(&self) -> usize { - 1200 - } - fn send(&self, payload: Bytes) -> Result<(), TransportError> { - if self.closed.load(Ordering::Relaxed) { - return Err(TransportError::Closed); - } - self.sent.lock().unwrap().push(payload); - Ok(()) - } - fn recv(&self) -> BoxFuture<'_, Option> { - Box::pin(async move { self.inbound.lock().await.recv().await }) - } - fn closed(&self) -> BoxFuture<'_, ()> { - Box::pin(async move { std::future::pending().await }) - } - fn is_closed(&self) -> bool { - self.closed.load(Ordering::Relaxed) - } - fn path_description(&self) -> String { - "test wire".into() - } - } - - #[test] - fn a_datagram_survives_the_wrapping_and_a_broken_one_is_refused() { - let id = peer(1); - let direct = wrap(TAG_DIRECT, None, b"hello"); - assert_eq!(unwrap(direct).unwrap(), Frame::Direct(Bytes::from("hello"))); - - let onward = wrap(TAG_TO_RELAY, Some(id), b"hello"); - assert_eq!( - unwrap(onward).unwrap(), - Frame::ToRelay { - to: id, - payload: Bytes::from("hello") - } - ); - - // Bytes off the network: nothing is read before it is checked. - assert!(unwrap(Bytes::new()).is_none()); - assert!(unwrap(Bytes::from_static(&[TAG_TO_RELAY, 1, 2, 3])).is_none()); - assert!(unwrap(Bytes::from_static(&[200, 1, 2, 3])).is_none()); - } - - #[tokio::test] - async fn a_direct_link_is_preferred_and_a_hop_is_used_when_there_is_none() { - let hub = RelayHub::new(network()); - let (them, middle) = (peer(1), peer(2)); - - let to_middle = Wire::new(middle); - hub.set_direct(middle, "test-ip", to_middle.clone()); - - // No direct link to them: nothing to send on, and nothing invented. - let link = hub.link(them, "test-ip"); - assert!(link.send(Bytes::from("x")).is_err()); - assert_eq!(link.via(), None); - - // Routed through the middle: the datagram goes out on that link, - // wrapped with who it is for. - hub.set_hop(them, "test-ip", Some(middle)); - assert_eq!(link.via(), Some(middle)); - link.send(Bytes::from("through you")).unwrap(); - let sent = to_middle.sent(); - assert_eq!(sent.len(), 1); - assert_eq!( - unwrap(sent[0].clone()).unwrap(), - Frame::ToRelay { - to: them, - payload: Bytes::from("through you") - } - ); - assert_eq!(hub.counters().sent_via, 1); - - // A direct link appears: it is used, and the hop is not. - let to_them = Wire::new(them); - hub.set_direct(them, "test-ip", to_them.clone()); - assert_eq!(link.via(), None); - link.send(Bytes::from("straight")).unwrap(); - assert_eq!( - unwrap(to_them.sent()[0].clone()).unwrap(), - Frame::Direct(Bytes::from("straight")) - ); - assert_eq!(to_middle.sent().len(), 1, "nothing more went the long way"); - } - - #[tokio::test] - async fn the_one_in_the_middle_passes_it_on_without_reading_it() { - // C between A and B. A's datagram arrives wrapped for B; C puts it - // on its link to B, saying who it came from, and never sees inside. - let hub = RelayHub::new(network()); - let (a, b) = (peer(1), peer(2)); - let from_a = Wire::new(a); - let to_b = Wire::new(b); - hub.set_direct(a, "test-ip", from_a.clone()); - hub.set_direct(b, "test-ip", to_b.clone()); - - let a_link = hub.link(a, "test-ip"); - let reading = tokio::spawn(async move { a_link.recv().await }); - - from_a.arrive(wrap(TAG_TO_RELAY, Some(b), b"opaque")).await; - // It is passed on rather than returned to the protocol here. - tokio::time::timeout(std::time::Duration::from_millis(200), async { - while to_b.sent().is_empty() { - tokio::task::yield_now().await; - } - }) - .await - .expect("it was passed on"); - assert_eq!( - unwrap(to_b.sent()[0].clone()).unwrap(), - Frame::Relayed { - from: a, - payload: Bytes::from("opaque") - } - ); - assert_eq!(hub.counters().forwarded, 1); - assert!(!reading.is_finished(), "the protocol was not handed it"); - - // Nowhere to put it is a drop and a count, never an error upwards. - from_a - .arrive(wrap(TAG_TO_RELAY, Some(peer(9)), b"nobody")) - .await; - tokio::time::timeout(std::time::Duration::from_millis(200), async { - while hub.counters().dropped_no_link == 0 { - tokio::task::yield_now().await; - } - }) - .await - .expect("counted"); - reading.abort(); - } - - #[tokio::test] - async fn what_arrives_through_somebody_is_handed_to_the_right_peer() { - // B's side: a datagram from A arrives on the link with C, and the - // protocol reading A's link is the one that gets it — otherwise - // the packet would be attributed to C and dropped as coming from - // an address C does not hold. - let hub = RelayHub::new(network()); - let (a, c) = (peer(1), peer(3)); - let from_c = Wire::new(c); - hub.set_direct(c, "test-ip", from_c.clone()); - let a_link = hub.link(a, "test-ip"); - let c_link = hub.link(c, "test-ip"); - - let reading_a = tokio::spawn(async move { a_link.recv().await }); - let driving_c = tokio::spawn(async move { c_link.recv().await }); - - from_c.arrive(wrap(TAG_RELAYED, Some(a), b"from a")).await; - let got = tokio::time::timeout(std::time::Duration::from_secs(2), reading_a) - .await - .expect("delivered") - .unwrap(); - assert_eq!(got, Some(Bytes::from("from a"))); - assert_eq!(hub.counters().received_via, 1); - driving_c.abort(); - } - - #[tokio::test] - async fn a_link_outlives_the_path_under_it() { - // The protocol's tunnel must not be torn down because a path - // changed: the link object is the peer, not the way to it. - let hub = RelayHub::new(network()); - let (them, middle) = (peer(1), peer(2)); - let direct = Wire::new(them); - hub.set_direct(them, "test-ip", direct.clone()); - let link = hub.link(them, "test-ip"); - assert!(!link.is_closed()); - - hub.clear_direct(them, "test-ip"); - assert!( - !link.is_closed(), - "still the same peer, still the same link" - ); - assert!( - link.send(Bytes::from("x")).is_err(), - "but nothing to send on" - ); - - let hop = Wire::new(middle); - hub.set_direct(middle, "test-ip", hop.clone()); - hub.set_hop(them, "test-ip", Some(middle)); - link.send(Bytes::from("x")).unwrap(); - assert_eq!(hop.sent().len(), 1); - - // The peer going away is what closes it. - hub.remove_peer(them); - assert!(link.is_closed()); - assert!(link.send(Bytes::from("x")).is_err()); - } - - #[tokio::test] - async fn the_size_a_protocol_may_use_does_not_change_with_the_path() { - let hub = RelayHub::new(network()); - let (them, middle) = (peer(1), peer(2)); - hub.set_direct(them, "test-ip", Wire::new(them)); - hub.set_direct(middle, "test-ip", Wire::new(middle)); - let link = hub.link(them, "test-ip"); - let direct_size = link.max_datagram_size(); - assert_eq!(direct_size, 1200 - RELAY_OVERHEAD); - - hub.set_hop(them, "test-ip", Some(middle)); - hub.clear_direct(them, "test-ip"); - assert_eq!( - link.max_datagram_size(), - direct_size, - "a tunnel that resized on every path change would be worse" - ); - } -} +#[path = "relay_tests.rs"] +mod tests; diff --git a/crates/tsunagi/src/dataplane/relay_tests.rs b/crates/tsunagi/src/dataplane/relay_tests.rs new file mode 100644 index 0000000..d6131f4 --- /dev/null +++ b/crates/tsunagi/src/dataplane/relay_tests.rs @@ -0,0 +1,287 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +use super::*; +use crate::identity::{NetworkKeys, NetworkName, NetworkSecret}; +use std::sync::atomic::AtomicBool; + +fn network() -> NetworkId { + NetworkKeys::derive( + &NetworkName::new("routing-tests").unwrap(), + &NetworkSecret::from_bytes([3; 32]).unwrap(), + ) + .network_id() +} +fn peer(seed: u8) -> EndpointId { + iroh::SecretKey::from_bytes(&[seed; 32]).public() +} +fn id(seed: u8) -> PeerId { + *peer(seed).as_bytes() +} + +#[derive(Debug)] +struct Wire { + peer: EndpointId, + sent: Mutex>, + feed: mpsc::Sender, + inbound: tokio::sync::Mutex>, + dead: AtomicBool, + count: AtomicU64, + record: bool, +} +impl Wire { + fn new(seed: u8, record: bool) -> Arc { + let (feed, inbound) = mpsc::channel(32); + Arc::new(Self { + peer: peer(seed), + sent: Mutex::default(), + feed, + inbound: tokio::sync::Mutex::new(inbound), + dead: AtomicBool::new(false), + count: AtomicU64::new(0), + record, + }) + } + fn take(&self) -> Vec { + std::mem::take(&mut self.sent.lock().unwrap()) + } +} +impl PacketLink for Wire { + fn network(&self) -> NetworkId { + network() + } + fn peer(&self) -> EndpointId { + self.peer + } + fn max_datagram_size(&self) -> usize { + MAX_DATA_DATAGRAM + } + fn send(&self, frame: Bytes) -> Result<(), TransportError> { + if self.is_closed() { + return Err(TransportError::Closed); + } + self.count.fetch_add(1, Ordering::Relaxed); + if self.record { + self.sent.lock().unwrap().push(frame); + } + Ok(()) + } + fn recv(&self) -> BoxFuture<'_, Option> { + Box::pin(async move { self.inbound.lock().await.recv().await }) + } + fn closed(&self) -> BoxFuture<'_, ()> { + Box::pin(std::future::pending()) + } + fn is_closed(&self) -> bool { + self.dead.load(Ordering::Relaxed) + } + fn path_description(&self) -> String { + "test-direct".into() + } +} + +fn topology(hub: &RelayHub, protocol: &str, edges: &[(u8, &[u8])], members: &[u8]) { + hub.set_topology( + protocol, + edges + .iter() + .map(|(a, bs)| (id(*a), bs.iter().map(|b| id(*b)).collect())) + .collect(), + members.iter().map(|p| id(*p)).collect(), + ); +} + +#[tokio::test] +async fn ecmp_is_per_flow_direct_wins_and_existing_tunnel_survives_changes() { + let hub = RelayHub::new(network(), peer(1)); + let b = Wire::new(2, true); + let c = Wire::new(3, true); + hub.set_direct(peer(2), "ip", b.clone()); + hub.set_direct(peer(3), "ip", c.clone()); + topology(&hub, "ip", &[(2, &[4]), (3, &[4])], &[1, 2, 3, 4]); + let link = hub.link(peer(4), "ip"); + let mut chosen = HashMap::new(); + for _ in 0..8 { + for flow in 0..100 { + link.send_flow(Bytes::from_static(b"ciphertext"), flow) + .unwrap(); + } + for (hop, wire) in [(2, &b), (3, &c)] { + for frame in wire.take() { + let header = envelope::decode(&frame).unwrap(); + assert_eq!(header.source, id(1)); + assert_eq!(header.destination, id(4)); + if let Some(old) = chosen.insert(header.flow, hop) { + assert_eq!(old, hop); + } + } + } + } + assert_eq!( + chosen.values().copied().collect::>(), + HashSet::from([2, 3]) + ); + let direct = Wire::new(4, true); + hub.set_direct(peer(4), "ip", direct.clone()); + link.send_flow(Bytes::from_static(b"direct"), 8).unwrap(); + assert_eq!(direct.take().len(), 1); + assert!(b.take().is_empty() && c.take().is_empty()); + hub.clear_direct(peer(4), "ip"); + b.dead.store(true, Ordering::Relaxed); + for flow in 0..100 { + link.send_flow(Bytes::from_static(b"fallback"), flow) + .unwrap(); + } + assert_eq!(c.take().len(), 100); + hub.clear_direct(peer(2), "ip"); + topology(&hub, "ip", &[(3, &[])], &[1, 2, 3, 4]); + assert!(link.send(Bytes::new()).is_err()); + assert!(!link.is_closed()); + assert!(Arc::ptr_eq(&link, &hub.link(peer(4), "ip"))); + hub.remove_peer(peer(4)); + assert!(link.is_closed()); + tokio::time::timeout(std::time::Duration::from_secs(1), link.closed()) + .await + .unwrap(); + assert!(link.recv().await.is_none()); + hub.close(); +} + +#[tokio::test] +async fn transport_reader_forwards_without_any_plugin_reader_and_isolates_protocols() { + let hub = RelayHub::new(network(), peer(2)); + let a = Wire::new(1, true); + let c = Wire::new(3, true); + hub.set_direct(peer(1), "ip", a.clone()); + hub.set_direct(peer(3), "ip", c.clone()); + topology(&hub, "ip", &[(3, &[4])], &[1, 2, 3, 4]); + let frame = envelope::encode(id(1), id(4), 91, b"end-to-end encrypted"); + let pointer = frame.as_ptr(); + a.feed.send(frame).await.unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(1), async { + while hub.counters().forwarded == 0 { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + let sent = c.take(); + assert_eq!(sent[0].as_ptr(), pointer); + assert_eq!( + envelope::decode(&sent[0]).unwrap().remaining, + ROUTING_HOP_LIMIT - 1 + ); + assert_eq!(&sent[0][RELAY_OVERHEAD..], b"end-to-end encrypted"); + let isolated = hub.link(peer(4), "other-ip"); + assert!(isolated.send(Bytes::new()).is_err()); + assert_eq!(hub.counters().forwarded, 1); + hub.close(); +} + +#[tokio::test] +async fn local_delivery_is_bounded_and_malformed_or_unknown_sources_are_dropped() { + let hub = RelayHub::new(network(), peer(2)); + let a = Wire::new(1, true); + hub.set_direct(peer(1), "ip", a); + let link = hub.link(peer(1), "ip"); + let plane = link.plane.clone(); + for _ in 0..INBOX + 1 { + plane.receive(id(1), envelope::encode(id(1), id(2), 0, b"ip")); + } + assert_eq!(hub.counters().dropped_congested, 1); + for _ in 0..INBOX { + assert_eq!(link.recv().await.unwrap(), Bytes::from_static(b"ip")); + } + plane.receive(id(1), Bytes::from_static(b"broken")); + plane.receive(id(1), envelope::encode(id(9), id(2), 0, b"unknown member")); + assert_eq!(hub.counters().dropped_unknown, 2); + plane.receive( + id(1), + envelope::encode(id(1), id(9), 0, b"unknown destination"), + ); + assert_eq!(hub.counters().dropped_no_link, 1); + hub.close(); +} + +#[tokio::test] +async fn inconsistent_tables_cannot_loop_beyond_the_hop_limit() { + let a = RelayHub::new(network(), peer(1)); + let b = RelayHub::new(network(), peer(2)); + let ab = Wire::new(2, true); + let ba = Wire::new(1, true); + a.set_direct(peer(2), "ip", ab.clone()); + b.set_direct(peer(1), "ip", ba.clone()); + // Deliberately inconsistent snapshots while updates are in flight. + topology(&a, "ip", &[(2, &[4])], &[1, 2, 3, 4]); + topology(&b, "ip", &[(1, &[4])], &[1, 2, 3, 4]); + let ap = a.link(peer(4), "ip").plane.clone(); + let bp = b.link(peer(4), "ip").plane.clone(); + let mut frame = envelope::encode(id(3), id(4), 7, b"transit"); + for turn in 0..ROUTING_HOP_LIMIT { + let (plane, incoming, wire) = if turn % 2 == 0 { + (&ap, id(2), &ab) + } else { + (&bp, id(1), &ba) + }; + plane.receive(incoming, frame); + let sent = wire.take(); + if turn == ROUTING_HOP_LIMIT - 1 { + assert!(sent.is_empty()); + break; + } + assert_eq!(sent.len(), 1); + frame = sent.into_iter().next().unwrap(); + } + assert_eq!( + a.counters().forwarded + b.counters().forwarded, + u64::from(ROUTING_HOP_LIMIT - 1) + ); + assert_eq!( + a.counters().dropped_hop_limit + b.counters().dropped_hop_limit, + 1 + ); + a.close(); + b.close(); +} + +/// Measures the actual synchronous transit routine, including header checks, +/// snapshot lookup, ECMP, in-place TTL update and mock transport submission. +/// Packet construction, encryption and socket I/O are outside this measurement. +#[tokio::test] +#[ignore = "manual release-mode forwarding microbenchmark"] +async fn forwarding_benchmark() { + let hub = RelayHub::new(network(), peer(2)); + let wire = Wire::new(3, false); + hub.set_direct(peer(3), "ip", wire.clone()); + let alternate = Wire::new(5, false); + hub.set_direct(peer(5), "ip", alternate.clone()); + topology(&hub, "ip", &[(3, &[4]), (5, &[4])], &[1, 2, 3, 4, 5]); + let plane = hub.link(peer(4), "ip").plane.clone(); + let source = id(1); + let destination = id(4); + let incoming = id(1); + for size in [64, 1280] { + let mut elapsed = std::time::Duration::ZERO; + const COUNT: usize = 10000; + const BATCHES: usize = 100; + for _ in 0..BATCHES { + let frames: Vec<_> = (0..COUNT) + .map(|flow| envelope::encode(source, destination, flow as u64, &vec![42; size])) + .collect(); + let start = std::time::Instant::now(); + for frame in frames { + plane.receive(incoming, std::hint::black_box(frame)); + } + elapsed += start.elapsed(); + } + let count = (COUNT * BATCHES) as f64; + println!( + "transit {size} B: {:.1} ns/packet, {:.2} Mpps (1M packets, two equal paths)", + elapsed.as_nanos() as f64 / count, + count / elapsed.as_secs_f64() / 1e6 + ); + } + assert_eq!( + wire.count.load(Ordering::Relaxed) + alternate.count.load(Ordering::Relaxed), + 2000000 + ); + hub.close(); +} diff --git a/crates/tsunagi/src/dataplane/routing/envelope.rs b/crates/tsunagi/src/dataplane/routing/envelope.rs new file mode 100644 index 0000000..b61b9c9 --- /dev/null +++ b/crates/tsunagi/src/dataplane/routing/envelope.rs @@ -0,0 +1,84 @@ +//! Fixed header. Transit changes one byte in an owned receive buffer. +use super::{FlowId, PeerId}; +use crate::config::{MAX_DATA_DATAGRAM, ROUTING_HOP_LIMIT}; +use bytes::{BufMut, Bytes, BytesMut}; + +pub(crate) const HEADER: usize = 2 + 32 + 32 + 8; +const VERSION: u8 = 1; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct Header { + pub source: PeerId, + pub destination: PeerId, + pub remaining: u8, + pub flow: FlowId, +} + +pub(crate) fn decode(bytes: &[u8]) -> Option
{ + if bytes.len() < HEADER + || bytes.len() > MAX_DATA_DATAGRAM + || bytes[0] != VERSION + || bytes[1] == 0 + || bytes[1] > ROUTING_HOP_LIMIT + { + return None; + } + Some(Header { + source: bytes[2..34].try_into().ok()?, + destination: bytes[34..66].try_into().ok()?, + remaining: bytes[1], + flow: u64::from_be_bytes(bytes[66..74].try_into().ok()?), + }) +} + +pub(crate) fn encode(source: PeerId, destination: PeerId, flow: FlowId, payload: &[u8]) -> Bytes { + let mut bytes = BytesMut::with_capacity(HEADER + payload.len()); + bytes.put_u8(VERSION); + bytes.put_u8(ROUTING_HOP_LIMIT); + bytes.extend_from_slice(&source); + bytes.extend_from_slice(&destination); + bytes.put_u64(flow); + bytes.extend_from_slice(payload); + bytes.freeze() +} + +pub(crate) fn decrement(bytes: Bytes) -> Bytes { + let mut bytes = match bytes.try_into_mut() { + Ok(bytes) => bytes, + Err(bytes) => BytesMut::from(bytes.as_ref()), + }; + // Called only after decode and after excluding remaining <= 1. + bytes[1] -= 1; + bytes.freeze() +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use super::*; + #[test] + fn transit_reuses_the_buffer_and_rejects_invalid_headers() { + let frame = encode([1; 32], [2; 32], 17, &[42; 1280]); + let address = frame.as_ptr(); + let frame = decrement(frame); + assert_eq!( + frame.as_ptr(), + address, + "no payload copy for an owned buffer" + ); + assert_eq!(decode(&frame).unwrap().remaining, ROUTING_HOP_LIMIT - 1); + assert_eq!(&frame[HEADER..], &[42; 1280]); + for length in 0..HEADER { + assert!(decode(&frame[..length]).is_none()); + } + for ttl in [0, ROUTING_HOP_LIMIT + 1] { + let mut bad = frame.to_vec(); + bad[1] = ttl; + assert!(decode(&bad).is_none()); + } + let mut bad = frame.to_vec(); + bad[0] = 99; + assert!(decode(&bad).is_none()); + assert!(decode(&vec![1; MAX_DATA_DATAGRAM + 1]).is_none()); + } +} diff --git a/crates/tsunagi/src/dataplane/routing/flow.rs b/crates/tsunagi/src/dataplane/routing/flow.rs new file mode 100644 index 0000000..ad47f52 --- /dev/null +++ b/crates/tsunagi/src/dataplane/routing/flow.rs @@ -0,0 +1,125 @@ +//! Optional IP flow classifier for IP plugins, called before encryption. +//! The router itself treats this identifier and the payload as opaque. + +use super::FlowId; + +fn hash(parts: &[&[u8]]) -> FlowId { + let mut value = 0xcbf29ce484222325u64; + for part in parts { + for byte in *part { + value = (value ^ u64::from(*byte)).wrapping_mul(0x100000001b3); + } + } + value +} + +/// Stable directional IP flow hash, independent of TCP sequence numbers, +/// checksums and packet contents. Fragmented traffic uses addresses/protocol +/// instead of ports: even non-initial fragments and subsequent fragmented +/// datagrams stay together. This deliberately coalesces fragmented flows. +/// Malformed packets get a coarse hash; parsing is bounded and never allocates. +pub fn ip_flow(packet: &[u8]) -> FlowId { + match packet.first().map(|b| b >> 4) { + Some(4) if packet.len() >= 20 => { + let addresses = &packet[12..20]; + let protocol = &packet[9..10]; + let offset = usize::from(packet[0] & 15) * 4; + if u16::from_be_bytes([packet[6], packet[7]]) & 0x3fff != 0 { + return hash(&[addresses, protocol]); + } + if offset >= 20 + && matches!(packet[9], 6 | 17) + && let Some(ports) = packet.get(offset..offset + 4) + { + return hash(&[addresses, protocol, ports]); + } + hash(&[addresses, protocol]) + } + Some(6) if packet.len() >= 40 => { + let addresses = &packet[8..40]; + let mut protocol = packet[6]; + let mut offset = 40; + for _ in 0..8 { + match protocol { + 44 => { + if let Some(fragment) = packet.get(offset..offset + 8) { + return hash(&[addresses, &fragment[..1]]); + } + break; + } + 0 | 43 | 60 | 51 => { + let Some(header) = packet.get(offset..offset + 2) else { + break; + }; + let size = if protocol == 51 { + (usize::from(header[1]) + 2) * 4 + } else { + (usize::from(header[1]) + 1) * 8 + }; + protocol = header[0]; + offset += size; + } + 6 | 17 => { + if let Some(ports) = packet.get(offset..offset + 4) { + return hash(&[addresses, &[protocol], ports]); + } + break; + } + _ => break, + } + } + hash(&[addresses, &[protocol]]) + } + _ => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tcp_flow_ignores_packet_contents_but_distinguishes_ports() { + let mut packet = [0u8; 80]; + packet[0] = 0x45; + packet[9] = 6; + packet[12..24].copy_from_slice(&[10, 0, 0, 1, 10, 0, 0, 2, 0, 22, 1, 2]); + let flow = ip_flow(&packet); + packet[4] = 123; // IP id changes for each unfragmented packet. + packet[24..].fill(47); + assert_eq!(flow, ip_flow(&packet)); + packet[23] = 3; + assert_ne!(flow, ip_flow(&packet)); + packet[6] = 0x20; + let fragment_flow = ip_flow(&packet); + packet[6] = 0; + packet[7] = 1; + packet[20..].fill(99); + assert_eq!(fragment_flow, ip_flow(&packet)); + packet[4] = 97; + assert_eq!(fragment_flow, ip_flow(&packet)); + } + + #[test] + fn ipv6_extensions_and_fragments_are_bounded_and_stable() { + let mut packet = [0u8; 80]; + packet[0] = 0x60; + packet[6] = 0; + packet[40] = 17; + packet[48..52].copy_from_slice(&[1, 2, 3, 4]); + let flow = ip_flow(&packet); + packet[52..].fill(5); + assert_eq!(flow, ip_flow(&packet)); + packet[50] = 9; + assert_ne!(flow, ip_flow(&packet)); + packet[6] = 44; + packet[40] = 6; + let flow = ip_flow(&packet); + packet[42] = 32; + packet[48..].fill(3); + assert_eq!(flow, ip_flow(&packet)); + for len in 0..packet.len() { + let _ = ip_flow(&packet[..len]); + } + } +} diff --git a/crates/tsunagi/src/dataplane/routing/mod.rs b/crates/tsunagi/src/dataplane/routing/mod.rs new file mode 100644 index 0000000..7736477 --- /dev/null +++ b/crates/tsunagi/src/dataplane/routing/mod.rs @@ -0,0 +1,131 @@ +//! Transport-independent routing over opaque identities. Tables are built on +//! topology changes, never on the packet path. No WireGuard or iroh APIs here. +pub(crate) mod envelope; +pub mod flow; + +use std::collections::{BTreeSet, HashMap, VecDeque}; + +/// Opaque authenticated identity; the transport adapter validates its keys. +pub type PeerId = [u8; 32]; +/// Stable flow identifier, carried unchanged between transit routers. +pub type FlowId = u64; + +/// A shortest route with all equal-cost first hops in stable order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Route { + /// Number of transport links to the destination. + pub hops: u8, + /// Equal-cost first hops. Direct destinations have one entry. + pub next_hops: Box<[PeerId]>, +} + +/// Immutable shortest-path table, replaced when connectivity changes. +#[derive(Debug, Clone, Default)] +pub struct RoutingTable { + routes: HashMap, +} + +impl RoutingTable { + /// Computes shortest directed paths bounded by `hop_limit`. + /// The caller supplies only authenticated, fresh, compatible edges. + pub fn build(local: PeerId, graph: &HashMap>, hop_limit: u8) -> Self { + let mut distances = HashMap::from([(local, 0u8)]); + let mut first_hops: HashMap> = HashMap::new(); + let mut pending = VecDeque::from([local]); + while let Some(node) = pending.pop_front() { + let distance = distances[&node]; + if distance >= hop_limit { + continue; + } + let inherited = first_hops.get(&node).cloned().unwrap_or_default(); + for &next in graph.get(&node).into_iter().flatten() { + let next_distance = distance + 1; + match distances.get(&next) { + Some(&known) if known < next_distance => continue, + None => { + distances.insert(next, next_distance); + pending.push_back(next); + } + _ => {} + } + let hops = first_hops.entry(next).or_default(); + if node == local { + hops.insert(next); + } else { + hops.extend(inherited.iter().copied()); + } + } + } + Self { + routes: first_hops + .into_iter() + .map(|(peer, hops)| { + ( + peer, + Route { + hops: distances[&peer], + next_hops: hops.into_iter().collect(), + }, + ) + }) + .collect(), + } + } + /// Precomputed route to a destination. + pub fn get(&self, destination: &PeerId) -> Option<&Route> { + self.routes.get(destination) + } + /// All reachable destinations, excluding ourselves. + pub fn iter(&self) -> impl Iterator { + self.routes.iter() + } +} + +/// Stable ECMP mixing, without random state or hashing the payload per hop. +pub(crate) fn mix64(mut value: u64) -> u64 { + value ^= value >> 30; + value = value.wrapping_mul(0xbf58476d1ce4e5b9); + value ^= value >> 27; + value = value.wrapping_mul(0x94d049bb133111eb); + value ^ (value >> 31) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use super::*; + fn p(n: u8) -> PeerId { + [n; 32] + } + #[test] + fn shortest_paths_keep_equal_cost_hops_and_prefer_direct_links() { + let mut graph = HashMap::from([ + (p(1), vec![p(3), p(2)]), + (p(2), vec![p(4)]), + (p(3), vec![p(4)]), + (p(4), vec![p(5)]), + (p(5), vec![p(1)]), + ]); + let routes = RoutingTable::build(p(1), &graph, 16); + assert_eq!(routes.get(&p(5)).unwrap().hops, 3); + assert_eq!(&*routes.get(&p(5)).unwrap().next_hops, &[p(2), p(3)]); + assert!(routes.get(&p(1)).is_none()); + assert!(routes.get(&p(9)).is_none()); + graph.get_mut(&p(1)).unwrap().push(p(5)); + assert_eq!( + RoutingTable::build(p(1), &graph, 16).get(&p(5)).unwrap(), + &Route { + hops: 1, + next_hops: Box::new([p(5)]) + } + ); + } + #[test] + fn topology_removal_and_hop_budget_remove_impossible_routes() { + let mut graph = HashMap::from([(p(1), vec![p(2)]), (p(2), vec![p(3)]), (p(3), vec![p(4)])]); + assert!(RoutingTable::build(p(1), &graph, 2).get(&p(4)).is_none()); + assert!(RoutingTable::build(p(1), &graph, 3).get(&p(4)).is_some()); + graph.remove(&p(2)); + assert!(RoutingTable::build(p(1), &graph, 16).get(&p(4)).is_none()); + } +} diff --git a/crates/tsunagi/src/dataplane/transport/mod.rs b/crates/tsunagi/src/dataplane/transport/mod.rs index 6fd40f5..f351254 100644 --- a/crates/tsunagi/src/dataplane/transport/mod.rs +++ b/crates/tsunagi/src/dataplane/transport/mod.rs @@ -88,6 +88,16 @@ pub trait PacketLink: Send + Sync + std::fmt::Debug + 'static { /// handed to the transport, nothing more. fn send(&self, payload: Bytes) -> Result<(), TransportError>; + /// Sends a datagram with an opaque stable flow identifier. Plugins derive + /// this before encryption; transports that do not route can ignore it. + fn send_flow( + &self, + payload: Bytes, + _flow: super::routing::FlowId, + ) -> Result<(), TransportError> { + self.send(payload) + } + /// Receives the next datagram, or `None` once the link is finished. fn recv(&self) -> BoxFuture<'_, Option>; diff --git a/crates/tsunagi/src/proto/message.rs b/crates/tsunagi/src/proto/message.rs index bb3b9c2..21eb122 100644 --- a/crates/tsunagi/src/proto/message.rs +++ b/crates/tsunagi/src/proto/message.rs @@ -20,7 +20,7 @@ use crate::error::ProtocolError; /// The version in the ALPN is the wire-compatibility version of the control /// protocol. It is independent of the network identity scheme version, so /// bumping it must not change any existing [`crate::NetworkId`]. -pub const ALPN: &[u8] = b"tsunagi/ctrl/1"; +pub const ALPN: &[u8] = b"tsunagi/ctrl/2"; /// ALPN of the tsunagi data plane. /// @@ -29,11 +29,10 @@ pub const ALPN: &[u8] = b"tsunagi/ctrl/1"; /// saturated or broken data plane cannot disturb control traffic, and the /// transport underneath can be replaced without touching the control protocol. /// -/// Version 3 fragments logical datagrams below the peer-relay envelope, so -/// the overlay's MTU is independent of the current QUIC path MTU. Older data -/// versions cannot form a data link; control and persistent identities remain -/// compatible. Both ends, including any intermediate peer, must be upgraded. -pub const DATA_ALPN: &[u8] = b"tsunagi/data/3"; +/// Version 4 adds source, destination, hop limit and stable flow id above +/// transport fragmentation. All members must upgrade together. Persistent +/// identities and network configuration do not change. +pub const DATA_ALPN: &[u8] = b"tsunagi/data/4"; /// Largest plugin protocol identifier accepted when opening a data channel. pub const MAX_DATA_PROTOCOL_LEN: usize = 32; @@ -42,7 +41,7 @@ pub const MAX_DATA_PROTOCOL_LEN: usize = 32; pub const MAX_SIGNATURE_LEN: usize = 64; /// Control protocol version carried inside the handshake. -pub const PROTOCOL_VERSION: u16 = 1; +pub const PROTOCOL_VERSION: u16 = 2; /// First message of the handshake, sent by the initiator. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -154,8 +153,8 @@ pub enum ControlMessage { /// and repairs itself; and volatile, so it lives here and not in /// signed state, which is for what has to survive a member being away. Reachable { - /// The peers, as raw endpoint keys. - peers: Vec<[u8; 32]>, + /// Direct links, scoped to the plugin protocol they actually carry. + links: Vec, }, /// Graceful goodbye. /// @@ -183,6 +182,15 @@ pub struct PeerHint { pub addrs: Vec, } +/// One directly observed edge, advertised only by its authenticated source. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ReachableLink { + /// The other end of this transport link. + pub peer: [u8; 32], + /// Protocol carried by the link. + pub protocol: String, +} + /// Longest address text accepted in a hint. pub const MAX_HINT_ADDR_LEN: usize = 128; @@ -256,10 +264,18 @@ pub fn validate(message: &ControlMessage, limits: &Limits) -> Result<(), Protoco } } } - ControlMessage::Reachable { peers } => { - // One entry per member at most; a sender claiming more than a - // network can hold is not describing this network. - check_len("reachable.peers", peers.len(), limits.max_state_records)?; + ControlMessage::Reachable { links } => { + check_len("reachable.links", links.len(), limits.max_state_records)?; + for link in links { + if link.protocol.is_empty() { + return Err(ProtocolError::Malformed("empty routing protocol")); + } + check_len( + "reachable.protocol", + link.protocol.len(), + MAX_PROTOCOL_ID_LEN, + )?; + } } ControlMessage::Announce(announcement) => { check_len( diff --git a/crates/tsunagi/tests/mainline_socket.rs b/crates/tsunagi/tests/mainline_socket.rs new file mode 100644 index 0000000..5daba80 --- /dev/null +++ b/crates/tsunagi/tests/mainline_socket.rs @@ -0,0 +1,68 @@ +//! The real Mainline UDP receive loop must be quiet while idle on Windows too. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use std::net::Ipv4Addr; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use tracing_subscriber::layer::SubscriberExt; + +struct SocketWarnings(Arc); + +impl tracing_subscriber::Layer for SocketWarnings { + fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) { + if event.metadata().target() == "mainline::rpc::socket" + && *event.metadata().level() <= tracing::Level::WARN + { + self.0.fetch_add(1, Ordering::Relaxed); + } + } +} + +#[tokio::test] +async fn an_idle_dht_socket_stays_quiet_and_still_answers_packets() { + // Mainline owns an OS thread, so a thread-local subscriber cannot see its + // events. This integration-test binary has just this test and installs + // its own subscriber; it cannot affect the application or other binaries. + let warnings = Arc::new(AtomicUsize::new(0)); + tracing::subscriber::set_global_default( + tracing_subscriber::registry().with(SocketWarnings(Arc::clone(&warnings))), + ) + .unwrap(); + + tokio::time::timeout(Duration::from_secs(5), async { + let dht = mainline::Dht::builder() + .no_bootstrap() + .server_mode() + .bind_address(Ipv4Addr::LOCALHOST) + .port(0) + .build() + .unwrap() + .as_async(); + + let address = dht.info().await.local_addr(); + // Each info response comes from the actor after another receive-loop + // iteration. With no peers or packets, these exercise real socket + // read timeouts, without an arbitrary sleep or public bootstrap. + for _ in 0..8 { + assert_eq!(dht.info().await.local_addr(), address); + } + assert_eq!(warnings.load(Ordering::Relaxed), 0, "idle receive warnings"); + + let client = tokio::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)) + .await + .unwrap(); + let ping = b"d1:ad2:id20:abcdefghijklmnopqrste1:q4:ping1:t4:ping1:y1:qe"; + client.send_to(ping, address).await.unwrap(); + let mut response = [0u8; 2048]; + let (len, from) = client.recv_from(&mut response).await.unwrap(); + assert_eq!(from, std::net::SocketAddr::V4(address)); + let response = &response[..len]; + assert!(response.windows(9).any(|field| field == b"1:t4:ping")); + assert!(response.windows(6).any(|field| field == b"1:y1:r")); + assert_eq!(warnings.load(Ordering::Relaxed), 0); + }) + .await + .expect("the DHT receive loop keeps making progress"); +} diff --git a/docs/architecture.md b/docs/architecture.md index 0f900a8..e079798 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -35,6 +35,16 @@ a `PluginContext` for re-announcements and error reports, and a bounded A data plane failure never stops the daemon: the control plane keeps running and the agent stays manageable. +## Userspace routing + +The routing engine builds shortest paths over opaque peer identifiers, with no +WireGuard or iroh dependency. The relay adapter binds those next hops to +`PacketLink` handles and publishes an immutable table per network and protocol. +One reader per raw transport forwards transit without entering the plugin or +TUN. End-to-end plugin links survive physical link changes. The existing +authenticated control mesh supplies first-hand topology; the data router does +not tunnel control sessions. See [routing.md](routing.md). + ## Module responsibilities | component | responsibility | @@ -47,7 +57,9 @@ and the agent stays manageable. | `storage` | mandatory state and the separately recoverable cache | | `state` | signed records that outlive a session, merged between replicas | | `dataplane::transport` | authenticated datagram links to peers; where reachability lives | -| `dataplane` | the contract a protocol implements, and nothing else | +| `dataplane::routing` | transport-independent graph, shortest paths and opaque flow identifiers | +| `dataplane::relay` | immutable forwarding snapshots and transport-to-transport transit | +| `dataplane` | the contract an IP protocol implements | | `overlay` | the one interface an agent owns: provisioning, the TUN, whose packet is whose | | `dns` | the DNS view of a network, and telling the system resolver about it | diff --git a/docs/protocol.md b/docs/protocol.md index 08b0d79..4951c5f 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -9,7 +9,7 @@ Two versions exist and are independent: - **Identity scheme**, `tsunagi-network-id-v1`. Frozen. Changing it creates a different network space for the same name and secret. -- **Control protocol**, ALPN `tsunagi/ctrl/1`, `PROTOCOL_VERSION = 1`. +- **Control protocol**, ALPN `tsunagi/ctrl/2`, `PROTOCOL_VERSION = 2`. Upgrading the crate or bumping the control protocol must never change an existing `NetworkId`. @@ -135,7 +135,7 @@ event does not report a network id. ## The data plane protocol IP plugin packets never travel on a control connection. They use their own -ALPN, `tsunagi/data/3`, on their own iroh connection: +ALPN, `tsunagi/data/4`, on their own iroh connection: ```text initiator -> responder : (the same membership handshake as above) @@ -172,9 +172,35 @@ holding up subsequent packets. Packet IDs are scoped to a QUIC connection; reassembly is created only after the membership handshake. All limits live in `config.rs`. There is no transport-layer retransmission added by this framing. -Version 3 is incompatible with previous data ALPNs. Upgrade both endpoints -and intermediate peers together. The control protocol, network secret, -device identity and stored network configuration are unchanged. +Version 4 adds a routing envelope inside the transport fragmentation framing: + +| field | bytes | encoding | +|---|---:|---| +| envelope version | 1 | `1` | +| remaining hops | 1 | initially `16`; transit decrements, drops at `1` | +| source peer | 32 | original endpoint public key | +| destination peer | 32 | final endpoint public key | +| flow id | 8 | big endian opaque stable hash supplied by the plugin | +| payload | remaining | end-to-end encrypted plugin bytes | + +The fixed header is 74 bytes. Zero/oversized hop limits, unknown versions, +short frames, oversized datagrams and unknown sources are rejected. A frame +addressed here is delivered to its source's protocol inbox; transit goes straight +to its next transport. The network and protocol are bound to the authenticated +transport connection, not supplied by the frame. Intermediate nodes cannot +decrypt or authenticate the inner WireGuard payload; the destination does that. +Flow ids are routing hints, not authorization proofs. + +Control ALPN 2 carries `Reachable { links: [{ peer, protocol }] }`. Each row +belongs to the authenticated sender and is replaced atomically, expires after +90 seconds, and is withdrawn on session closure. Only compatible authenticated +members enter a protocol's graph; local edges always come from actual links. +Announcements refresh on the existing control maintenance interval and are +sent immediately on link changes. Topology stays volatile, outside signed state. + +Upgrade both endpoints and intermediate peers together. Older control and data +ALPNs cannot interoperate. The network secret, device identity, identity +derivation, transcript encoding and stored network configuration are unchanged. Separate connections mean separate congestion control, so a saturated data plane cannot delay control messages, and a data plane failure cannot take the @@ -193,6 +219,8 @@ not affect other networks. | `Ping { seq, payload }` | small request used to verify the exchange | | `Pong { seq, payload }` | the echoed reply | | `State { records }` | a snapshot of signed records, merged into what the receiver holds | +| `Peers { peers }` | unverified member address hints, requiring their own handshake | +| `Reachable { links }` | sender's current direct data links, scoped by protocol | | `Bye { reason }` | graceful goodbye; not a revocation of anything | A `State` snapshot is merged, never substituted: an author missing from it is diff --git a/docs/routing.md b/docs/routing.md new file mode 100644 index 0000000..4567540 --- /dev/null +++ b/docs/routing.md @@ -0,0 +1,90 @@ +# Userspace mesh routing + +```text +local OS → TUN → IP plugin (encrypt) → router → transport peer link +transport peer link → router → transport peer link (transit) +transport peer link → router → IP plugin (decrypt) → TUN → local OS +``` + +The TUN is the boundary of the local host. The kernel sees one overlay route; +it never forwards transit traffic. WireGuard encryption remains end to end. +The core router sees only an opaque payload and routing metadata. iroh is the +current transport adapter, with fragmentation below the routing envelope so +the default 1280-byte interface MTU also works on small QUIC paths. + +## Graph and lifecycle + +Each authenticated control session advertises that peer's own direct data +links, including the plugin protocol. Tables are separate for each network +and protocol; only members offering a matching enabled protocol version enter +the graph. A snapshot refreshes on the maintenance interval, expires after +90 seconds, and is withdrawn when the session ends. Link arrival, closure and +changed announcements rebuild routes immediately. Unchanged announcements +refresh their age without republishing the forwarding snapshot. + +Breadth-first search computes the shortest directed paths and every equal-cost +first hop in sorted order. An actual direct link always wins. The first row is +always supplied by local transport state, never by a remote claim. Routes stop +at 16 links. A packet also carries a decreasing hop limit, bounding temporary +loops if different nodes have not yet received the same topology update. + +The existing control plane still forms authenticated pairwise sessions. +Routing provides multihop **data** paths among these members; it does not add +control-plane flooding or carry control sessions through the data plane. +Announcements are volatile, not durable membership or availability guarantees. + +## Packet path + +The control loop binds next hops to transport handles in an immutable table +and publishes it through `ArcSwap`. Readers do not acquire the topology mutex. +The transit routine validates the fixed 74-byte envelope, looks up the source +and destination, selects a cached next hop, decrements one byte, and calls the +transport's synchronous datagram send. It never searches the graph, formats an +endpoint string, validates an Ed25519 key, decrypts, or touches TUN. + +An exclusively owned receive buffer is reused when decrementing the hop limit. +Shared buffers require a copy through the safe `bytes` API. There is no added +transit queue or timer; every raw transport has its own reader. Readers yield +after 64 ready packets to avoid starving other runtime tasks. Local delivery +uses a bounded 256-datagram inbox and drops a new packet when full. Transport +queues and fragment reassembly retain their own limits. + +`PacketLink::send_flow` accepts an opaque 64-bit flow identifier. IP plugins +derive it before encryption from source/destination addresses, protocol and +TCP/UDP ports. WireGuard keeps tags alongside its bounded pending queue, so +the first application packets retain their flow identity after a handshake. +Transit preserves the tag; it never hashes changing ciphertext. Handshake and +keepalive frames use flow zero. Fragmented IP traffic uses a coarse address/ +protocol hash because later fragments lack ports; fragmented flows between +the same addresses coalesce. A transition between fragmented and unfragmented +traffic can change its path. The default overlay MTU avoids needing IP +fragmentation for ordinary host TCP traffic. + +ECMP is deterministic for a source and flow while the table is unchanged. +Closed next hops are skipped until the control loop replaces the table. A +topology change can move a flow; datagrams remain unreliable and unordered, +and there is no promise to preserve order across a physical path failure. +Logical peer links and end-to-end encryption state survive these changes. + +## Verification and measurement + +The default offline suite exercises four real agents, iroh transports and +WireGuard tunnels with memory TUNs, plus graph/forwarder unit tests. It needs +neither administrative rights nor public relays/DHT. + +Run the forwarding microbenchmark explicitly: + +```sh +cargo test --release -p tsunagi --lib forwarding_benchmark -- --ignored --nocapture +``` + +It processes one million 64-byte frames and one million 1280-byte frames with +two equal next hops. Frame construction is outside the timed section; the +actual ingress routine, header validation, snapshot lookup, ECMP, TTL update, +buffer release and mock transport submission are inside. The mock transport +counts sends without storing frames. Results are CPU forwarding cost, **not** +end-to-end network latency or VPN throughput; encryption, fragmentation, +sockets, congestion and scheduling contribute separately. + +Wire compatibility: control ALPN `tsunagi/ctrl/2`, data ALPN `tsunagi/data/4`. +Upgrade every participant together; saved identities and network state persist. diff --git a/docs/testing.md b/docs/testing.md index fd1bf33..f0fec9f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -63,15 +63,22 @@ intermediate peer. Fragment tests cover reordering, duplicates, loss, changing fragment sizes, malformed input, timeout and memory bounds. These are packet transport checks, not a claim to have run an SSH server or a host TCP stack. -`crates/tsunagi/src/dataplane/relay.rs` has its own tests for the way -through a peer in the middle: the wrapping and what a malformed one does, a -direct path being preferred over a hop, the middle passing a datagram on -without being handed it, what arrives through somebody reaching the peer it -came from rather than the one that carried it, a link outliving the paths -under it, and the datagram size not changing when the path does. -`tests/wireguard.rs` proves it end to end — two agents that can each reach a -third and not each other, with a real WireGuard packet crossing through the -middle. +Routing tests cover shortest paths, direct preference, deterministic per-flow +ECMP, link loss, protocol isolation, malformed/unknown frames, bounded local +queues, zero-copy transit of an owned buffer, and deliberately inconsistent +tables whose loop terminates at the hop limit. A transit reader is exercised +without any plugin reader, so forwarding cannot accidentally depend on one. +WireGuard tests also cover flow tags queued before a handshake, queue overflow, +and their preservation through encryption. + +`tests/wireguard.rs` includes a four-agent chain A—B—C—D with only adjacent data +links. Real encrypted 1280-byte TCP packets travel in both directions while the +middle TUNs remain empty. A direct A—D link is enabled, then removed; the route +switches back to the chain without replacing end-to-end tunnels. + +The ignored `forwarding_benchmark` measures the synchronous transit routine in +release mode, excluding crypto and socket I/O. Run it explicitly as described +in [routing.md](routing.md); it has no timing threshold in the default suite. Unit tests in `crates/tsunagi/src/state/` cover the signed record model directly: tampering with any field breaks verification, a newer version wins while an older one @@ -123,6 +130,10 @@ What the default suite does **not** cover is the real TUN interface, because that needs `CAP_NET_ADMIN`. Everything above it does run. Mainline discovery tests use an isolated loopback `mainline::Testnet`. +`tests/mainline_socket.rs` checks that idle UDP receive timeouts do not emit +warnings (including Windows error 10060) and that the same node still answers +a real KRPC ping afterwards. The local dependency correction is documented in +[`vendor/mainline/PATCHES.md`](../vendor/mainline/PATCHES.md). `tests/mainline.rs` restores two agents after their DHT and disposable cache have disappeared. `tests/discovery_lifecycle.rs` checks that publication continues while connected, lookup resumes after isolation, and a hung backend diff --git a/docs/wireguard.md b/docs/wireguard.md index 5f8a056..55a319e 100644 --- a/docs/wireguard.md +++ b/docs/wireguard.md @@ -184,7 +184,7 @@ QUIC DATAGRAM frames themselves cannot fragment. Reassembly is bounded and expires incomplete packets; one lost fragment loses one packet, without blocking unrelated traffic. The logical data payload limit is 64 KiB, with the relay envelope subtracted before it reaches the plugin. -The new framing uses data ALPN `tsunagi/data/3`; both ends and intermediate +The new framing uses data ALPN `tsunagi/data/4`; both ends and intermediate peers need the updated binary. Network identities and saved state do not change. See [protocol.md](protocol.md#the-data-plane-protocol) for the wire format. diff --git a/vendor/mainline/Cargo.toml b/vendor/mainline/Cargo.toml new file mode 100644 index 0000000..b352a99 --- /dev/null +++ b/vendor/mainline/Cargo.toml @@ -0,0 +1,147 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.85" +name = "mainline" +version = "8.0.0" +authors = [ + "nuh.dev", + "SeverinAlexB ", + "SHAcollision ", + "dzdidi ", + "Kevin Karsopawiro ", +] +build = false +exclude = [ + "/docs/*", + "/examples/*", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Simple, robust, BitTorrent's Mainline DHT implementation" +homepage = "https://github.com/pubky/mainline" +readme = "README.md" +keywords = [ + "bittorrent", + "torrent", + "dht", + "kademlia", + "mainline", +] +categories = ["network-programming"] +license = "MIT" +repository = "https://github.com/pubky/mainline" + +[package.metadata.docs.rs] +all-features = true + +[features] +async = [ + "node", + "flume/async", + "dep:futures-lite", +] +default = ["full"] +full = ["async"] +node = ["dep:flume"] + +[lib] +name = "mainline" +path = "src/lib.rs" + +[dependencies.crc] +version = "3.4.0" +default-features = false + +[dependencies.document-features] +version = "0.2.12" + +[dependencies.dyn-clone] +version = "1.0.20" +default-features = false + +[dependencies.ed25519-dalek] +version = "3.0.0-pre.1" +default-features = false + +[dependencies.flume] +version = "0.12.0" +optional = true +default-features = false + +[dependencies.futures-lite] +version = "2.6.1" +optional = true +default-features = false + +[dependencies.getrandom] +version = "0.4" +default-features = false + +[dependencies.lru] +version = "0.16.2" +default-features = false + +[dependencies.serde] +version = "1.0.228" +features = ["derive"] + +[dependencies.serde_bencode] +version = "0.2.4" +default-features = false + +[dependencies.serde_bytes] +version = "0.11.19" + +[dependencies.sha1_smol] +version = "1.0.1" +default-features = false + +[dependencies.thiserror] +version = "2.0.18" +default-features = false + +[dependencies.tracing] +version = "0.1.44" + +[dev-dependencies.clap] +version = "4.5.57" +features = ["derive"] + +[dev-dependencies.colored] +version = "3.1.1" + +[dev-dependencies.ctrlc] +version = "3.5.1" + +[dev-dependencies.dashmap] +version = "6.1" + +[dev-dependencies.flume] +version = "0.12.0" +default-features = false + +[dev-dependencies.futures] +version = "0.3.31" + +[dev-dependencies.histo] +version = "1.0.0" + +[dev-dependencies.rayon] +version = "1.11.0" + +[dev-dependencies.tracing-subscriber] +version = "0.3" diff --git a/vendor/mainline/Cargo.toml.orig b/vendor/mainline/Cargo.toml.orig new file mode 100644 index 0000000..ff83dab --- /dev/null +++ b/vendor/mainline/Cargo.toml.orig @@ -0,0 +1,64 @@ +[package] +name = "mainline" +version = "8.0.0" +authors = [ + "nuh.dev", + "SeverinAlexB ", + "SHAcollision ", + "dzdidi ", + "Kevin Karsopawiro " +] +edition = "2021" +rust-version = "1.85" +description = "Simple, robust, BitTorrent's Mainline DHT implementation" +homepage = "https://github.com/pubky/mainline" +license = "MIT" +keywords = ["bittorrent", "torrent", "dht", "kademlia", "mainline"] +categories = ["network-programming"] +repository = "https://github.com/pubky/mainline" +exclude = ["/docs/*", "/examples/*"] + +[dependencies] +getrandom = { version = "0.4", default-features = false } +serde_bencode = { version = "0.2.4", default-features = false } +serde = { version = "1.0.228", features = ["derive"] } +serde_bytes = "0.11.19" +thiserror = { version = "2.0.18", default-features = false } +crc = { version = "3.4.0", default-features = false } +sha1_smol = { version = "1.0.1", default-features = false } +ed25519-dalek = { version = "3.0.0-pre.1", default-features = false } +tracing = "0.1.44" +lru = { version = "0.16.2", default-features = false } +dyn-clone = { version = "1.0.20", default-features = false } + +document-features = "0.2.12" + +# `node` dependencies +flume = { version = "0.12.0", default-features = false, optional = true } + +# `async` dependencies +futures-lite = { version = "2.6.1", default-features = false, optional = true } + +[dev-dependencies] +clap = { version = "4.5.57", features = ["derive"] } +futures = "0.3.31" +tracing-subscriber = "0.3" +ctrlc = "3.5.1" +histo = "1.0.0" +rayon = "1.11.0" +dashmap = "6.1" +flume = { version = "0.12.0", default-features = false } +colored = "3.1.1" + +[features] +## Include [Dht] node. +node = ["dep:flume"] +## Enable [Dht::as_async()] to use [async_dht::AsyncDht]. +async = ["node", "flume/async", "dep:futures-lite"] + +full = ["async"] + +default = ["full"] + +[package.metadata.docs.rs] +all-features = true diff --git a/vendor/mainline/LICENSE b/vendor/mainline/LICENSE new file mode 100644 index 0000000..4fdca5c --- /dev/null +++ b/vendor/mainline/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 raptorswing + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/mainline/PATCHES.md b/vendor/mainline/PATCHES.md new file mode 100644 index 0000000..6eff385 --- /dev/null +++ b/vendor/mainline/PATCHES.md @@ -0,0 +1,36 @@ +# Mainline 8.0.0: Windows receive timeout correction + +This directory contains the published `mainline` 8.0.0 sources, manifest and +MIT license. The workspace selects this copy through `[patch.crates-io]`. + +- Upstream: https://github.com/pubky/mainline +- Published source commit: `b0cabe684f310004c6dcfe8099b91f0d239b11e3` +- Crates.io archive SHA-256: + `d32eaee3dcba6e0bbbefe8bd896a8bd6039d5e74b199c0fe248e9feb547c2a26` +- The upstream manifest is retained, including its development dependencies; + the crate is excluded from the application workspace. + +The only source change is in `src/rpc/socket.rs`: `KrpcSocket::recv_from` +treats `ErrorKind::TimedOut` like `WouldBlock`. Its 50 ms socket read timeout +is a normal idle tick, not a failed DHT operation. Windows reports this as +WSAETIMEDOUT (10060), producing approximately 16 warnings per second with +the unmodified crate. Other receive errors still emit the original warning. +Request deadlines, DHT routing, publication and lookup behavior are unchanged. + +Rust documents the platform difference here: +https://doc.rust-lang.org/std/net/struct.UdpSocket.html#method.set_read_timeout + +Regression coverage is part of the regular workspace suite: + +```sh +cargo test --locked -p tsunagi --test mainline_socket +``` + +It drives the actual DHT actor through idle receive ticks on loopback, checks +that no socket warnings were emitted, then exchanges a real KRPC ping. The +existing Mainline Testnet tests also cover publication, lookup and recovery. +No public DHT, privileged interface or changes to host settings are needed. + +When upstream releases this correction, upgrade the dependency and remove +this directory, the patch entry and its workspace exclusion. Keep the +regression test. Do not patch Cargo's global registry cache. diff --git a/vendor/mainline/README.md b/vendor/mainline/README.md new file mode 100644 index 0000000..8a4586a --- /dev/null +++ b/vendor/mainline/README.md @@ -0,0 +1,69 @@ +# Mainline + +Simple, robust, BitTorrent's [Mainline](https://en.wikipedia.org/wiki/Mainline_DHT) DHT implementation. + +This library is focused on being the best and simplest Rust client for Mainline, especially focused on reliable and fast time-to-first-response. + +It should work as a routing / storing node (server mode) as well, and has been running in production for many months without an issue. +However if you are concerned about spam or DoS, you should consider implementing [rate limiting](#rate-limiting). + +**[API Docs](https://docs.rs/mainline/latest/mainline/)** + +## Getting started + +Check the [Examples](https://github.com/Pubky/mainline/tree/main/examples). + +## Features + +### Client + +Running as a client, means you can store and query for values on the DHT, but not accept any incoming requests. + +```rust +use mainline::Dht; + +let dht = Dht::client().unwrap(); +``` + +Supported BEPs: +- [x] [BEP_0005 DHT Protocol](https://www.bittorrent.org/beps/bep_0005.html) +- [x] [BEP_0042 DHT Security extension](https://www.bittorrent.org/beps/bep_0042.html) +- [x] [BEP_0043 Read-only DHT Nodes](https://www.bittorrent.org/beps/bep_0043.html) +- [x] [BEP_0044 Storing arbitrary data in the DHT](https://www.bittorrent.org/beps/bep_0044.html) + +This implementation also includes [measures against Vertical Sybil Attacks](./docs/sybil-resistance.md). + +### Server + +Running as a server is the same as a client, but you also respond to incoming requests and serve as a routing and storing node, supporting the general routing of the DHT, and contributing to the storage capacity of the DHT. + +```rust +use mainline::Dht; + +let dht = Dht::server().unwrap(); // or `Dht::builder::server_mode().build();` +``` + +Supported BEPs: +- [x] [BEP_0005 DHT Protocol](https://www.bittorrent.org/beps/bep_0005.html) +- [x] [BEP_0042 DHT Security extension](https://www.bittorrent.org/beps/bep_0042.html) +- [x] [BEP_0043 Read-only DHT Nodes](https://www.bittorrent.org/beps/bep_0043.html) +- [x] [BEP_0044 Storing arbitrary data in the DHT](https://www.bittorrent.org/beps/bep_0044.html) + +#### Rate limiting + +The server implementation has no rate-limiting, you can run your own [request filter](./examples/request_filter.rs) and apply your custom rate-limiting. +However, that limit/block will only apply _after_ parsing incoming messages, and it won't affect handling incoming responses. + +### Adaptive mode + +The default Adaptive mode will start the node in client mode, and after 15 minutes of running with a publicly accessible address, +it will switch to server mode. This way nodes that can serve as routing nodes (accessible and less likely to churn), serve as such. + +If you want to explicitly start in Server mode, because you know you are not running behind firewall, +you can call `Dht::builder().server_mode().build()`, and you can optionally add your known public ip so the node doesn't have to depend on, +votes from responding nodes: `Dht::builder().server_mode().public_ip().build()`. + +## Acknowledgment + +This implementation was possible thanks to [Webtorrent's Bittorrent-dht](https://github.com/webtorrent/bittorrent-dht) as a reference, +and [Rustydht-lib](https://github.com/raptorswing/rustydht-lib) that saved me a lot of time, especially at the serialization and deserialization of Bencode messages. diff --git a/vendor/mainline/src/async_dht.rs b/vendor/mainline/src/async_dht.rs new file mode 100644 index 0000000..23423c9 --- /dev/null +++ b/vendor/mainline/src/async_dht.rs @@ -0,0 +1,1031 @@ +//! AsyncDht node. + +use std::{ + net::SocketAddrV4, + pin::Pin, + task::{Context, Poll}, +}; + +use futures_lite::{Stream, StreamExt}; + +use crate::{ + common::{ + hash_immutable, most_recent_mutable_item, AnnouncePeerRequestArguments, + FindNodeRequestArguments, GetPeersRequestArguments, GetValueRequestArguments, Id, + MutableItem, Node, PutImmutableRequestArguments, PutMutableRequestArguments, + PutRequestSpecific, + }, + dht::{ActorMessage, Dht, PutMutableError, ResponseSender}, + rpc::{GetMutableOutcome, GetRequestSpecific, Info, PutError, PutOutcome, PutQueryError}, +}; + +impl Dht { + /// Return an async version of the Dht client. + pub fn as_async(self) -> AsyncDht { + AsyncDht(self) + } +} + +#[derive(Debug, Clone)] +/// Async version of the Dht node. +pub struct AsyncDht(Dht); + +impl AsyncDht { + /// Information and statistics about this [Dht] node. + pub async fn info(&self) -> Info { + let (tx, rx) = flume::bounded::(1); + self.send(ActorMessage::Info(tx)); + + rx.recv_async() + .await + .expect("actor thread unexpectedly shutdown") + } + + /// Turn this node's routing table to a list of bootstrapping nodes. + pub async fn to_bootstrap(&self) -> Vec { + let (tx, rx) = flume::bounded::>(1); + self.send(ActorMessage::ToBootstrap(tx)); + + rx.recv_async() + .await + .expect("actor thread unexpectedly shutdown") + } + + // === Public Methods === + + /// Await until the bootstrapping query is done. + /// + /// Returns true if the bootstrapping was successful. + pub async fn bootstrapped(&self) -> bool { + let info = self.info().await; + let nodes = self.find_node(*info.id()).await; + + !nodes.is_empty() + } + + // === Find nodes === + + /// Returns the closest 20 [secure](Node::is_secure) nodes to a target [Id]. + /// + /// Mostly useful to crawl the DHT. + /// + /// The returned nodes are claims by other nodes, they may be lies, or may have churned + /// since they were last seen, but haven't been pinged yet. + /// + /// You might need to ping them to confirm they exist, and responsive, or if you want to + /// learn more about them like the client they are using, or if they support a given BEP. + /// + /// If you are trying to find the closest nodes to a target with intent to [Self::put], + /// a request directly to these nodes (using `extra_nodes` parameter), then you should + /// use [Self::get_closest_nodes] instead. + pub async fn find_node(&self, target: Id) -> Box<[Node]> { + let (tx, rx) = flume::bounded::>(1); + self.send(ActorMessage::Get( + GetRequestSpecific::FindNode(FindNodeRequestArguments { target }), + ResponseSender::ClosestNodes(tx), + )); + + rx.recv_async() + .await + .expect("Query was dropped before sending a response, please open an issue.") + } + + // === Peers === + + /// Get peers for a given infohash. + /// + /// Note: each node of the network will only return a _random_ subset (usually 20) + /// of the total peers it has for a given infohash, so if you are getting responses + /// from 20 nodes, you can expect up to 400 peers in total, but if there are more + /// announced peers on that infohash, you are likely to miss some, the logic here + /// for Bittorrent is that any peer will introduce you to more peers through "peer exchange" + /// so if you are implementing something different from Bittorrent, you might want + /// to implement your own logic for gossipping more peers after you discover the first ones. + pub fn get_peers(&self, info_hash: Id) -> GetStream> { + let (tx, rx) = flume::unbounded::>(); + self.send(ActorMessage::Get( + GetRequestSpecific::GetPeers(GetPeersRequestArguments { info_hash }), + ResponseSender::Peers(tx), + )); + + GetStream(rx.into_stream()) + } + + /// Announce a peer for a given infohash. + /// + /// The peer will be announced on this process IP. + /// If explicit port is passed, it will be used, otherwise the port will be implicitly + /// assumed by remote nodes to be the same ase port they received the request from. + pub async fn announce_peer( + &self, + info_hash: Id, + port: Option, + ) -> Result { + let (port, implied_port) = match port { + Some(port) => (port, None), + None => (0, Some(true)), + }; + + self.put( + PutRequestSpecific::AnnouncePeer(AnnouncePeerRequestArguments { + info_hash, + port, + implied_port, + }), + None, + ) + .await + .map(|outcome| outcome.target) + .map_err(|error| match error { + PutError::Query(error) => error, + PutError::Concurrency(_) => { + unreachable!("should not receive a concurrency error from announce peer query") + } + }) + } + + // === Immutable data === + + /// Get an Immutable data by its sha1 hash. + pub async fn get_immutable(&self, target: Id) -> Option> { + let (tx, rx) = flume::unbounded::>(); + self.send(ActorMessage::Get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + seq: None, + salt: None, + }), + ResponseSender::Immutable(tx), + )); + + rx.recv_async().await.map(Some).unwrap_or(None) + } + + /// Put an immutable data to the DHT. + pub async fn put_immutable(&self, value: &[u8]) -> Result { + let target: Id = hash_immutable(value).into(); + + self.put( + PutRequestSpecific::PutImmutable(PutImmutableRequestArguments { + target, + v: value.into(), + }), + None, + ) + .await + .map(|outcome| outcome.target) + .map_err(|error| match error { + PutError::Query(error) => error, + PutError::Concurrency(_) => { + unreachable!("should not receive a concurrency error from put immutable query") + } + }) + } + + // === Mutable data === + + /// Get a mutable data by its `public_key` and optional `salt`. + /// + /// You can ask for items `more_recent_than` than a certain `seq`, + /// usually one that you already have seen before, similar to `If-Modified-Since` header in HTTP. + /// + /// # Order + /// + /// The order of [MutableItem]s returned by this stream is not guaranteed to + /// reflect their `seq` value. You should not assume that the later items are + /// more recent than earlier ones. + /// + /// Consider using [Self::get_mutable_most_recent] if that is what you need. + pub fn get_mutable( + &self, + public_key: &[u8; 32], + salt: Option<&[u8]>, + more_recent_than: Option, + ) -> GetStream { + self.get_mutable_detailed(public_key, salt, more_recent_than) + .items + } + + /// Get mutable data and final aggregate diagnostics for the lookup. + /// + /// Valid mutable items are yielded on [GetMutableDetailed::items] as they arrive. + /// Final counters are available by awaiting [GetMutableOutcomeReceiver::recv]. + pub fn get_mutable_detailed( + &self, + public_key: &[u8; 32], + salt: Option<&[u8]>, + more_recent_than: Option, + ) -> GetMutableDetailed { + let salt = salt.map(Into::into); + let target = MutableItem::target_from_key(public_key, salt.as_deref()); + let (values_tx, values_rx) = flume::unbounded::(); + let (outcome_tx, outcome_rx) = flume::bounded::(1); + + self.send(ActorMessage::Get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + seq: more_recent_than, + salt, + }), + ResponseSender::MutableDetailed { + values: values_tx, + outcome: outcome_tx, + }, + )); + + GetMutableDetailed { + items: GetStream(values_rx.into_stream()), + outcome: GetMutableOutcomeReceiver(outcome_rx), + } + } + + /// Get the most recent [MutableItem] from the network. + pub async fn get_mutable_most_recent( + &self, + public_key: &[u8; 32], + salt: Option<&[u8]>, + ) -> Option { + self.get_mutable(public_key, salt, None) + .fold(None::, most_recent_mutable_item) + .await + } + + /// Put a mutable data to the DHT. + /// + /// Returns details about the successful PUT, including how many DHT nodes acknowledged it. + /// + /// # Lost Update Problem + /// + /// As mainline DHT is a distributed system, it is vulnerable to [Write–write conflict](https://en.wikipedia.org/wiki/Write-write_conflict). + /// + /// ## Read first + /// + /// To mitigate the risk of lost updates, you should call the [Self::get_mutable_most_recent] method + /// then start authoring the new [MutableItem] based on the most recent as in the following example: + /// + ///```rust + /// use mainline::{Dht, MutableItem, SigningKey, Testnet}; + /// use std::net::Ipv4Addr; + /// + /// let testnet = Testnet::builder(3).build().unwrap(); + /// let dht = Dht::builder() + /// .bootstrap(&testnet.bootstrap) + /// .bind_address(Ipv4Addr::LOCALHOST) + /// .build() + /// .unwrap() + /// .as_async(); + /// + /// let signing_key = SigningKey::from_bytes(&[0; 32]); + /// let key = signing_key.verifying_key().to_bytes(); + /// let salt = Some(b"salt".as_ref()); + /// + /// futures::executor::block_on(async move { + /// let (item, cas) = if let Some(most_recent) = dht.get_mutable_most_recent(&key, salt).await { + /// // 1. Optionally Create a new value to take the most recent's value in consideration. + /// let mut new_value = most_recent.value().to_vec(); + /// new_value.extend_from_slice(b" more data"); + /// + /// // 2. Increment the sequence number to be higher than the most recent's. + /// let most_recent_seq = most_recent.seq(); + /// let new_seq = most_recent_seq + 1; + /// + /// ( + /// MutableItem::new(signing_key, &new_value, new_seq, salt), + /// // 3. Use the most recent [MutableItem::seq] as a `CAS`. + /// Some(most_recent_seq) + /// ) + /// } else { + /// (MutableItem::new(signing_key, b"first value", 1, salt), None) + /// }; + /// + /// let target = *item.target(); + /// let outcome = dht.put_mutable(item, cas).await.unwrap(); + /// assert_eq!(outcome.target, target); + /// }); + /// ``` + /// + /// ## Errors + /// + /// In addition to the [PutQueryError] common with all PUT queries, PUT mutable item + /// query has other [Concurrency errors][crate::rpc::ConcurrencyError], that try to detect write conflict + /// risks or obvious conflicts. + /// + /// If you are lucky to get one of these errors (which is not guaranteed), then you should + /// read the most recent item again, and repeat the steps in the previous example. + pub async fn put_mutable( + &self, + item: MutableItem, + cas: Option, + ) -> Result { + let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, cas)); + + self.put(request, None).await.map_err(|error| match error { + PutError::Query(err) => PutMutableError::Query(err), + PutError::Concurrency(err) => PutMutableError::Concurrency(err), + }) + } + + // === Raw === + + /// Get closet nodes to a specific target, that support [BEP_0044](https://www.bittorrent.org/beps/bep_0044.html). + /// + /// Useful to [Self::put] a request to nodes further from the 20 closest nodes to the + /// [PutRequestSpecific::target]. Which itself is useful to circumvent [extreme vertical sybil attacks](https://github.com/pubky/mainline/blob/main/docs/censorship-resistance.md#extreme-vertical-sybil-attacks). + pub async fn get_closest_nodes(&self, target: Id) -> Box<[Node]> { + let (tx, rx) = flume::unbounded::>(); + self.send(ActorMessage::Get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + salt: None, + seq: None, + }), + ResponseSender::ClosestNodes(tx), + )); + + rx.recv_async() + .await + .expect("Query was dropped before sending a response, please open an issue.") + } + + /// Send a PUT request to the closest nodes, and optionally some extra nodes. + /// + /// This is useful to put data to regions of the DHT other than the closest nodes + /// to this request's [target][PutRequestSpecific::target]. + /// + /// You can find nodes close to other regions of the network by calling + /// [Self::get_closest_nodes] with the target that you want to find the closest nodes to. + /// + /// Note: extra nodes need to have [Node::valid_token]. + /// + /// Returns details about the successful PUT. + pub async fn put( + &self, + request: PutRequestSpecific, + extra_nodes: Option>, + ) -> Result { + self.put_inner(request, extra_nodes) + .recv_async() + .await + .expect("Query was dropped before sending a response, please open an issue.") + } + + // === Private Methods === + + pub(crate) fn put_inner( + &self, + request: PutRequestSpecific, + extra_nodes: Option>, + ) -> flume::Receiver> { + let (tx, rx) = flume::bounded::>(1); + self.send(ActorMessage::Put(request, tx, extra_nodes)); + + rx + } + + fn send(&self, message: ActorMessage) { + self.0.send(message) + } +} + +/// A [Stream] of incoming peers, immutable or mutable values. +pub struct GetStream(flume::r#async::RecvStream<'static, T>); + +impl Stream for GetStream { + type Item = T; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + this.0.poll_next(cx) + } +} + +/// Mutable GET values stream plus a final diagnostic outcome. +pub struct GetMutableDetailed { + /// Valid mutable items returned by queried DHT nodes. + pub items: GetStream, + /// Final aggregate diagnostics for the lookup. + pub outcome: GetMutableOutcomeReceiver, +} + +/// Async receiver for a detailed mutable GET outcome. +pub struct GetMutableOutcomeReceiver(flume::Receiver); + +impl GetMutableOutcomeReceiver { + /// Wait for the mutable GET query to finish and return its aggregate diagnostics. + pub async fn recv(self) -> GetMutableOutcome { + self.0 + .recv_async() + .await + .expect("Query was dropped before sending a response, please open an issue.") + } +} + +#[cfg(test)] +mod test { + use std::net::Ipv4Addr; + use std::{str::FromStr, time::Duration}; + + use ed25519_dalek::SigningKey; + use futures::StreamExt; + + use crate::{dht::Testnet, rpc::ConcurrencyError}; + + use super::*; + + fn counted_mutable_responses(outcome: &GetMutableOutcome) -> u32 { + outcome.values + + outcome.no_values + + outcome.no_more_recent + + outcome.invalid_values + + outcome.invalid_responses + + outcome.krpc_errors + } + + fn counted_valid_mutable_responses(outcome: &GetMutableOutcome) -> u32 { + outcome.values + outcome.no_values + outcome.no_more_recent + } + + #[test] + fn announce_get_peer() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let info_hash = Id::random(); + + a.announce_peer(info_hash, Some(45555)) + .await + .expect("failed to announce"); + + let peers = b.get_peers(info_hash).next().await.expect("No peers"); + + assert_eq!(peers.first().unwrap().port(), 45555); + } + + futures::executor::block_on(test()); + } + + #[test] + fn put_get_immutable() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let value = b"Hello World!"; + let expected_target = Id::from_str("e5f96f6f38320f0f33959cb4d3d656452117aadb").unwrap(); + + let target = a.put_immutable(value).await.unwrap(); + assert_eq!(target, expected_target); + + let response = b.get_immutable(target).await; + assert_eq!(response, Some(value.to_vec().into_boxed_slice())); + } + + futures::executor::block_on(test()); + } + + #[test] + fn raw_put_immutable_returns_outcome() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let dht = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let value = b"Hello World!"; + let target: Id = hash_immutable(value).into(); + let request = PutRequestSpecific::PutImmutable(PutImmutableRequestArguments { + target, + v: value.as_ref().into(), + }); + + let outcome = dht.put(request, None).await.unwrap(); + + assert_eq!(outcome.target, target); + assert!(outcome.stored_at > 0); + } + + futures::executor::block_on(test()); + } + + #[test] + fn put_get_mutable() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + let target = *item.target(); + + let outcome = a.put_mutable(item.clone(), None).await.unwrap(); + + assert_eq!(outcome.target, target); + assert!(outcome.stored_at > 0); + + let response = b + .get_mutable(signer.verifying_key().as_bytes(), None, None) + .next() + .await + .expect("No mutable values"); + + assert_eq!(&response, &item); + } + + futures::executor::block_on(test()); + } + + #[test] + fn get_mutable_detailed_no_values() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let dht = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let detailed = dht.get_mutable_detailed(signer.verifying_key().as_bytes(), None, None); + let GetMutableDetailed { items, outcome } = detailed; + + let items = items.collect::>().await; + let outcome = outcome.recv().await; + + assert!(items.is_empty()); + assert!(outcome.queried > 0); + assert_eq!(outcome.values, 0); + assert!(outcome.no_values > 0); + assert_eq!( + outcome.valid_responses(), + counted_valid_mutable_responses(&outcome) + ); + assert_eq!(outcome.responded(), counted_mutable_responses(&outcome)); + } + + futures::executor::block_on(test()); + } + + #[test] + fn get_mutable_detailed_returns_values_outcome() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let item = MutableItem::new(signer.clone(), b"Hello World!", 1000, None); + a.put_mutable(item.clone(), None).await.unwrap(); + + let detailed = b.get_mutable_detailed(signer.verifying_key().as_bytes(), None, None); + let GetMutableDetailed { items, outcome } = detailed; + + let items = items.collect::>().await; + let outcome = outcome.recv().await; + + assert!(items.iter().any(|response| response == &item)); + assert!(outcome.queried > 0); + assert_eq!(outcome.values, items.len() as u32); + assert!(outcome.values > 0); + assert_eq!( + outcome.valid_responses(), + counted_valid_mutable_responses(&outcome) + ); + assert_eq!(outcome.responded(), counted_mutable_responses(&outcome)); + } + + futures::executor::block_on(test()); + } + + #[test] + fn get_mutable_detailed_no_more_recent_value() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let item = MutableItem::new(signer.clone(), b"Hello World!", seq, None); + a.put_mutable(item, None).await.unwrap(); + + let detailed = + b.get_mutable_detailed(signer.verifying_key().as_bytes(), None, Some(seq)); + let GetMutableDetailed { items, outcome } = detailed; + + let items = items.collect::>().await; + let outcome = outcome.recv().await; + + assert!(items.is_empty()); + assert!(outcome.queried > 0); + assert_eq!(outcome.values, 0); + assert!(outcome.no_more_recent > 0); + assert_eq!( + outcome.valid_responses(), + counted_valid_mutable_responses(&outcome) + ); + assert_eq!(outcome.responded(), counted_mutable_responses(&outcome)); + } + + futures::executor::block_on(test()); + } + + #[test] + fn put_get_mutable_no_more_recent_value() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + + a.put_mutable(item.clone(), None).await.unwrap(); + + let response = b + .get_mutable(signer.verifying_key().as_bytes(), None, Some(seq)) + .next() + .await; + + assert!(&response.is_none()); + } + + futures::executor::block_on(test()); + } + + #[test] + fn get_mutable_most_recent_prefers_highest_seq() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let dht = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let newer = MutableItem::new(signer.clone(), b"newer", 1001, None); + dht.put_mutable(newer.clone(), None).await.unwrap(); + + let older = MutableItem::new(signer, b"older", 1000, None); + let (sender, _) = flume::bounded::>(1); + let request = + PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(older, None)); + dht.0 + .0 + .send(ActorMessage::Put(request, sender, None)) + .unwrap(); + + let most_recent = dht + .get_mutable_most_recent(newer.key(), None) + .await + .expect("No mutable values"); + + assert_eq!(most_recent.seq(), newer.seq()); + assert_eq!(most_recent.value(), newer.value()); + } + + futures::executor::block_on(test()); + } + + #[test] + fn repeated_put_query() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let first = a.put_immutable(&[1, 2, 3]).await; + let second = a.put_immutable(&[1, 2, 3]).await; + + assert_eq!(first.unwrap(), second.unwrap()); + } + + futures::executor::block_on(test()); + } + + #[test] + fn concurrent_get_mutable() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + + a.put_mutable(item.clone(), None).await.unwrap(); + + let _response_first = b + .get_mutable(signer.verifying_key().as_bytes(), None, None) + .next() + .await + .expect("No mutable values"); + + let response_second = b + .get_mutable(signer.verifying_key().as_bytes(), None, None) + .next() + .await + .expect("No mutable values"); + + assert_eq!(&response_second, &item); + } + + futures::executor::block_on(test()); + } + + #[test] + fn concurrent_put_mutable_same() { + let testnet = Testnet::builder(10).build().unwrap(); + + let dht = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + + let mut handles = vec![]; + + for _ in 0..2 { + let dht = dht.clone(); + let item = item.clone(); + + let handle = std::thread::spawn(move || { + futures::executor::block_on(async { dht.put_mutable(item, None).await.unwrap() }); + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + } + + #[test] + fn concurrent_put_mutable_different() { + let testnet = Testnet::builder(10).build().unwrap(); + + let dht = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let mut handles = vec![]; + + for i in 0..2 { + let dht = dht.clone(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + + let mut value = b"Hello World!".to_vec(); + value.push(i); + + let item = MutableItem::new(signer.clone(), &value, seq, None); + + let handle = std::thread::spawn(move || { + futures::executor::block_on(async { + let result = dht.put_mutable(item, None).await; + if i == 0 { + assert!(result.is_ok()) + } else { + assert!(matches!( + result, + Err(PutMutableError::Concurrency(ConcurrencyError::ConflictRisk)) + )) + } + }) + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + } + + #[test] + fn concurrent_put_mutable_different_with_cas() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let dht = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + let value = b"Hello World!".to_vec(); + + // First + { + let item = MutableItem::new(signer.clone(), &value, 1000, None); + + let (sender, _) = flume::bounded::>(1); + let request = + PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, None)); + dht.0 + .0 + .send(ActorMessage::Put(request, sender, None)) + .unwrap(); + } + + std::thread::sleep(Duration::from_millis(100)); + + // Second + { + let item = MutableItem::new(signer, &value, 1001, None); + + dht.put_mutable(item, Some(1000)).await.unwrap(); + } + } + + futures::executor::block_on(test()); + } + + #[test] + fn conflict_301_cas() { + async fn test() { + let testnet = Testnet::builder(10).build().unwrap(); + + let dht = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap() + .as_async(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + let value = b"Hello World!".to_vec(); + + dht.put_mutable(MutableItem::new(signer.clone(), &value, 1001, None), None) + .await + .unwrap(); + + assert!(matches!( + dht.put_mutable(MutableItem::new(signer, &value, 1002, None), Some(1000)) + .await, + Err(PutMutableError::Concurrency(ConcurrencyError::CasFailed)) + )); + } + + futures::executor::block_on(test()); + } +} diff --git a/vendor/mainline/src/common.rs b/vendor/mainline/src/common.rs new file mode 100644 index 0000000..dddd2a3 --- /dev/null +++ b/vendor/mainline/src/common.rs @@ -0,0 +1,16 @@ +//! Miscellaneous common structs used throughout the library. + +mod id; +mod immutable; +pub mod messages; +mod mutable; +mod node; +mod routing_table; + +pub use id::*; +pub use immutable::*; +pub use messages::*; +pub(crate) use mutable::most_recent_mutable_item; +pub use mutable::*; +pub use node::*; +pub use routing_table::*; diff --git a/vendor/mainline/src/common/id.rs b/vendor/mainline/src/common/id.rs new file mode 100644 index 0000000..5b269bc --- /dev/null +++ b/vendor/mainline/src/common/id.rs @@ -0,0 +1,354 @@ +//! Kademlia node Id or a lookup target +use crc::{Crc, CRC_32_ISCSI}; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::{ + fmt::{self, Debug, Display, Formatter}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, +}; + +/// The size of node IDs in bits. +pub const ID_SIZE: usize = 20; +pub const MAX_DISTANCE: u8 = ID_SIZE as u8 * 8; + +const IPV4_MASK: u32 = 0x030f3fff; +const CASTAGNOLI: Crc = Crc::::new(&CRC_32_ISCSI); + +#[derive(Clone, Copy, PartialEq, Ord, PartialOrd, Eq, Hash, Serialize, Deserialize)] +/// Kademlia node Id or a lookup target +pub struct Id([u8; ID_SIZE]); + +impl Id { + /// Generate a random Id + pub fn random() -> Id { + let mut bytes: [u8; 20] = [0; 20]; + getrandom::fill(&mut bytes).expect("getrandom"); + Id(bytes) + } + + /// Create a new Id from some bytes. Returns Err if the input is not 20 bytes long. + pub fn from_bytes>(bytes: T) -> Result { + let bytes = bytes.as_ref(); + if bytes.len() != ID_SIZE { + return Err(InvalidIdSize(bytes.len())); + } + + let mut tmp: [u8; ID_SIZE] = [0; ID_SIZE]; + tmp[..ID_SIZE].clone_from_slice(&bytes[..ID_SIZE]); + + Ok(Id(tmp)) + } + + /// Simplified XOR distance between this Id and a target Id. + /// + /// The distance is the number of trailing non zero bits in the XOR result. + /// + /// Distance to self is 0 + /// Distance to the furthest Id is 160 + /// Distance to an Id with 5 leading matching bits is 155 + pub fn distance(&self, other: &Id) -> u8 { + MAX_DISTANCE - self.xor(other).leading_zeros() + } + + /// Returns the number of leading zeros in the binary representation of `self`. + pub fn leading_zeros(&self) -> u8 { + for (i, byte) in self.0.iter().enumerate() { + if *byte != 0 { + // leading zeros so far + laedinge zeros of this byte + return (i as u32 * 8 + byte.leading_zeros()) as u8; + } + } + + 160 + } + + /// Performs bitwise XOR between two Ids + pub fn xor(&self, other: &Id) -> Id { + let mut result = [0_u8; 20]; + + for (i, (a, b)) in self.0.iter().zip(other.0).enumerate() { + result[i] = a ^ b; + } + + result.into() + } + + /// Returns a byte slice of this Id. + pub fn as_bytes(&self) -> &[u8; 20] { + &self.0 + } + + /// Create a new Id according to [BEP_0042](http://bittorrent.org/beps/bep_0042.html). + pub fn from_addr(addr: &SocketAddr) -> Id { + let ip = addr.ip(); + + Id::from_ip(ip) + } + + /// Create a new Id from an Ipv4 address according to [BEP_0042](http://bittorrent.org/beps/bep_0042.html). + pub fn from_ip(ip: IpAddr) -> Id { + match ip { + IpAddr::V4(addr) => Id::from_ipv4(addr), + IpAddr::V6(_addr) => unimplemented!("Ipv6 is not supported"), + } + } + + /// Create a new Id from an Ipv4 address according to [BEP_0042](http://bittorrent.org/beps/bep_0042.html). + pub fn from_ipv4(ipv4: Ipv4Addr) -> Id { + let mut bytes = [0_u8; 21]; + getrandom::fill(&mut bytes).expect("getrandom"); + + from_ipv4_and_r(bytes[1..].try_into().expect("infallible"), ipv4, bytes[0]) + } + + /// Validate that this Id is valid with respect to [BEP_0042](http://bittorrent.org/beps/bep_0042.html). + pub fn is_valid_for_ip(&self, ipv4: Ipv4Addr) -> bool { + if ipv4.is_private() || ipv4.is_link_local() || ipv4.is_loopback() { + return true; + } + + let expected = first_21_bits(&id_prefix_ipv4(ipv4, self.0[ID_SIZE - 1])); + + self.first_21_bits() == expected + } + + pub(crate) fn first_21_bits(&self) -> [u8; 3] { + first_21_bits(&self.0) + } +} + +fn first_21_bits(bytes: &[u8]) -> [u8; 3] { + [bytes[0], bytes[1], bytes[2] & 0xf8] +} + +fn from_ipv4_and_r(bytes: [u8; 20], ip: Ipv4Addr, r: u8) -> Id { + let mut bytes = bytes; + let prefix = id_prefix_ipv4(ip, r); + + // Set first 21 bits to the prefix + bytes[0] = prefix[0]; + bytes[1] = prefix[1]; + // set the first 5 bits of the 3rd byte to the remaining 5 bits of the prefix + bytes[2] = (prefix[2] & 0xf8) | (bytes[2] & 0x7); + + // Set the last byte to the random r + bytes[ID_SIZE - 1] = r; + + Id(bytes) +} + +fn id_prefix_ipv4(ip: Ipv4Addr, r: u8) -> [u8; 3] { + let r32: u32 = r.into(); + let ip_int: u32 = u32::from_be_bytes(ip.octets()); + let masked_ip: u32 = (ip_int & IPV4_MASK) | (r32 << 29); + + let mut digest = CASTAGNOLI.digest(); + digest.update(&masked_ip.to_be_bytes()); + + let crc = digest.finalize(); + + crc.to_be_bytes()[..3] + .try_into() + .expect("Failed to convert bytes 0-2 of the crc into a 3-byte array") +} + +impl Display for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + #[allow(clippy::format_collect)] + let hex_chars: String = self.0.iter().map(|byte| format!("{:02x}", byte)).collect(); + + write!(f, "{}", hex_chars) + } +} + +impl From<[u8; ID_SIZE]> for Id { + fn from(bytes: [u8; ID_SIZE]) -> Id { + Id(bytes) + } +} + +impl From<&[u8; ID_SIZE]> for Id { + fn from(bytes: &[u8; ID_SIZE]) -> Id { + Id(*bytes) + } +} + +impl From for [u8; ID_SIZE] { + fn from(value: Id) -> Self { + value.0 + } +} + +impl FromStr for Id { + type Err = DecodeIdError; + + fn from_str(s: &str) -> Result { + if s.len() % 2 != 0 { + return Err(DecodeIdError::OddNumberOfCharacters); + } + + let mut bytes = Vec::with_capacity(s.len() / 2); + + for i in 0..s.len() / 2 { + let byte_str = &s[i * 2..(i * 2) + 2]; + if let Ok(byte) = u8::from_str_radix(byte_str, 16) { + bytes.push(byte); + } else { + return Err(DecodeIdError::InvalidHexCharacter(byte_str.into())); + } + } + + Ok(Id::from_bytes(bytes)?) + } +} + +impl Debug for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Id({})", self) + } +} + +#[derive(Debug)] +pub struct InvalidIdSize(usize); + +impl std::error::Error for InvalidIdSize {} + +impl std::fmt::Display for InvalidIdSize { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Invalid Id size, expected 20, got {0}", self.0) + } +} + +#[derive(thiserror::Error, Debug)] +/// Mainline crate error enum. +pub enum DecodeIdError { + /// Id is expected to by 20 bytes. + #[error(transparent)] + InvalidIdSize(#[from] InvalidIdSize), + + #[error("Hex encoding should contain an even number of hex characters")] + /// Hex encoding should contain an even number of hex characters + OddNumberOfCharacters, + + /// Invalid hex character + #[error("Invalid Id encoding: {0}")] + InvalidHexCharacter(String), +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn distance_to_self() { + let id = Id::random(); + let distance = id.distance(&id); + assert_eq!(distance, 0) + } + + #[test] + fn distance_to_id() { + let id = Id::from_str("0639A1E24FBB8AB277DF033476AB0DE10FAB3BDC").unwrap(); + + let target = Id::from_str("035b1aeb9737ade1a80933594f405d3f772aa08e").unwrap(); + + let distance = id.distance(&target); + + assert_eq!(distance, 155) + } + + #[test] + fn distance_to_random_id() { + let id = Id::random(); + let target = Id::random(); + + let distance = id.distance(&target); + + assert_ne!(distance, 0) + } + + #[test] + fn distance_to_furthest() { + let id = Id::random(); + + let mut opposite = [0_u8; 20]; + for (i, &value) in id.as_bytes().iter().enumerate() { + opposite[i] = value ^ 0xff; + } + let target = Id::from_bytes(opposite).unwrap(); + + let distance = id.distance(&target); + + assert_eq!(distance, MAX_DISTANCE) + } + + #[test] + fn from_u8_20() { + let bytes = [8; 20]; + + let id: Id = bytes.into(); + + assert_eq!(*id.as_bytes(), bytes); + } + + #[test] + fn from_ipv4() { + let vectors = vec![ + (Ipv4Addr::new(124, 31, 75, 21), 1, [0x5f, 0xbf, 0xbf]), + (Ipv4Addr::new(21, 75, 31, 124), 86, [0x5a, 0x3c, 0xe9]), + (Ipv4Addr::new(65, 23, 51, 170), 22, [0xa5, 0xd4, 0x32]), + (Ipv4Addr::new(84, 124, 73, 14), 65, [0x1b, 0x03, 0x21]), + (Ipv4Addr::new(43, 213, 53, 83), 90, [0xe5, 0x6f, 0x6c]), + ]; + + for vector in vectors { + test(vector.0, vector.1, vector.2); + } + + fn test(ip: Ipv4Addr, r: u8, expected_prefix: [u8; 3]) { + let id = Id::random(); + let result = from_ipv4_and_r(*id.as_bytes(), ip, r); + let prefix = first_21_bits(result.as_bytes()); + + assert_eq!(prefix, first_21_bits(&expected_prefix)); + assert_eq!(result.as_bytes()[ID_SIZE - 1], r); + } + } + + #[test] + fn is_valid_for_ipv4() { + let valid_vectors = vec![ + ( + Ipv4Addr::new(124, 31, 75, 21), + "5fbfbff10c5d6a4ec8a88e4c6ab4c28b95eee401", + ), + ( + Ipv4Addr::new(21, 75, 31, 124), + "5a3ce9c14e7a08645677bbd1cfe7d8f956d53256", + ), + ( + Ipv4Addr::new(65, 23, 51, 170), + "a5d43220bc8f112a3d426c84764f8c2a1150e616", + ), + ( + Ipv4Addr::new(84, 124, 73, 14), + "1b0321dd1bb1fe518101ceef99462b947a01ff41", + ), + ( + Ipv4Addr::new(43, 213, 53, 83), + "e56f6cbf5b7c4be0237986d5243b87aa6d51305a", + ), + ]; + + for vector in valid_vectors { + test(vector.0, vector.1); + } + + fn test(ip: Ipv4Addr, hex: &str) { + let id = Id::from_str(hex).unwrap(); + + assert!(id.is_valid_for_ip(ip)); + } + } +} diff --git a/vendor/mainline/src/common/immutable.rs b/vendor/mainline/src/common/immutable.rs new file mode 100644 index 0000000..b278264 --- /dev/null +++ b/vendor/mainline/src/common/immutable.rs @@ -0,0 +1,52 @@ +//! Helper functions for immutable items. + +use sha1_smol::Sha1; + +use super::ID_SIZE; +use crate::Id; + +pub fn validate_immutable(v: &[u8], target: Id) -> bool { + hash_immutable(v) == *target.as_bytes() +} + +pub fn hash_immutable(v: &[u8]) -> [u8; ID_SIZE] { + let mut encoded = Vec::with_capacity(v.len() + 3); + encoded.extend(format!("{}:", v.len()).bytes()); + encoded.extend_from_slice(v); + + let mut hasher = Sha1::new(); + hasher.update(&encoded); + + hasher.digest().bytes() +} + +#[cfg(test)] +mod test { + use super::*; + use std::str::FromStr; + + #[test] + fn test_validate_immutable() { + let v = vec![ + 171, 118, 111, 111, 174, 109, 195, 32, 138, 140, 113, 176, 76, 135, 116, 132, 156, 126, + 75, 173, + ]; + + let target = Id::from_bytes([ + 2, 23, 113, 43, 67, 11, 185, 26, 26, 30, 204, 238, 204, 1, 13, 84, 52, 40, 86, 231, + ]) + .unwrap(); + + assert!(validate_immutable(&v, target)); + assert!(!validate_immutable(&v[1..], target)); + } + + #[test] + + fn test_hash_immutable() { + let v = b"From the river to the sea, Palestine will be free"; + let target = Id::from_str("4238af8aff56cf6e0007d9d2003bf23d33eea7c3").unwrap(); + + assert_eq!(hash_immutable(v), *target.as_bytes()); + } +} diff --git a/vendor/mainline/src/common/messages.rs b/vendor/mainline/src/common/messages.rs new file mode 100644 index 0000000..529e96c --- /dev/null +++ b/vendor/mainline/src/common/messages.rs @@ -0,0 +1,1255 @@ +//! Serialize and decerealize Krpc messages. + +// Copied from + +#![allow(missing_docs)] + +mod internal; + +use std::convert::TryInto; +use std::net::{Ipv4Addr, SocketAddrV4}; + +use crate::common::{Id, Node, ID_SIZE}; + +use super::InvalidIdSize; + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct Message { + pub transaction_id: u32, + + /// The version of the requester or responder. + pub version: Option<[u8; 4]>, + + /// The IP address and port ("SocketAddr") of the requester as seen from the responder's point of view. + /// This should be set only on response, but is defined at this level with the other common fields to avoid defining yet another layer on the response objects. + pub requester_ip: Option, + + pub message_type: MessageType, + + /// For bep0043. When set true on a request, indicates that the requester can't reply to requests and that responders should not add requester to their routing tables. + /// Should only be set on requests - undefined behavior when set on a response. + pub read_only: bool, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum MessageType { + Request(RequestSpecific), + + Response(ResponseSpecific), + + Error(ErrorSpecific), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct ErrorSpecific { + pub code: i32, + pub description: String, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct RequestSpecific { + pub requester_id: Id, + pub request_type: RequestTypeSpecific, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum RequestTypeSpecific { + Ping, + FindNode(FindNodeRequestArguments), + GetPeers(GetPeersRequestArguments), + GetValue(GetValueRequestArguments), + + Put(PutRequest), +} + +#[derive(Debug, PartialEq, Clone)] +pub struct PutRequest { + pub token: Box<[u8]>, + pub put_request_type: PutRequestSpecific, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum PutRequestSpecific { + AnnouncePeer(AnnouncePeerRequestArguments), + PutImmutable(PutImmutableRequestArguments), + PutMutable(PutMutableRequestArguments), +} + +impl PutRequestSpecific { + pub fn target(&self) -> &Id { + match self { + PutRequestSpecific::AnnouncePeer(AnnouncePeerRequestArguments { + info_hash, .. + }) => info_hash, + PutRequestSpecific::PutMutable(PutMutableRequestArguments { target, .. }) => target, + PutRequestSpecific::PutImmutable(PutImmutableRequestArguments { target, .. }) => target, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ResponseSpecific { + Ping(PingResponseArguments), + FindNode(FindNodeResponseArguments), + GetPeers(GetPeersResponseArguments), + GetImmutable(GetImmutableResponseArguments), + GetMutable(GetMutableResponseArguments), + NoValues(NoValuesResponseArguments), + NoMoreRecentValue(NoMoreRecentValueResponseArguments), +} + +// === PING === +#[derive(Debug, PartialEq, Clone)] +pub struct PingResponseArguments { + pub responder_id: Id, +} + +// === FIND_NODE === +#[derive(Debug, PartialEq, Clone)] +pub struct FindNodeRequestArguments { + pub target: Id, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct FindNodeResponseArguments { + pub responder_id: Id, + pub nodes: Box<[Node]>, +} + +// Get anything + +#[derive(Debug, PartialEq, Clone)] +pub struct GetValueRequestArguments { + pub target: Id, + pub seq: Option, + // A bit of a hack, using this to carry an optional + // salt in the query.request field of [crate::query] + // not really encoded, decoded or sent over the wire. + pub salt: Option>, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct NoValuesResponseArguments { + pub responder_id: Id, + pub token: Box<[u8]>, + pub nodes: Option>, +} + +// === Get Peers === + +#[derive(Debug, PartialEq, Clone)] +pub struct GetPeersRequestArguments { + pub info_hash: Id, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct GetPeersResponseArguments { + pub responder_id: Id, + pub token: Box<[u8]>, + pub values: Vec, + pub nodes: Option>, +} + +// === Announce Peer === + +#[derive(Debug, PartialEq, Clone)] +pub struct AnnouncePeerRequestArguments { + pub info_hash: Id, + pub port: u16, + pub implied_port: Option, +} + +// === Get Immutable === + +#[derive(Debug, PartialEq, Clone)] +pub struct GetImmutableResponseArguments { + pub responder_id: Id, + pub token: Box<[u8]>, + pub nodes: Option>, + pub v: Box<[u8]>, +} + +// === Get Mutable === + +#[derive(Debug, PartialEq, Clone)] +pub struct GetMutableResponseArguments { + pub responder_id: Id, + pub token: Box<[u8]>, + pub nodes: Option>, + pub v: Box<[u8]>, + pub k: [u8; 32], + pub seq: i64, + pub sig: [u8; 64], +} + +#[derive(Debug, PartialEq, Clone)] +pub struct NoMoreRecentValueResponseArguments { + pub responder_id: Id, + pub token: Box<[u8]>, + pub nodes: Option>, + pub seq: i64, +} + +// === Put Immutable === + +#[derive(Debug, PartialEq, Clone)] +pub struct PutImmutableRequestArguments { + pub target: Id, + pub v: Box<[u8]>, +} + +// === Put Mutable === + +#[derive(Debug, PartialEq, Clone)] +pub struct PutMutableRequestArguments { + pub target: Id, + pub v: Box<[u8]>, + pub k: [u8; 32], + pub seq: i64, + pub sig: [u8; 64], + pub salt: Option>, + pub cas: Option, +} + +impl Message { + fn into_serde_message(self) -> internal::DHTMessage { + internal::DHTMessage { + transaction_id: self.transaction_id.to_be_bytes(), + version: self.version, + ip: self + .requester_ip + .map(|sockaddr| sockaddr_to_bytes(&sockaddr)), + read_only: if self.read_only { Some(1) } else { Some(0) }, + variant: match self.message_type { + MessageType::Request(RequestSpecific { + requester_id, + request_type, + }) => internal::DHTMessageVariant::Request(match request_type { + RequestTypeSpecific::Ping => internal::DHTRequestSpecific::Ping { + arguments: internal::DHTPingRequestArguments { + id: requester_id.into(), + }, + }, + RequestTypeSpecific::FindNode(find_node_args) => { + internal::DHTRequestSpecific::FindNode { + arguments: internal::DHTFindNodeRequestArguments { + id: requester_id.into(), + target: find_node_args.target.into(), + }, + } + } + RequestTypeSpecific::GetPeers(get_peers_args) => { + internal::DHTRequestSpecific::GetPeers { + arguments: internal::DHTGetPeersRequestArguments { + id: requester_id.into(), + info_hash: get_peers_args.info_hash.into(), + }, + } + } + RequestTypeSpecific::GetValue(get_mutable_args) => { + internal::DHTRequestSpecific::GetValue { + arguments: internal::DHTGetValueRequestArguments { + id: requester_id.into(), + target: get_mutable_args.target.into(), + seq: get_mutable_args.seq, + }, + } + } + RequestTypeSpecific::Put(PutRequest { + token, + put_request_type, + }) => match put_request_type { + PutRequestSpecific::AnnouncePeer(announce_peer_args) => { + internal::DHTRequestSpecific::AnnouncePeer { + arguments: internal::DHTAnnouncePeerRequestArguments { + id: requester_id.into(), + token, + + info_hash: announce_peer_args.info_hash.into(), + port: announce_peer_args.port, + implied_port: if announce_peer_args.implied_port.is_some() { + Some(1) + } else { + Some(0) + }, + }, + } + } + PutRequestSpecific::PutImmutable(put_immutable_arguments) => { + internal::DHTRequestSpecific::PutValue { + arguments: internal::DHTPutValueRequestArguments { + id: requester_id.into(), + token, + + target: put_immutable_arguments.target.into(), + v: put_immutable_arguments.v, + k: None, + seq: None, + sig: None, + salt: None, + cas: None, + }, + } + } + PutRequestSpecific::PutMutable(put_mutable_arguments) => { + internal::DHTRequestSpecific::PutValue { + arguments: internal::DHTPutValueRequestArguments { + id: requester_id.into(), + token, + + target: put_mutable_arguments.target.into(), + v: put_mutable_arguments.v, + k: Some(put_mutable_arguments.k), + seq: Some(put_mutable_arguments.seq), + sig: Some(put_mutable_arguments.sig), + salt: put_mutable_arguments.salt, + cas: put_mutable_arguments.cas, + }, + } + } + }, + }), + + MessageType::Response(res) => internal::DHTMessageVariant::Response(match res { + ResponseSpecific::Ping(ping_args) => internal::DHTResponseSpecific::Ping { + arguments: internal::DHTPingResponseArguments { + id: ping_args.responder_id.into(), + }, + }, + ResponseSpecific::FindNode(find_node_args) => { + internal::DHTResponseSpecific::FindNode { + arguments: internal::DHTFindNodeResponseArguments { + id: find_node_args.responder_id.into(), + nodes: nodes4_to_bytes(&find_node_args.nodes), + }, + } + } + ResponseSpecific::GetPeers(get_peers_args) => { + internal::DHTResponseSpecific::GetPeers { + arguments: internal::DHTGetPeersResponseArguments { + id: get_peers_args.responder_id.into(), + token: get_peers_args.token, + nodes: get_peers_args + .nodes + .as_ref() + .map(|nodes| nodes4_to_bytes(nodes)), + values: peers_to_bytes(&get_peers_args.values), + }, + } + } + ResponseSpecific::NoValues(no_values_arguments) => { + internal::DHTResponseSpecific::NoValues { + arguments: internal::DHTNoValuesResponseArguments { + id: no_values_arguments.responder_id.into(), + token: no_values_arguments.token, + nodes: no_values_arguments + .nodes + .as_ref() + .map(|nodes| nodes4_to_bytes(nodes)), + }, + } + } + ResponseSpecific::GetImmutable(get_immutable_args) => { + internal::DHTResponseSpecific::GetImmutable { + arguments: internal::DHTGetImmutableResponseArguments { + id: get_immutable_args.responder_id.into(), + token: get_immutable_args.token, + nodes: get_immutable_args + .nodes + .as_ref() + .map(|nodes| nodes4_to_bytes(nodes)), + v: get_immutable_args.v, + }, + } + } + ResponseSpecific::GetMutable(get_mutable_args) => { + internal::DHTResponseSpecific::GetMutable { + arguments: internal::DHTGetMutableResponseArguments { + id: get_mutable_args.responder_id.into(), + token: get_mutable_args.token, + nodes: get_mutable_args + .nodes + .as_ref() + .map(|nodes| nodes4_to_bytes(nodes)), + v: get_mutable_args.v, + k: get_mutable_args.k, + seq: get_mutable_args.seq, + sig: get_mutable_args.sig, + }, + } + } + ResponseSpecific::NoMoreRecentValue(args) => { + internal::DHTResponseSpecific::NoMoreRecentValue { + arguments: internal::DHTNoMoreRecentValueResponseArguments { + id: args.responder_id.into(), + token: args.token, + nodes: args.nodes.as_ref().map(|nodes| nodes4_to_bytes(nodes)), + seq: args.seq, + }, + } + } + }), + + MessageType::Error(err) => { + internal::DHTMessageVariant::Error(internal::DHTErrorSpecific { + error_info: (err.code, err.description), + }) + } + }, + } + } + + fn from_serde_message(msg: internal::DHTMessage) -> Result { + Ok(Message { + transaction_id: u32::from_be_bytes(msg.transaction_id), + version: msg.version, + requester_ip: match msg.ip { + Some(ip) => Some(bytes_to_sockaddr(ip)?), + _ => None, + }, + read_only: if let Some(read_only) = msg.read_only { + read_only > 0 + } else { + false + }, + message_type: match msg.variant { + internal::DHTMessageVariant::Request(req_variant) => { + MessageType::Request(match req_variant { + internal::DHTRequestSpecific::Ping { arguments } => RequestSpecific { + requester_id: Id::from_bytes(arguments.id)?, + request_type: RequestTypeSpecific::Ping, + }, + internal::DHTRequestSpecific::FindNode { arguments } => RequestSpecific { + requester_id: Id::from_bytes(arguments.id)?, + request_type: RequestTypeSpecific::FindNode(FindNodeRequestArguments { + target: Id::from_bytes(arguments.target)?, + }), + }, + internal::DHTRequestSpecific::GetPeers { arguments } => RequestSpecific { + requester_id: Id::from_bytes(arguments.id)?, + request_type: RequestTypeSpecific::GetPeers(GetPeersRequestArguments { + info_hash: Id::from_bytes(arguments.info_hash)?, + }), + }, + internal::DHTRequestSpecific::GetValue { arguments } => RequestSpecific { + requester_id: Id::from_bytes(arguments.id)?, + + request_type: RequestTypeSpecific::GetValue(GetValueRequestArguments { + target: Id::from_bytes(arguments.target)?, + seq: arguments.seq, + salt: None, + }), + }, + internal::DHTRequestSpecific::AnnouncePeer { arguments } => { + RequestSpecific { + requester_id: Id::from_bytes(arguments.id)?, + request_type: RequestTypeSpecific::Put(PutRequest { + token: arguments.token, + put_request_type: PutRequestSpecific::AnnouncePeer( + AnnouncePeerRequestArguments { + implied_port: arguments + .implied_port + .map(|implied_port| implied_port != 0), + info_hash: arguments.info_hash.into(), + port: arguments.port, + }, + ), + }), + } + } + internal::DHTRequestSpecific::PutValue { arguments } => { + if let Some(k) = arguments.k { + RequestSpecific { + requester_id: Id::from_bytes(arguments.id)?, + + request_type: RequestTypeSpecific::Put(PutRequest { + token: arguments.token, + put_request_type: PutRequestSpecific::PutMutable( + PutMutableRequestArguments { + target: Id::from_bytes(arguments.target)?, + v: arguments.v, + k, + seq: arguments.seq.ok_or( + DecodeMessageError::MissingMutableSequence, + )?, + sig: arguments.sig.ok_or( + DecodeMessageError::MissingMutableSignature, + )?, + salt: arguments.salt, + cas: arguments.cas, + }, + ), + }), + } + } else if arguments.seq.is_some() + || arguments.sig.is_some() + || arguments.salt.is_some() + || arguments.cas.is_some() + { + return Err( + DecodeMessageError::UnexpectedMutableFieldsInImmutablePut, + ); + } else { + RequestSpecific { + requester_id: Id::from_bytes(arguments.id)?, + + request_type: RequestTypeSpecific::Put(PutRequest { + token: arguments.token, + put_request_type: PutRequestSpecific::PutImmutable( + PutImmutableRequestArguments { + target: Id::from_bytes(arguments.target)?, + v: arguments.v, + }, + ), + }), + } + } + } + }) + } + + internal::DHTMessageVariant::Response(res_variant) => { + MessageType::Response(match res_variant { + internal::DHTResponseSpecific::Ping { arguments } => { + ResponseSpecific::Ping(PingResponseArguments { + responder_id: Id::from_bytes(arguments.id)?, + }) + } + internal::DHTResponseSpecific::FindNode { arguments } => { + ResponseSpecific::FindNode(FindNodeResponseArguments { + responder_id: Id::from_bytes(arguments.id)?, + nodes: bytes_to_nodes4(&arguments.nodes)?, + }) + } + internal::DHTResponseSpecific::GetPeers { arguments } => { + ResponseSpecific::GetPeers(GetPeersResponseArguments { + responder_id: Id::from_bytes(arguments.id)?, + token: arguments.token, + nodes: match arguments.nodes { + Some(nodes) => Some(bytes_to_nodes4(nodes)?), + None => None, + }, + values: bytes_to_peers(arguments.values)?, + }) + } + internal::DHTResponseSpecific::NoValues { arguments } => { + ResponseSpecific::NoValues(NoValuesResponseArguments { + responder_id: Id::from_bytes(arguments.id)?, + token: arguments.token, + nodes: match arguments.nodes { + Some(nodes) => Some(bytes_to_nodes4(nodes)?), + None => None, + }, + }) + } + internal::DHTResponseSpecific::GetImmutable { arguments } => { + ResponseSpecific::GetImmutable(GetImmutableResponseArguments { + responder_id: Id::from_bytes(arguments.id)?, + token: arguments.token, + nodes: match arguments.nodes { + Some(nodes) => Some(bytes_to_nodes4(nodes)?), + None => None, + }, + v: arguments.v, + }) + } + internal::DHTResponseSpecific::GetMutable { arguments } => { + ResponseSpecific::GetMutable(GetMutableResponseArguments { + responder_id: Id::from_bytes(arguments.id)?, + token: arguments.token, + nodes: match arguments.nodes { + Some(nodes) => Some(bytes_to_nodes4(nodes)?), + None => None, + }, + v: arguments.v, + k: arguments.k, + seq: arguments.seq, + sig: arguments.sig, + }) + } + internal::DHTResponseSpecific::NoMoreRecentValue { arguments } => { + ResponseSpecific::NoMoreRecentValue( + NoMoreRecentValueResponseArguments { + responder_id: Id::from_bytes(arguments.id)?, + token: arguments.token, + nodes: match arguments.nodes { + Some(nodes) => Some(bytes_to_nodes4(nodes)?), + None => None, + }, + seq: arguments.seq, + }, + ) + } + }) + } + + internal::DHTMessageVariant::Error(err) => MessageType::Error(ErrorSpecific { + code: err.error_info.0, + description: err.error_info.1, + }), + }, + }) + } + + pub fn to_bytes(&self) -> Result, serde_bencode::Error> { + self.clone().into_serde_message().to_bytes() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < 15 { + return Err(DecodeMessageError::TooShort); + } else if bytes[0] != 100 { + return Err(DecodeMessageError::NotBencodeDictionary); + } + + Message::from_serde_message(internal::DHTMessage::from_bytes(bytes)?) + } + + /// Return the Id of the sender of the Message + /// + /// This is less straightforward than it seems because not *all* messages are sent + /// with an Id (all are except Error messages). This is reflected in the structure + /// of DHT Messages, and makes it a bit annoying to learn the sender's Id without + /// unraveling the entire message. This method is a convenience method to extract + /// the sender (or "author") Id from the guts of any Message. + pub fn get_author_id(&self) -> Option { + let id = match &self.message_type { + MessageType::Request(arguments) => arguments.requester_id, + MessageType::Response(response_variant) => match response_variant { + ResponseSpecific::Ping(arguments) => arguments.responder_id, + ResponseSpecific::FindNode(arguments) => arguments.responder_id, + ResponseSpecific::GetPeers(arguments) => arguments.responder_id, + ResponseSpecific::GetImmutable(arguments) => arguments.responder_id, + ResponseSpecific::GetMutable(arguments) => arguments.responder_id, + ResponseSpecific::NoValues(arguments) => arguments.responder_id, + ResponseSpecific::NoMoreRecentValue(arguments) => arguments.responder_id, + }, + MessageType::Error(_) => { + return None; + } + }; + + Some(id) + } + + /// If the response contains a closer nodes to the target, return that! + pub fn get_closer_nodes(&self) -> Option<&[Node]> { + match &self.message_type { + MessageType::Response(response_variant) => match response_variant { + ResponseSpecific::Ping(_) => None, + ResponseSpecific::FindNode(arguments) => Some(&arguments.nodes), + ResponseSpecific::GetPeers(arguments) => arguments.nodes.as_deref(), + ResponseSpecific::GetMutable(arguments) => arguments.nodes.as_deref(), + ResponseSpecific::GetImmutable(arguments) => arguments.nodes.as_deref(), + ResponseSpecific::NoValues(arguments) => arguments.nodes.as_deref(), + ResponseSpecific::NoMoreRecentValue(arguments) => arguments.nodes.as_deref(), + }, + _ => None, + } + } + + pub fn get_token(&self) -> Option<(Id, &[u8])> { + match &self.message_type { + MessageType::Response(response_variant) => match response_variant { + ResponseSpecific::Ping(_) => None, + ResponseSpecific::FindNode(_) => None, + ResponseSpecific::GetPeers(arguments) => { + Some((arguments.responder_id, &arguments.token)) + } + ResponseSpecific::GetImmutable(arguments) => { + Some((arguments.responder_id, &arguments.token)) + } + ResponseSpecific::GetMutable(arguments) => { + Some((arguments.responder_id, &arguments.token)) + } + ResponseSpecific::NoValues(arguments) => { + Some((arguments.responder_id, &arguments.token)) + } + ResponseSpecific::NoMoreRecentValue(arguments) => { + Some((arguments.responder_id, &arguments.token)) + } + }, + _ => None, + } + } +} + +fn bytes_to_sockaddr>(bytes: T) -> Result { + let bytes = bytes.as_ref(); + match bytes.len() { + 6 => { + let ip = Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]); + + let port_bytes_as_array: [u8; 2] = bytes[4..6] + .try_into() + .map_err(|_| DecodeMessageError::InvalidPortEncoding)?; + + let port: u16 = u16::from_be_bytes(port_bytes_as_array); + + Ok(SocketAddrV4::new(ip, port)) + } + 18 => Err(DecodeMessageError::Ipv6Unsupported), + _ => Err(DecodeMessageError::InvalidSocketAddrEncodingLength), + } +} + +pub fn sockaddr_to_bytes(sockaddr: &SocketAddrV4) -> [u8; 6] { + let mut bytes = [0u8; 6]; + + bytes[0..4].copy_from_slice(&sockaddr.ip().octets()); + + bytes[4..6].copy_from_slice(&sockaddr.port().to_be_bytes()); + + bytes +} + +const NODE_BYTE_SIZE: usize = ID_SIZE + 6; + +fn nodes4_to_bytes(nodes: &[Node]) -> Box<[u8]> { + let mut bytes = Vec::with_capacity(NODE_BYTE_SIZE * nodes.len()); + + for node in nodes { + bytes.extend_from_slice(node.id().as_bytes()); + bytes.extend_from_slice(&sockaddr_to_bytes(&node.address())); + } + + bytes.into_boxed_slice() +} + +fn bytes_to_nodes4>(bytes: T) -> Result, DecodeMessageError> { + let bytes = bytes.as_ref(); + + if bytes.len() % NODE_BYTE_SIZE != 0 { + return Err(DecodeMessageError::InvalidNodes4); + } + + let expected_num = bytes.len() / NODE_BYTE_SIZE; + let mut to_ret = Vec::with_capacity(expected_num); + for i in 0..bytes.len() / NODE_BYTE_SIZE { + let i = i * NODE_BYTE_SIZE; + let id = Id::from_bytes(&bytes[i..i + ID_SIZE])?; + let sockaddr = bytes_to_sockaddr(&bytes[i + ID_SIZE..i + NODE_BYTE_SIZE])?; + let node = Node::new(id, sockaddr); + to_ret.push(node); + } + + Ok(to_ret.into_boxed_slice()) +} + +fn peers_to_bytes(peers: &[SocketAddrV4]) -> Vec { + peers + .iter() + .map(|p| serde_bytes::ByteBuf::from(sockaddr_to_bytes(p))) + .collect() +} + +fn bytes_to_peers>( + bytes: T, +) -> Result, DecodeMessageError> { + let bytes = bytes.as_ref(); + bytes.iter().map(bytes_to_sockaddr).collect() +} + +#[derive(thiserror::Error, Debug)] +/// Mainline crate error enum. +pub enum DecodeMessageError { + #[error("Expected message to be longer than 15 characters")] + TooShort, + + #[error("Expected message to start with 'd'")] + NotBencodeDictionary, + + #[error("Wrong number of bytes for nodes")] + InvalidNodes4, + + #[error("wrong number of bytes for port")] + InvalidPortEncoding, + + #[error("IPv6 is not yet implemented")] + Ipv6Unsupported, + + #[error("Wrong number of bytes for sockaddr")] + InvalidSocketAddrEncodingLength, + + #[error("mutable put message is missing a sequence number")] + MissingMutableSequence, + + #[error("mutable put message is missing a signature")] + MissingMutableSignature, + + #[error("immutable put message contains mutable-only fields")] + UnexpectedMutableFieldsInImmutablePut, + + #[error("Failed to parse packet bytes: {0}")] + BencodeError(#[from] serde_bencode::Error), + + #[error(transparent)] + InvalidIdSize(#[from] InvalidIdSize), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ping_request() { + let original_msg = Message { + transaction_id: 258, + version: None, + requester_ip: None, + read_only: false, + message_type: MessageType::Request(RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::Ping, + }), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_ping_response() { + let original_msg = Message { + transaction_id: 258, + version: Some([0xde, 0xad, 0, 1]), + requester_ip: Some("99.100.101.102:1030".parse().unwrap()), + read_only: false, + message_type: MessageType::Response(ResponseSpecific::Ping(PingResponseArguments { + responder_id: Id::random(), + })), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_find_node_request() { + let original_msg = Message { + transaction_id: 258, + version: Some([0x62, 0x61, 0x72, 0x66]), + requester_ip: None, + read_only: false, + message_type: MessageType::Request(RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::FindNode(FindNodeRequestArguments { + target: Id::random(), + }), + }), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_find_node_request_read_only() { + let original_msg = Message { + transaction_id: 258, + version: Some([0x62, 0x61, 0x72, 0x66]), + requester_ip: None, + read_only: true, + message_type: MessageType::Request(RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::FindNode(FindNodeRequestArguments { + target: Id::random(), + }), + }), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_find_node_response() { + let original_msg = Message { + transaction_id: 258, + version: Some([1, 2, 3, 4]), + requester_ip: Some("50.51.52.53:5455".parse().unwrap()), + read_only: false, + message_type: MessageType::Response(ResponseSpecific::FindNode( + FindNodeResponseArguments { + responder_id: Id::random(), + nodes: [Node::new(Id::random(), "49.50.52.52:5354".parse().unwrap())].into(), + }, + )), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg.get_author_id(), original_msg.get_author_id()); + assert_eq!( + parsed_msg.get_closer_nodes().map(|nodes| nodes + .iter() + .map(|n| (n.id(), n.address())) + .collect::>()), + original_msg.get_closer_nodes().map(|nodes| nodes + .iter() + .map(|n| (n.id(), n.address())) + .collect::>()) + ); + } + + #[test] + fn test_get_peers_request() { + let original_msg = Message { + transaction_id: 258, + version: Some([72, 73, 0, 1]), + requester_ip: None, + read_only: false, + message_type: MessageType::Request(RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::GetPeers(GetPeersRequestArguments { + info_hash: Id::random(), + }), + }), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_get_peers_response() { + let original_msg = Message { + transaction_id: 3, + version: Some([1, 2, 3, 4]), + requester_ip: Some("50.51.52.53:5455".parse().unwrap()), + read_only: true, + message_type: MessageType::Response(ResponseSpecific::NoValues( + NoValuesResponseArguments { + responder_id: Id::random(), + token: [99, 100, 101, 102].into(), + nodes: Some( + [Node::new(Id::random(), "49.50.52.52:5354".parse().unwrap())].into(), + ), + }, + )), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + + assert_eq!(parsed_msg.transaction_id, original_msg.transaction_id); + assert_eq!(parsed_msg.version, original_msg.version); + assert_eq!(parsed_msg.requester_ip, original_msg.requester_ip); + assert_eq!(parsed_msg.get_author_id(), original_msg.get_author_id()); + assert_eq!( + parsed_msg.get_closer_nodes().map(|nodes| nodes + .iter() + .map(|n| (n.id(), n.address())) + .collect::>()), + original_msg.get_closer_nodes().map(|nodes| nodes + .iter() + .map(|n| (n.id(), n.address())) + .collect::>()) + ); + } + + #[test] + fn test_get_peers_response_peers() { + let original_msg = Message { + transaction_id: 3, + version: Some([1, 2, 3, 4]), + requester_ip: Some("50.51.52.53:5455".parse().unwrap()), + read_only: false, + message_type: MessageType::Response(ResponseSpecific::GetPeers( + GetPeersResponseArguments { + responder_id: Id::random(), + token: vec![99, 100, 101, 102].into(), + nodes: None, + values: ["123.123.123.123:123".parse().unwrap()].into(), + }, + )), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_get_peers_response_neither() { + let serde_message = internal::DHTMessage { + ip: None, + read_only: None, + transaction_id: [1, 2, 3, 4], + version: None, + variant: internal::DHTMessageVariant::Response( + internal::DHTResponseSpecific::NoValues { + arguments: internal::DHTNoValuesResponseArguments { + id: Id::random().into(), + token: vec![0, 1].into(), + nodes: None, + }, + }, + ), + }; + let parsed_msg = Message::from_serde_message(serde_message).unwrap(); + assert!(matches!( + parsed_msg.message_type, + MessageType::Response(ResponseSpecific::NoValues(NoValuesResponseArguments { .. })) + )); + } + + #[test] + fn test_get_immutable_request() { + let original_msg = Message { + transaction_id: 258, + version: Some([72, 73, 0, 1]), + requester_ip: None, + read_only: false, + message_type: MessageType::Request(RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::GetValue(GetValueRequestArguments { + target: Id::random(), + seq: Some(1231), + salt: None, + }), + }), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_get_immutable_response() { + let original_msg = Message { + transaction_id: 3, + version: Some([1, 2, 3, 4]), + requester_ip: Some("50.51.52.53:5455".parse().unwrap()), + read_only: false, + message_type: MessageType::Response(ResponseSpecific::GetImmutable( + GetImmutableResponseArguments { + responder_id: Id::random(), + token: [99, 100, 101, 102].into(), + nodes: None, + v: [99, 100, 101, 102].into(), + }, + )), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_put_immutable_request() { + let original_msg = Message { + transaction_id: 3, + version: Some([1, 2, 3, 4]), + requester_ip: Some("50.51.52.53:5455".parse().unwrap()), + read_only: false, + message_type: MessageType::Request(RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::Put(PutRequest { + token: [99, 100, 101, 102].into(), + put_request_type: PutRequestSpecific::PutImmutable( + PutImmutableRequestArguments { + target: Id::random(), + v: [99, 100, 101, 102].into(), + }, + ), + }), + }), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_put_mutable_request() { + let original_msg = Message { + transaction_id: 3, + version: Some([1, 2, 3, 4]), + requester_ip: Some("50.51.52.53:5455".parse().unwrap()), + read_only: false, + message_type: MessageType::Request(RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::Put(PutRequest { + token: [99, 100, 101, 102].into(), + put_request_type: PutRequestSpecific::PutMutable(PutMutableRequestArguments { + target: Id::random(), + v: [99, 100, 101, 102].into(), + k: [100; 32], + seq: 100, + sig: [0; 64], + salt: Some([0, 2, 4, 8].into()), + cas: Some(100), + }), + }), + }), + }; + + let serde_msg = original_msg.clone().into_serde_message(); + let bytes = serde_msg.to_bytes().unwrap(); + let parsed_serde_msg = internal::DHTMessage::from_bytes(&bytes).unwrap(); + let parsed_msg = Message::from_serde_message(parsed_serde_msg).unwrap(); + assert_eq!(parsed_msg, original_msg); + } + + #[test] + fn test_put_mutable_request_missing_sequence_is_rejected() { + let message = internal::DHTMessage { + ip: None, + read_only: None, + transaction_id: [1, 2, 3, 4], + version: None, + variant: internal::DHTMessageVariant::Request(internal::DHTRequestSpecific::PutValue { + arguments: internal::DHTPutValueRequestArguments { + id: Id::random().into(), + target: Id::random().into(), + token: vec![0].into(), + v: vec![0].into(), + k: Some([0; 32]), + sig: Some([0; 64]), + seq: None, + cas: None, + salt: None, + }, + }), + }; + + assert!(matches!( + Message::from_bytes(&message.to_bytes().unwrap()), + Err(DecodeMessageError::MissingMutableSequence) + )); + } + + #[test] + fn test_put_mutable_request_missing_signature_is_rejected() { + let message = internal::DHTMessage { + ip: None, + read_only: None, + transaction_id: [1, 2, 3, 4], + version: None, + variant: internal::DHTMessageVariant::Request(internal::DHTRequestSpecific::PutValue { + arguments: internal::DHTPutValueRequestArguments { + id: Id::random().into(), + target: Id::random().into(), + token: vec![0].into(), + v: vec![0].into(), + k: Some([0; 32]), + sig: None, + seq: Some(0), + cas: None, + salt: None, + }, + }), + }; + + assert!(matches!( + Message::from_bytes(&message.to_bytes().unwrap()), + Err(DecodeMessageError::MissingMutableSignature) + )); + } + + #[test] + fn test_put_immutable_request_with_mutable_fields_is_rejected() { + let immutable_put = || internal::DHTMessage { + ip: None, + read_only: None, + transaction_id: [1, 2, 3, 4], + version: None, + variant: internal::DHTMessageVariant::Request(internal::DHTRequestSpecific::PutValue { + arguments: internal::DHTPutValueRequestArguments { + id: Id::random().into(), + target: Id::random().into(), + token: vec![0].into(), + v: vec![0].into(), + k: None, + sig: None, + seq: None, + cas: None, + salt: None, + }, + }), + }; + + let mut message = immutable_put(); + let internal::DHTMessageVariant::Request(internal::DHTRequestSpecific::PutValue { + arguments, + }) = &mut message.variant + else { + unreachable!("constructed a put value request"); + }; + arguments.seq = Some(0); + assert!(matches!( + Message::from_bytes(&message.to_bytes().unwrap()), + Err(DecodeMessageError::UnexpectedMutableFieldsInImmutablePut) + )); + + let mut message = immutable_put(); + let internal::DHTMessageVariant::Request(internal::DHTRequestSpecific::PutValue { + arguments, + }) = &mut message.variant + else { + unreachable!("constructed a put value request"); + }; + arguments.sig = Some([0; 64]); + assert!(matches!( + Message::from_bytes(&message.to_bytes().unwrap()), + Err(DecodeMessageError::UnexpectedMutableFieldsInImmutablePut) + )); + + let mut message = immutable_put(); + let internal::DHTMessageVariant::Request(internal::DHTRequestSpecific::PutValue { + arguments, + }) = &mut message.variant + else { + unreachable!("constructed a put value request"); + }; + arguments.salt = Some(vec![0].into()); + assert!(matches!( + Message::from_bytes(&message.to_bytes().unwrap()), + Err(DecodeMessageError::UnexpectedMutableFieldsInImmutablePut) + )); + + let mut message = immutable_put(); + let internal::DHTMessageVariant::Request(internal::DHTRequestSpecific::PutValue { + arguments, + }) = &mut message.variant + else { + unreachable!("constructed a put value request"); + }; + arguments.cas = Some(0); + assert!(matches!( + Message::from_bytes(&message.to_bytes().unwrap()), + Err(DecodeMessageError::UnexpectedMutableFieldsInImmutablePut) + )); + } +} diff --git a/vendor/mainline/src/common/messages/internal.rs b/vendor/mainline/src/common/messages/internal.rs new file mode 100644 index 0000000..04bf647 --- /dev/null +++ b/vendor/mainline/src/common/messages/internal.rs @@ -0,0 +1,333 @@ +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTMessage { + #[serde(rename = "t", with = "serde_bytes")] + // Only few messages received seems to not use exactly 2 bytes, + // and they don't seem to have a version. + pub transaction_id: [u8; 4], + + #[serde(default)] + #[serde(rename = "v", with = "serde_bytes")] + pub version: Option<[u8; 4]>, + + #[serde(flatten)] + pub variant: DHTMessageVariant, + + #[serde(default)] + #[serde(with = "serde_bytes")] + // Ipv6 is not supported anyways. + pub ip: Option<[u8; 6]>, + + #[serde(default)] + #[serde(rename = "ro")] + pub read_only: Option, +} + +impl DHTMessage { + pub fn from_bytes(bytes: &[u8]) -> Result { + let obj = serde_bencode::from_bytes(bytes)?; + Ok(obj) + } + + pub fn to_bytes(&self) -> Result, serde_bencode::Error> { + serde_bencode::to_bytes(self) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(tag = "y")] +pub enum DHTMessageVariant { + #[serde(rename = "q")] + Request(DHTRequestSpecific), + + #[serde(rename = "r")] + Response(DHTResponseSpecific), + + #[serde(rename = "e")] + Error(DHTErrorSpecific), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(tag = "q")] +pub enum DHTRequestSpecific { + #[serde(rename = "ping")] + Ping { + #[serde(rename = "a")] + arguments: DHTPingRequestArguments, + }, + + #[serde(rename = "find_node")] + FindNode { + #[serde(rename = "a")] + arguments: DHTFindNodeRequestArguments, + }, + + #[serde(rename = "get_peers")] + GetPeers { + #[serde(rename = "a")] + arguments: DHTGetPeersRequestArguments, + }, + + #[serde(rename = "announce_peer")] + AnnouncePeer { + #[serde(rename = "a")] + arguments: DHTAnnouncePeerRequestArguments, + }, + + #[serde(rename = "get")] + GetValue { + #[serde(rename = "a")] + arguments: DHTGetValueRequestArguments, + }, + + #[serde(rename = "put")] + PutValue { + #[serde(rename = "a")] + arguments: DHTPutValueRequestArguments, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] // This means order matters! Order these from most to least detailed +pub enum DHTResponseSpecific { + GetMutable { + #[serde(rename = "r")] + arguments: DHTGetMutableResponseArguments, + }, + + NoMoreRecentValue { + #[serde(rename = "r")] + arguments: DHTNoMoreRecentValueResponseArguments, + }, + + GetImmutable { + #[serde(rename = "r")] + arguments: DHTGetImmutableResponseArguments, + }, + + GetPeers { + #[serde(rename = "r")] + arguments: DHTGetPeersResponseArguments, + }, + + NoValues { + #[serde(rename = "r")] + arguments: DHTNoValuesResponseArguments, + }, + + FindNode { + #[serde(rename = "r")] + arguments: DHTFindNodeResponseArguments, + }, + + Ping { + #[serde(rename = "r")] + arguments: DHTPingResponseArguments, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTErrorSpecific { + #[serde(rename = "e")] + pub error_info: (i32, String), +} + +// === PING === + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTPingRequestArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTPingResponseArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], +} + +// === FIND NODE === + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTFindNodeRequestArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub target: [u8; 20], +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTFindNodeResponseArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub nodes: Box<[u8]>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTNoValuesResponseArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub token: Box<[u8]>, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub nodes: Option>, +} + +// === Get Peers === + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTGetPeersRequestArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub info_hash: [u8; 20], +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTGetPeersResponseArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub token: Box<[u8]>, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub nodes: Option>, + + // values are not optional, because if they are missing this missing + // we can just treat this as DHTNoValuesResponseArguments + pub values: Vec, +} + +// === Announce Peer === + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTAnnouncePeerRequestArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub info_hash: [u8; 20], + + pub port: u16, + + #[serde(with = "serde_bytes")] + pub token: Box<[u8]>, + + #[serde(default)] + pub implied_port: Option, +} + +// === Get Value === + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTGetValueRequestArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub target: [u8; 20], + + #[serde(default)] + pub seq: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTGetImmutableResponseArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub token: Box<[u8]>, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub nodes: Option>, + + #[serde(with = "serde_bytes")] + pub v: Box<[u8]>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTNoMoreRecentValueResponseArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub token: Box<[u8]>, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub nodes: Option>, + + pub seq: i64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTGetMutableResponseArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub token: Box<[u8]>, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub nodes: Option>, + + #[serde(with = "serde_bytes")] + pub v: Box<[u8]>, + + #[serde(with = "serde_bytes")] + pub k: [u8; 32], + + #[serde(with = "serde_bytes")] + pub sig: [u8; 64], + + pub seq: i64, +} + +// === Put Value === + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct DHTPutValueRequestArguments { + #[serde(with = "serde_bytes")] + pub id: [u8; 20], + + #[serde(with = "serde_bytes")] + pub target: [u8; 20], + + #[serde(with = "serde_bytes")] + pub token: Box<[u8]>, + + #[serde(with = "serde_bytes")] + pub v: Box<[u8]>, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub k: Option<[u8; 32]>, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub sig: Option<[u8; 64]>, + + #[serde(default)] + pub seq: Option, + + #[serde(default)] + pub cas: Option, + + #[serde(with = "serde_bytes")] + #[serde(default)] + pub salt: Option>, +} diff --git a/vendor/mainline/src/common/mutable.rs b/vendor/mainline/src/common/mutable.rs new file mode 100644 index 0000000..9a7f5e4 --- /dev/null +++ b/vendor/mainline/src/common/mutable.rs @@ -0,0 +1,286 @@ +//! Helper functions and structs for mutable items. + +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use serde::{Deserialize, Serialize}; +use sha1_smol::Sha1; +use std::convert::TryFrom; + +use crate::Id; + +use super::PutMutableRequestArguments; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +/// [BEP_0044](https://www.bittorrent.org/beps/bep_0044.html)'s Mutable item. +pub struct MutableItem { + /// hash of the key and optional salt + target: Id, + /// ed25519 public key + key: [u8; 32], + /// sequence number + pub(crate) seq: i64, + /// mutable value + pub(crate) value: Box<[u8]>, + /// ed25519 signature + #[serde(with = "serde_bytes")] + signature: [u8; 64], + /// Optional salt + salt: Option>, +} + +impl MutableItem { + /// Create a new mutable item from a signing key, value, sequence number and optional salt. + pub fn new(signer: SigningKey, value: &[u8], seq: i64, salt: Option<&[u8]>) -> Self { + let signable = encode_signable(seq, value, salt); + let signature = signer.sign(&signable); + + Self::new_signed_unchecked( + signer.verifying_key().to_bytes(), + signature.into(), + value, + seq, + salt, + ) + } + + /// Return the target of a [MutableItem] by hashing its `public_key` and an optional `salt` + pub fn target_from_key(public_key: &[u8; 32], salt: Option<&[u8]>) -> Id { + let mut encoded = vec![]; + + encoded.extend(public_key); + + if let Some(salt) = salt { + encoded.extend(salt); + } + + let mut hasher = Sha1::new(); + hasher.update(&encoded); + let bytes = hasher.digest().bytes(); + + bytes.into() + } + + /// Create a new mutable item from an already signed value. + pub fn new_signed_unchecked( + key: [u8; 32], + signature: [u8; 64], + value: &[u8], + seq: i64, + salt: Option<&[u8]>, + ) -> Self { + Self { + target: MutableItem::target_from_key(&key, salt), + key, + value: value.into(), + seq, + signature, + salt: salt.map(|s| s.into()), + } + } + + pub(crate) fn from_dht_message( + target: Id, + key: &[u8], + v: Box<[u8]>, + seq: i64, + signature: &[u8], + salt: Option>, + ) -> Result { + let key = VerifyingKey::try_from(key).map_err(|_| MutableError::InvalidMutablePublicKey)?; + + let signature = + Signature::from_slice(signature).map_err(|_| MutableError::InvalidMutableSignature)?; + + key.verify(&encode_signable(seq, &v, salt.as_deref()), &signature) + .map_err(|_| MutableError::InvalidMutableSignature)?; + + if Self::target_from_key(&key.to_bytes(), salt.as_deref()) != target { + return Err(MutableError::InvalidMutableTarget); + } + + Ok(Self { + target, + key: key.to_bytes(), + value: v, + seq, + signature: signature.to_bytes(), + salt, + }) + } + + // === Getters === + + /// Returns the target (info hash) of this item. + pub fn target(&self) -> &Id { + &self.target + } + + /// Returns a reference to the 32 bytes Ed25519 public key of this item. + pub fn key(&self) -> &[u8; 32] { + &self.key + } + + /// Returns a byte slice of the value of this item. + pub fn value(&self) -> &[u8] { + &self.value + } + + /// Returns the `seq` (sequence) number of this item. + pub fn seq(&self) -> i64 { + self.seq + } + + /// Returns the signature over this item. + pub fn signature(&self) -> &[u8; 64] { + &self.signature + } + + /// Returns the `Salt` value used for generating the + /// [Self::target] if any. + pub fn salt(&self) -> Option<&[u8]> { + self.salt.as_deref() + } +} + +pub fn encode_signable(seq: i64, value: &[u8], salt: Option<&[u8]>) -> Box<[u8]> { + let mut signable = vec![]; + + if let Some(salt) = salt { + signable.extend(format!("4:salt{}:", salt.len()).into_bytes()); + signable.extend(salt); + } + + signable.extend(format!("3:seqi{}e1:v{}:", seq, value.len()).into_bytes()); + signable.extend(value); + + signable.into() +} + +pub(crate) fn most_recent_mutable_item( + most_recent: Option, + item: MutableItem, +) -> Option { + match most_recent { + Some(mr) + if mr.seq() > item.seq() || (mr.seq() == item.seq() && mr.value() >= item.value()) => + { + Some(mr) + } + _ => Some(item), + } +} + +#[derive(thiserror::Error, Debug)] +/// Mainline crate error enum. +pub enum MutableError { + #[error("Invalid mutable item signature")] + /// Invalid mutable item signature + InvalidMutableSignature, + + #[error("Invalid mutable item public key")] + /// Invalid mutable item public key + InvalidMutablePublicKey, + + #[error("Mutable item target does not match its public key and salt")] + /// Mutable item target does not match its public key and salt + InvalidMutableTarget, +} + +impl PutMutableRequestArguments { + /// Create a [PutMutableRequestArguments] from a [MutableItem], + /// and an optional CAS condition, which is usually the [MutableItem::seq] + /// of the most recent known [MutableItem] + pub fn from(item: MutableItem, cas: Option) -> Self { + Self { + target: item.target, + v: item.value, + k: item.key, + seq: item.seq, + sig: item.signature, + salt: item.salt, + cas, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use ed25519_dalek::SigningKey; + + #[test] + fn signable_without_salt() { + let signable = encode_signable(4, b"Hello world!", None); + + assert_eq!(&*signable, b"3:seqi4e1:v12:Hello world!"); + } + #[test] + fn signable_with_salt() { + let signable = encode_signable(4, b"Hello world!", Some(b"foobar")); + + assert_eq!(&*signable, b"4:salt6:foobar3:seqi4e1:v12:Hello world!"); + } + + #[test] + fn most_recent_mutable_item_selects_by_seq_then_value() { + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let lower_seq = MutableItem::new(signer.clone(), b"lower-seq", 999, None); + let current = MutableItem::new(signer.clone(), b"current1", 1000, None); + let higher_seq = MutableItem::new(signer.clone(), b"higher-seq", 1001, None); + let same_seq_lower_value = MutableItem::new(signer.clone(), b"current0", 1000, None); + let same_seq_higher_value = MutableItem::new(signer, b"current2", 1000, None); + + assert_eq!( + most_recent_mutable_item(None, current.clone()), + Some(current.clone()) + ); + + assert_eq!( + most_recent_mutable_item(Some(current.clone()), higher_seq.clone()), + Some(higher_seq) + ); + + assert_eq!( + most_recent_mutable_item(Some(current.clone()), lower_seq), + Some(current.clone()) + ); + + assert_eq!( + most_recent_mutable_item(Some(current.clone()), same_seq_higher_value.clone()), + Some(same_seq_higher_value) + ); + + assert_eq!( + most_recent_mutable_item(Some(current.clone()), same_seq_lower_value), + Some(current.clone()) + ); + + assert_eq!( + most_recent_mutable_item(Some(current.clone()), current.clone()), + Some(current) + ); + } + + #[test] + fn from_dht_message_rejects_a_signed_item_for_a_different_target() { + let signer = SigningKey::from_bytes(&[42; 32]); + let item = MutableItem::new(signer, b"value", 1, Some(b"salt")); + let mut wrong_target = *item.target().as_bytes(); + wrong_target[0] ^= 1; + + let result = MutableItem::from_dht_message( + wrong_target.into(), + item.key(), + item.value().into(), + item.seq(), + item.signature(), + item.salt().map(Into::into), + ); + + assert!(matches!(result, Err(MutableError::InvalidMutableTarget))); + } +} diff --git a/vendor/mainline/src/common/node.rs b/vendor/mainline/src/common/node.rs new file mode 100644 index 0000000..5260ff9 --- /dev/null +++ b/vendor/mainline/src/common/node.rs @@ -0,0 +1,142 @@ +//! Struct and implementation of the Node entry in the Kademlia routing table +use std::{ + fmt::{self, Debug, Formatter}, + net::SocketAddrV4, + sync::Arc, + time::{Duration, Instant}, +}; + +use crate::common::Id; + +/// The age of a node's last_seen time before it is considered stale and removed from a full bucket +/// on inserting a new node. +pub const STALE_TIME: Duration = Duration::from_secs(15 * 60); +const MIN_PING_BACKOFF_INTERVAL: Duration = Duration::from_secs(10); +pub const TOKEN_ROTATE_INTERVAL: Duration = Duration::from_secs(60 * 5); + +#[derive(PartialEq)] +pub(crate) struct NodeInner { + pub(crate) id: Id, + pub(crate) address: SocketAddrV4, + pub(crate) token: Option>, + pub(crate) last_seen: Instant, +} + +impl NodeInner { + pub fn random() -> Self { + Self { + id: Id::random(), + address: SocketAddrV4::new(0.into(), 0), + token: None, + last_seen: Instant::now(), + } + } +} + +#[derive(Clone, PartialEq)] +/// Node entry in Kademlia routing table +pub struct Node(pub(crate) Arc); + +impl Debug for Node { + fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + fmt.debug_struct("Node") + .field("id", &self.0.id) + .field("address", &self.0.address) + .field("last_seen", &self.0.last_seen.elapsed().as_secs()) + .finish() + } +} + +impl Node { + /// Creates a new Node from an id and socket address. + pub fn new(id: Id, address: SocketAddrV4) -> Node { + Node(Arc::new(NodeInner { + id, + address, + token: None, + last_seen: Instant::now(), + })) + } + + pub(crate) fn new_with_token(id: Id, address: SocketAddrV4, token: Box<[u8]>) -> Self { + Node(Arc::new(NodeInner { + id, + address, + token: Some(token), + last_seen: Instant::now(), + })) + } + + /// Creates a node with random Id for testing purposes. + pub fn random() -> Node { + Node(Arc::new(NodeInner::random())) + } + + /// Create a node that is unique per `i` as it has a random Id and sets IP and port to `i` + #[cfg(test)] + pub fn unique(i: usize) -> Node { + Node::new(Id::random(), SocketAddrV4::new((i as u32).into(), i as u16)) + } + + // === Getters === + + /// Returns the id of this node + pub fn id(&self) -> &Id { + &self.0.id + } + + /// Returns the address of this node + pub fn address(&self) -> SocketAddrV4 { + self.0.address + } + + /// Returns the token we received from this node if any. + pub fn token(&self) -> Option> { + self.0.token.clone() + } + + /// Node is last seen more than a threshold ago. + pub fn is_stale(&self) -> bool { + self.0.last_seen.elapsed() > STALE_TIME + } + + /// Node's token was received 5 minutes ago or less + pub fn valid_token(&self) -> bool { + self.0.last_seen.elapsed() <= TOKEN_ROTATE_INTERVAL + } + + pub(crate) fn should_ping(&self) -> bool { + self.0.last_seen.elapsed() > MIN_PING_BACKOFF_INTERVAL + } + + /// Returns true if both nodes have the same ip and port + pub fn same_address(&self, other: &Self) -> bool { + self.0.address == other.0.address + } + + /// Returns true if both nodes have the same ip + pub fn same_ip(&self, other: &Self) -> bool { + self.0.address.ip() == other.0.address.ip() + } + + /// Node [Id] is valid for its IP address. + /// + /// Check [BEP_0042](https://www.bittorrent.org/beps/bep_0042.html). + pub fn is_secure(&self) -> bool { + self.0.id.is_valid_for_ip(*self.0.address.ip()) + } + + /// Returns true if Any of the existing nodes: + /// - Have the same IP as this node, And: + /// = The existing nodes is Not secure. + /// = The existing nodes is secure And shares the same first 21 bits. + /// + /// Effectively, allows only One non-secure node or Eight secure nodes from the same IP, in the routing table or ClosestNodes. + pub(crate) fn already_exists(&self, nodes: &[Self]) -> bool { + nodes.iter().any(|existing| { + self.same_ip(existing) + && (!existing.is_secure() + || self.id().first_21_bits() == existing.id().first_21_bits()) + }) + } +} diff --git a/vendor/mainline/src/common/routing_table.rs b/vendor/mainline/src/common/routing_table.rs new file mode 100644 index 0000000..1899513 --- /dev/null +++ b/vendor/mainline/src/common/routing_table.rs @@ -0,0 +1,649 @@ +//! Simplified Kademlia routing table + +use std::collections::BTreeMap; +use std::slice::Iter; + +use crate::common::{Id, Node}; +use crate::rpc::ClosestNodes; + +/// K = the default maximum size of a k-bucket. +pub const MAX_BUCKET_SIZE_K: usize = 20; + +#[derive(Debug, Clone)] +/// Simplified Kademlia routing table +pub struct RoutingTable { + id: Id, + buckets: BTreeMap, +} + +impl RoutingTable { + /// Create a new [RoutingTable] with a given id. + pub fn new(id: Id) -> Self { + let buckets = BTreeMap::new(); + + RoutingTable { id, buckets } + } + + /// Returns the [Id] of this node, where the distance is measured from. + pub fn id(&self) -> &Id { + &self.id + } + + /// Returns the map of distances and their [KBucket] + pub(crate) fn buckets(&self) -> &BTreeMap { + &self.buckets + } + + // === Public Methods === + + /// Attempts to add a node to this routing table, and return `true` if it did. + pub fn add(&mut self, node: Node) -> bool { + let distance = self.id.distance(node.id()); + + if distance == 0 { + // Do not add self to the routing_table + return false; + } + + if self + .buckets() + .values() + .any(|bucket| node.already_exists(&bucket.nodes)) + { + return false; + }; + + let bucket = self.buckets.entry(distance).or_default(); + + bucket.add(node) + } + + /// Remove a node from this routing table. + pub fn remove(&mut self, node_id: &Id) { + let distance = self.id.distance(node_id); + + if let Some(bucket) = self.buckets.get_mut(&distance) { + bucket.remove(node_id) + } + } + + /// Return the closest nodes to the target while prioritizing secure nodes, + /// as defined in [BEP_0042](https://www.bittorrent.org/beps/bep_0042.html) + pub fn closest(&self, target: Id) -> Box<[Node]> { + let mut closest = ClosestNodes::new(target); + + for bucket in self.buckets.values() { + for node in &bucket.nodes { + closest.add(node.clone()); + } + } + + closest.nodes()[..MAX_BUCKET_SIZE_K.min(closest.len())].into() + } + + /// Secure version of [Self::closest] that tries to circumvent sybil attacks. + pub fn closest_secure( + &self, + target: Id, + dht_size_estimate: usize, + subnets: usize, + ) -> Vec { + let mut closest = ClosestNodes::new(target); + + for node in self.nodes() { + closest.add(node); + } + + closest + .take_until_secure(dht_size_estimate, subnets) + .to_vec() + } + + /// Returns `true` if this routing table is empty. + pub fn is_empty(&self) -> bool { + self.buckets.values().all(|bucket| bucket.is_empty()) + } + + /// Return the number of nodes in this routing table. + pub fn size(&self) -> usize { + self.buckets + .values() + .fold(0, |acc, bucket| acc + bucket.nodes.len()) + } + + /// Returns an iterator over the nodes in this routing table. + pub fn nodes(&self) -> RoutingTableIterator<'_> { + RoutingTableIterator { + bucket_index: 1, + node_index: 0, + table: self, + } + } + + /// Export an owned vector of nodes from this routing table. + pub fn to_owned_nodes(&self) -> Vec { + self.nodes().collect() + } + + /// Turn this routing table to a list of bootstrapping nodes. + pub fn to_bootstrap(&self) -> Vec { + self.nodes() + .filter(|n| !n.is_stale()) + .map(|n| n.address().to_string()) + .collect() + } + + // === Private Methods === + + #[cfg(test)] + fn contains(&self, node_id: &Id) -> bool { + let distance = self.id.distance(node_id); + + if let Some(bucket) = self.buckets.get(&distance) { + if bucket.contains(node_id) { + return true; + } + } + false + } +} + +pub struct RoutingTableIterator<'a> { + bucket_index: u8, + node_index: usize, + table: &'a RoutingTable, +} + +impl Iterator for RoutingTableIterator<'_> { + type Item = Node; + + fn next(&mut self) -> Option { + while self.bucket_index <= 160 { + if let Some(current_bucket) = self.table.buckets.get(&self.bucket_index) { + if let Some(current_node) = current_bucket.nodes.get(self.node_index) { + self.node_index += 1; + + if self.node_index == current_bucket.nodes.len() { + self.node_index = 0; + self.bucket_index += 1; + } + + return Some(current_node.clone()); + } + }; + + self.bucket_index += 1; + } + + None + } +} + +/// Kbuckets are similar to LRU caches that checks and evicts unresponsive nodes, +/// without dropping any responsive nodes in the process. +#[derive(Debug, Clone)] +pub struct KBucket { + /// Nodes in the k-bucket, sorted by the least recently seen. + nodes: Vec, +} + +impl KBucket { + pub fn new() -> Self { + KBucket { + nodes: Vec::with_capacity(MAX_BUCKET_SIZE_K), + } + } + + // === Getters === + + // === Public Methods === + + pub fn add(&mut self, incoming: Node) -> bool { + if let Some(index) = self.iter().position(|n| n.id() == incoming.id()) { + let existing = self.nodes[index].clone(); + + // If the incoming node is secure, then we trust its IP address for this Id, + // and even if it changed its port number, we should accept it. + // + // If neither nodes are secure for this Id, but the incoming is the same IP, + // then add the incoming one, effectively updating the node's + // `last_seen` and moving it to the end of the bucket. + // Possibly also updating the port, which is a good thing, instead of waiting + // for the old port to timeout (not responding to Pings). + // + // Using same ip instead of same address, allow + if incoming.is_secure() || (!existing.is_secure() && existing.same_ip(&incoming)) { + self.nodes.remove(index); + self.nodes.push(incoming); + + true + } else { + false + } + } else if self.nodes.len() < MAX_BUCKET_SIZE_K { + self.nodes.push(incoming); + true + } else if self.nodes[0].is_stale() { + // Remove the least recently seen node and add the new one + self.nodes.remove(0); + self.nodes.push(incoming); + + true + } else { + false + } + } + + pub fn remove(&mut self, node_id: &Id) { + self.nodes.retain(|node| node.id() != node_id); + } + + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + + pub fn iter(&self) -> Iter<'_, Node> { + self.nodes.iter() + } + + #[cfg(test)] + fn contains(&self, id: &Id) -> bool { + self.iter().any(|node| node.id() == id) + } +} + +impl Default for KBucket { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod test { + use std::net::SocketAddrV4; + use std::str::FromStr; + use std::sync::Arc; + use std::time::Instant; + + use crate::common::{Id, KBucket, Node, NodeInner, RoutingTable, MAX_BUCKET_SIZE_K}; + + #[test] + fn table_is_empty() { + let mut table = RoutingTable::new(Id::random()); + assert!(table.is_empty()); + + table.add(Node::random()); + assert!(!table.is_empty()); + } + + #[test] + fn to_vec() { + let mut table = RoutingTable::new(Id::random()); + + let mut expected_nodes: Vec = vec![]; + + for i in 0..MAX_BUCKET_SIZE_K { + expected_nodes.push(Node::unique(i)); + } + + for node in &expected_nodes { + table.add(node.clone()); + } + + let mut sorted_table = table.nodes().collect::>(); + sorted_table.sort_by(|a, b| a.id().cmp(b.id())); + + let mut sorted_expected = expected_nodes.to_vec(); + sorted_expected.sort_by(|a, b| a.id().cmp(b.id())); + + assert_eq!(sorted_table, sorted_expected); + } + + #[test] + fn contains() { + let mut table = RoutingTable::new(Id::random()); + + let node = Node::random(); + + assert!(!table.contains(node.id())); + + table.add(node.clone()); + assert!(table.contains(node.id())); + } + + #[test] + fn remove() { + let mut table = RoutingTable::new(Id::random()); + + let node = Node::random(); + + table.add(node.clone()); + assert!(table.contains(node.id())); + + table.remove(node.id()); + assert!(!table.contains(node.id())); + } + + #[test] + fn buckets_are_sets() { + let mut table = RoutingTable::new(Id::random()); + + let node1 = Node::random(); + let node2 = Node::new(*node1.id(), node1.address()); + + table.add(node1); + table.add(node2); + + assert_eq!(table.size(), 1); + } + + #[test] + fn should_not_add_self() { + let mut table = RoutingTable::new(Id::random()); + let node = Node::new(*table.id(), SocketAddrV4::new(0.into(), 0)); + + table.add(node.clone()); + + assert!(!table.add(node)); + assert!(table.is_empty()) + } + + #[test] + fn should_not_add_more_than_k() { + let mut bucket = KBucket::new(); + + for i in 0..MAX_BUCKET_SIZE_K { + let node = Node::random(); + assert!(bucket.add(node), "Failed to add node {}", i); + } + + let node = Node::random(); + + assert!(!bucket.add(node)); + } + + #[test] + fn should_update_existing_node() { + // Same address + { + let mut bucket = KBucket::new(); + + let node1 = Node::random(); + let node2 = Node::new(*node1.id(), node1.address()); + + bucket.add(node1.clone()); + bucket.add(Node::random()); + + assert_ne!(bucket.nodes[1].id(), node1.id()); + + bucket.add(node2); + + assert_eq!(bucket.nodes.len(), 2); + assert_eq!(bucket.nodes[1].id(), node1.id()); + } + + // Different port + { + let mut bucket = KBucket::new(); + + let node1 = Node::random(); + let node2 = Node::new(*node1.id(), SocketAddrV4::new(*node1.address().ip(), 1)); + + bucket.add(node1.clone()); + bucket.add(Node::random()); + + assert_ne!(bucket.nodes[1].id(), node1.id()); + + bucket.add(node2.clone()); + + assert_eq!(bucket.nodes.len(), 2); + assert_eq!(bucket.nodes[1].id(), node1.id()); + } + + { + let mut bucket = KBucket::new(); + + let secure = Node(Arc::new(NodeInner { + id: Id::from_str("5a3ce9c14e7a08645677bbd1cfe7d8f956d53256").unwrap(), + address: SocketAddrV4::new([21, 75, 31, 124].into(), 0), + token: None, + last_seen: Instant::now(), + })); + + let unsecure = Node::new(*secure.id(), SocketAddrV4::new([0, 0, 0, 0].into(), 1)); + + { + bucket.add(unsecure.clone()); + bucket.add(secure.clone()); + + assert_eq!(bucket.nodes[0].address(), secure.address()) + } + + { + bucket.add(secure.clone()); + bucket.add(unsecure.clone()); + + assert_eq!(bucket.nodes[0].address(), secure.address()) + } + } + + // Different ip + { + let mut bucket = KBucket::new(); + + let node1 = Node::random(); + let node2 = Node::new(*node1.id(), SocketAddrV4::new([0, 0, 0, 1].into(), 1)); + + bucket.add(node1.clone()); + bucket.add(Node::random()); + + assert_ne!(bucket.nodes[1].id(), node1.id()); + + bucket.add(node2.clone()); + + assert_eq!(bucket.nodes.len(), 2); + assert_ne!(bucket.nodes[1].id(), node1.id()); + assert_ne!(bucket.nodes[1].address(), node2.address()); + } + } + + #[test] + fn closest() { + let ids = [ + "fb449c17f6c34fadea26a5a83e1952e815e001ea", + "e63b72f95aacee40ad087f83afb475645739f669", + "58c65677e3833cb0f15733a6363cc4cb1352f90a", + "fd042ff1404b495720ad8345404ff5f25acd02a8", + "dbed34a2c8db568fe59c10adcca9e81825b3dcfd", + "079d40b746b5721f59972ebde423429739844914", + "094f1d2fb4b95ba2c3250b014a9f06d13cd9eb9a", + "98805a55523458c56d59339266bdcecc82370ecd", + "0a1d6cce47c60f2c7357e9fec2910192de6eb336", + "fb689ce0e18c2c22f316976d3ae524aed4137773", + "0d01c32b4cf386b0b784b718b999d0e9dac07876", + "9465e80d80f707b222c4ae6ee81c02b62f607629", + "6cdc012328cc7a3a9a5b967e93387686e19c9f75", + "99719dfc220b145e2aac71d6b3e276731d85be1c", + "94d2037bbc534a5f1d672ce3e3350576c2b78ed1", + "b48d0aeb94cd3766f23d2ac098bbccf01485dc20", + "3b6e1c05f199edd7dee87d3cc8422c8f0ed02358", + "d9b50c6ca730c89f8fc9f518136cef6139dd2252", + "15827c92e6efbc4f56e507e548409c4bc04360bf", + "3c8ff1e484c21132f8e6b8112a2feab984536f57", + "c9a8163fa3e85065d46567bfac39b5452cfb3ae8", + "ef79f77e9eed9ad51094ce2747e2c4fdc3a81326", + "81f038cabb8a845f39da0d40716bf0707da55187", + "907fdf0aa137200b395bc210763ed947b03dfc2e", + "b0bce9873042aee29cbc7ec395647f6cc7a482f8", + "e6b8d5567bc05d9b68f23d562645bc030729abc9", + "74667cb7c629fb7e63749134b16e27446984c517", + "cdc7f4d5825dc316de20d998bc0f1c5e91e36a5e", + "701e7b5af5fabcf0bc3de97cb05a7c00da3e53c6", + "36eb09b1db4af2b11312742faa2bb42621fce753", + "9e4923966754c02b036698e95f95cec8fc40a9d2", + "0e43d66e9da1bfc7e2581155dfd1b8f4be57d3f1", + "647679a0d8816d2f62200e7b6ef6171297756dd5", + "c03d9008add37f8414cb41549448bb2dcb5c6c9b", + "dff82b028a6ec033e00b387df8e386417b92a47c", + "42e8b38494b0ee11003592da11b5cbe43332190e", + "03161976385301ac9b965202e8f3922cef840790", + "7d598e5726fb58501d8cc65faf6b676bab7cb4bc", + "54ddde105d3f2c6ea7a5e7641ff24522eea2e784", + "3a75532b5916c772c1b7a18627bf170cf915aeb3", + "fa2b38321419e63cb890f8a8b5c53a1c4728a10a", + "a3ba598bee9da287092f4f2f3864322af38e1824", + "a94df01f21d870a006748b6ab3c04d31428c959d", + "396aabc66c603617f376409053d1e2cec3813101", + "a7b4becc2304da63792eb6c33f95677b2e7c9f8c", + "58b1623af15a9828ccf41b8cee47d123c5cfe8b6", + "3cb7eeac7be3a0195a9243537d452f790ccf1ca9", + "e0296cfc4726d91a1f7f041e24638a1276a08bed", + "aeb03edad3edc7c54a3c5f7916ecba981e65ce91", + "4a81a4596b7c4b8706fd8b5c88ddfde18ca72293", + "0d4e9ae7c486e5a0361bd4e3b918b6bdca89cfcb", + "81d394b44403315f9845c3da6f018b8daedd89ef", + "345630675ff0f319c8f2bb355edf59f9bd93072f", + "b61fbd992a13af05feba939f597b5f6ee61188e3", + "5ea45447e2e79a5f3b3d8c2f68aebdabf71c42f9", + "84325dd6fbd9a93f4ab61d091a9562a6c6111df4", + "e7c796aeecd47cfd01a2d62fd3fb1d41aafa2464", + "897457b33c4eb1ffcab08331877108cbf3fac6de", + "833843b1f33e720c17bccfb75647a49040861b4c", + "06b49c253d3fc9800cfd75605d26426f8ccb89af", + "5024212c42bed9f45e48c450147fecb3e934fc4e", + "5a9de8041b045a7a4f85b71a6dc6a794a7fcd4ea", + "70cad33774ddacb51ed1918adedeb67ff13a3b1e", + "840d201e3c213c01b4ab85983efaac44f0671552", + "aa7ffc7999a1b1bb79ce19b61c37f70331f492d6", + "e2ec0c07e15411564292b5fa75246e4c385f4411", + "38c1a0d14f548d4d81655920ec564b08e9fcf5e6", + "1d128b8343569c7e9a8985879fafd325d458d31c", + "4fcd30cbe02b74cece57babac93aded26ecdc893", + "57d8a6d782ee1df62ceebd5d10884805ed382336", + "54443ed3476d1d542f37bf069973bbd2b64c1b27", + "0e7ba6c5e4c29cf4fff25733892b63cf2a6efdfc", + "18824378226a6d33bcdbe39dd3bc9ee656ce20a2", + "93b0cb01befc90b65a0026acf85bea2fefec7d44", + "d65e378a1ec70cc79ae5b4469ae7f0e8939033fe", + "9230a2f8ac81e73f16c63dd60adb030328fbc983", + "302de797c9d73275ea184d7f6a8bf77364a8fd52", + "cea92f6e6612ef408d8c22ad5c1ed602bb2aedbf", + "353f2ff278f4ee038e7b217276a82d6ed0617130", + "e962e3a1946afa0d3ee97f3a0418cb3489a5f84c", + "a4e42b6cf98e957684aa4e7006940d31bcb76b1f", + "57af8f960b2450ffa0dc5bc7314fece53996d4d0", + "28e73f73084bc8e91fe9ec0a5581b583ef468d8c", + "9481589ddec9a6d9ad2cee7f73e8319aab3f1e95", + "edec09cc7476cd019560874def4af852bfeaffe3", + "6c3ae2cf5f9452d5176788e15635c5958581c931", + "f547b9717e84036c3d5eefec6d6ee3bfa5af89cb", + "87b51f4bf1ccd41cda3aa85c71da5de56aeeda33", + "e743092a576b92c8c05e04d5d2b23f2838825fd1", + "e713b84894b761e2a4e20fd0e5a81ae48a6b6f9d", + "6b1abca34099d2436bac8ab25aa17a57cbfe1564", + "93cb2977e536a680c043b158345254c14b946d52", + "8c2754fa9e93cbf1cccfd9241ebe0cc141199cfe", + "13e4abf95a8a9e6525419b4db7b1704ed0a2789d", + "8d53d453d7cfbb9bc386e128fa68aca388a5ddc6", + "caebf39e9c9b48d87277f2a13faa5931a24819a4", + "5025ca6cda98f31bc3ef321dd9a015b7f06b8bfa", + "531fbf18fdf3e513091614f20d65e920a505ca41", + "2f81e6159f7de0bc90c8a1db661b33bffbee85fd", + "85d4d9954f3a28228a2786b320ad58a46a13f37b", + ]; + + let nodes: Vec = ids + .iter() + .enumerate() + .map(|(i, str)| { + let id = Id::from_str(str).unwrap(); + Node(Arc::new(NodeInner { + id, + address: SocketAddrV4::new((i as u32).into(), i as u16), + token: None, + last_seen: Instant::now(), + })) + }) + .collect(); + + let local_id = Id::from_str("ba3042eb2d373b19e7c411ce6826e31b37be0b2e").unwrap(); + + let mut table = RoutingTable::new(local_id); + + for node in nodes { + table.add(node); + } + + { + let expected_closest_ids: Vec<_> = [ + "897457b33c4eb1ffcab08331877108cbf3fac6de", + "907fdf0aa137200b395bc210763ed947b03dfc2e", + "9230a2f8ac81e73f16c63dd60adb030328fbc983", + "93b0cb01befc90b65a0026acf85bea2fefec7d44", + "93cb2977e536a680c043b158345254c14b946d52", + "9465e80d80f707b222c4ae6ee81c02b62f607629", + "9481589ddec9a6d9ad2cee7f73e8319aab3f1e95", + "94d2037bbc534a5f1d672ce3e3350576c2b78ed1", + "98805a55523458c56d59339266bdcecc82370ecd", + "99719dfc220b145e2aac71d6b3e276731d85be1c", + "9e4923966754c02b036698e95f95cec8fc40a9d2", + "a3ba598bee9da287092f4f2f3864322af38e1824", + "a4e42b6cf98e957684aa4e7006940d31bcb76b1f", + "a7b4becc2304da63792eb6c33f95677b2e7c9f8c", + "a94df01f21d870a006748b6ab3c04d31428c959d", + "aa7ffc7999a1b1bb79ce19b61c37f70331f492d6", + "aeb03edad3edc7c54a3c5f7916ecba981e65ce91", + "b0bce9873042aee29cbc7ec395647f6cc7a482f8", + "b48d0aeb94cd3766f23d2ac098bbccf01485dc20", + "b61fbd992a13af05feba939f597b5f6ee61188e3", + ] + .iter() + .map(|id| Id::from_str(id).unwrap()) + .collect(); + + let target = local_id; + let closest = table.closest(target); + + let mut closest_ids: Vec = closest.iter().map(|n| *n.id()).collect(); + closest_ids.sort(); + + assert_eq!(closest_ids, expected_closest_ids); + } + + { + let expected_closest_ids: Vec<_> = [ + "c03d9008add37f8414cb41549448bb2dcb5c6c9b", + "c9a8163fa3e85065d46567bfac39b5452cfb3ae8", + "cdc7f4d5825dc316de20d998bc0f1c5e91e36a5e", + "cea92f6e6612ef408d8c22ad5c1ed602bb2aedbf", + "d65e378a1ec70cc79ae5b4469ae7f0e8939033fe", + "d9b50c6ca730c89f8fc9f518136cef6139dd2252", + "dbed34a2c8db568fe59c10adcca9e81825b3dcfd", + "dff82b028a6ec033e00b387df8e386417b92a47c", + "e0296cfc4726d91a1f7f041e24638a1276a08bed", + "e2ec0c07e15411564292b5fa75246e4c385f4411", + "e63b72f95aacee40ad087f83afb475645739f669", + "e6b8d5567bc05d9b68f23d562645bc030729abc9", + "e7c796aeecd47cfd01a2d62fd3fb1d41aafa2464", + "e962e3a1946afa0d3ee97f3a0418cb3489a5f84c", + "edec09cc7476cd019560874def4af852bfeaffe3", + "ef79f77e9eed9ad51094ce2747e2c4fdc3a81326", + "fa2b38321419e63cb890f8a8b5c53a1c4728a10a", + "fb449c17f6c34fadea26a5a83e1952e815e001ea", + "fb689ce0e18c2c22f316976d3ae524aed4137773", + "fd042ff1404b495720ad8345404ff5f25acd02a8", + ] + .iter() + .map(|str| Id::from_str(str).unwrap()) + .collect(); + + let target = Id::from_str("d1406a3d3a8354d566f21dba8bd06c537cde2a20").unwrap(); + let closest = table.closest(target); + + let mut closest_ids: Vec = closest.iter().map(|n| *n.id()).collect(); + closest_ids.sort(); + + assert_eq!(closest_ids, expected_closest_ids); + } + } +} diff --git a/vendor/mainline/src/dht.rs b/vendor/mainline/src/dht.rs new file mode 100644 index 0000000..eba18ed --- /dev/null +++ b/vendor/mainline/src/dht.rs @@ -0,0 +1,1363 @@ +//! Dht node. + +use std::{ + collections::HashMap, + net::{Ipv4Addr, SocketAddrV4, ToSocketAddrs}, + thread, + time::Duration, +}; + +use flume::{Receiver, Sender, TryRecvError}; + +use tracing::info; + +use crate::{ + common::{ + hash_immutable, most_recent_mutable_item, AnnouncePeerRequestArguments, + FindNodeRequestArguments, GetPeersRequestArguments, GetValueRequestArguments, Id, + MutableItem, PutImmutableRequestArguments, PutMutableRequestArguments, PutRequestSpecific, + }, + rpc::{ + to_socket_address, ConcurrencyError, GetMutableOutcome, GetRequestSpecific, Info, PutError, + PutOutcome, PutQueryError, Response, Rpc, + }, + Node, ServerSettings, +}; + +use crate::rpc::config::Config; + +#[derive(Debug, Clone)] +/// Mainline Dht node. +pub struct Dht(pub(crate) Sender); + +#[derive(Debug, Default, Clone)] +/// A builder for the [Dht] node. +pub struct DhtBuilder(Config); + +impl DhtBuilder { + /// Set this node's server_mode. + pub fn server_mode(&mut self) -> &mut Self { + self.0.server_mode = true; + + self + } + + /// Set a custom settings for the node to use at server mode. + /// + /// Defaults to [ServerSettings::default] + pub fn server_settings(&mut self, server_settings: ServerSettings) -> &mut Self { + self.0.server_settings = server_settings; + + self + } + + /// Set bootstrapping nodes. + pub fn bootstrap(&mut self, bootstrap: &[T]) -> &mut Self { + self.0.bootstrap = Some(to_socket_address(bootstrap)); + + self + } + + /// Add more bootstrap nodes to default bootstrapping nodes. + /// + /// Useful when you want to augment the default bootstrapping nodes with + /// dynamic list of nodes you have seen in previous sessions. + pub fn extra_bootstrap(&mut self, extra_bootstrap: &[T]) -> &mut Self { + let mut bootstrap = self.0.bootstrap.clone().unwrap_or_default(); + for address in to_socket_address(extra_bootstrap) { + bootstrap.push(address); + } + self.0.bootstrap = Some(bootstrap); + + self + } + + /// Remove the existing bootstrapping nodes, usually to create the first node in a new network. + pub fn no_bootstrap(&mut self) -> &mut Self { + self.0.bootstrap = Some(vec![]); + + self + } + + /// Set an explicit port to listen on. + pub fn port(&mut self, port: u16) -> &mut Self { + self.0.port = Some(port); + + self + } + + /// A known public IPv4 address for this node to generate + /// a secure node Id from according to [BEP_0042](https://www.bittorrent.org/beps/bep_0042.html) + /// + /// Defaults to depending on suggestions from responding nodes. + pub fn public_ip(&mut self, public_ip: Ipv4Addr) -> &mut Self { + self.0.public_ip = Some(public_ip); + + self + } + + /// UDP socket request timeout duration. + /// + /// The longer this duration is, the longer queries take until they are deemeed "done". + /// The shortet this duration is, the more responses from busy nodes we miss out on, + /// which affects the accuracy of queries trying to find closest nodes to a target. + /// + /// Defaults to [crate::DEFAULT_REQUEST_TIMEOUT] + pub fn request_timeout(&mut self, request_timeout: Duration) -> &mut Self { + self.0.request_timeout = request_timeout; + + self + } + + /// Set the address to bind to. + /// + /// Defaults to 0.0.0.0 (all interfaces). + pub fn bind_address(&mut self, bind_address: Ipv4Addr) -> &mut Self { + self.0.bind_address = Some(bind_address); + + self + } + + /// Create a Dht node. + pub fn build(&self) -> Result { + Dht::new(self.0.clone()) + } +} + +impl Dht { + /// Create a new Dht node. + /// + /// Could return an error if it failed to bind to the specified + /// port or other io errors while binding the udp socket. + pub fn new(config: Config) -> Result { + let (sender, receiver) = flume::unbounded(); + + thread::Builder::new() + .name("Mainline Dht actor thread".to_string()) + .spawn(move || run(config, receiver))?; + + let (tx, rx) = flume::bounded(1); + + sender + .send(ActorMessage::Check(tx)) + .expect("actor thread unexpectedly shutdown"); + + rx.recv().expect("actor thread unexpectedly shutdown")?; + + Ok(Dht(sender)) + } + + /// Returns a builder to edit settings before creating a Dht node. + pub fn builder() -> DhtBuilder { + DhtBuilder::default() + } + + /// Create a new DHT client with default bootstrap nodes. + pub fn client() -> Result { + Dht::builder().build() + } + + /// Create a new DHT node that is running in [Server mode][DhtBuilder::server_mode] as + /// soon as possible. + /// + /// You shouldn't use this option unless you are sure your + /// DHT node is publicly accessible (not firewalled) _AND_ will be long running, + /// and/or you are running your own local network for testing. + /// + /// If you are not sure, use [Self::client] and it will switch + /// to server mode when/if these two conditions are met. + pub fn server() -> Result { + Dht::builder().server_mode().build() + } + + // === Getters === + + /// Information and statistics about this [Dht] node. + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn info(&self) -> Info { + let (tx, rx) = flume::bounded::(1); + self.send(ActorMessage::Info(tx)); + + rx.recv().expect("actor thread unexpectedly shutdown") + } + + /// Turn this node's routing table to a list of bootstrapping nodes. + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn to_bootstrap(&self) -> Vec { + let (tx, rx) = flume::bounded::>(1); + self.send(ActorMessage::ToBootstrap(tx)); + + rx.recv().expect("actor thread unexpectedly shutdown") + } + + // === Public Methods === + + /// Block until the bootstrapping query is done. + /// + /// Returns true if the bootstrapping was successful. + #[allow(deprecated)] + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn bootstrapped(&self) -> bool { + let info = self.info(); + let nodes = self.find_node(*info.id()); + + !nodes.is_empty() + } + + // === Find nodes === + + /// Returns the closest 20 [secure](Node::is_secure) nodes to a target [Id]. + /// + /// Mostly useful to crawl the DHT. + /// + /// The returned nodes are claims by other nodes, they may be lies, or may have churned + /// since they were last seen, but haven't been pinged yet. + /// + /// You might need to ping them to confirm they exist, and responsive, or if you want to + /// learn more about them like the client they are using, or if they support a given BEP. + /// + /// If you are trying to find the closest nodes to a target with intent to [Self::put], + /// a request directly to these nodes (using `extra_nodes` parameter), then you should + /// use [Self::get_closest_nodes] instead. + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn find_node(&self, target: Id) -> Box<[Node]> { + let (tx, rx) = flume::bounded::>(1); + self.send(ActorMessage::Get( + GetRequestSpecific::FindNode(FindNodeRequestArguments { target }), + ResponseSender::ClosestNodes(tx), + )); + + rx.recv() + .expect("Query was dropped before sending a response, please open an issue.") + } + + // === Peers === + + /// Get peers for a given infohash. + /// + /// Note: each node of the network will only return a _random_ subset (usually 20) + /// of the total peers it has for a given infohash, so if you are getting responses + /// from 20 nodes, you can expect up to 400 peers in total, but if there are more + /// announced peers on that infohash, you are likely to miss some, the logic here + /// for Bittorrent is that any peer will introduce you to more peers through "peer exchange" + /// so if you are implementing something different from Bittorrent, you might want + /// to implement your own logic for gossipping more peers after you discover the first ones. + #[allow(deprecated)] + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn get_peers(&self, info_hash: Id) -> GetIterator> { + let (tx, rx) = flume::unbounded::>(); + self.send(ActorMessage::Get( + GetRequestSpecific::GetPeers(GetPeersRequestArguments { info_hash }), + ResponseSender::Peers(tx), + )); + + GetIterator(rx.into_iter()) + } + + /// Announce a peer for a given infohash. + /// + /// The peer will be announced on this process IP. + /// If explicit port is passed, it will be used, otherwise the port will be implicitly + /// assumed by remote nodes to be the same ase port they received the request from. + #[allow(deprecated)] + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn announce_peer(&self, info_hash: Id, port: Option) -> Result { + let (port, implied_port) = match port { + Some(port) => (port, None), + None => (0, Some(true)), + }; + + self.put( + PutRequestSpecific::AnnouncePeer(AnnouncePeerRequestArguments { + info_hash, + port, + implied_port, + }), + None, + ) + .map_err(|error| match error { + PutError::Query(error) => error, + PutError::Concurrency(_) => { + unreachable!("should not receive a concurrency error from announce peer query") + } + }) + } + + // === Immutable data === + + /// Get an Immutable data by its sha1 hash. + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn get_immutable(&self, target: Id) -> Option> { + let (tx, rx) = flume::unbounded::>(); + self.send(ActorMessage::Get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + seq: None, + salt: None, + }), + ResponseSender::Immutable(tx), + )); + + rx.recv().map(Some).unwrap_or(None) + } + + /// Put an immutable data to the DHT. + #[allow(deprecated)] + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn put_immutable(&self, value: &[u8]) -> Result { + let target: Id = hash_immutable(value).into(); + + self.put( + PutRequestSpecific::PutImmutable(PutImmutableRequestArguments { + target, + v: value.into(), + }), + None, + ) + .map_err(|error| match error { + PutError::Query(error) => error, + PutError::Concurrency(_) => { + unreachable!("should not receive a concurrency error from put immutable query") + } + }) + } + + // === Mutable data === + + /// Get a mutable data by its `public_key` and optional `salt`. + /// + /// You can ask for items `more_recent_than` than a certain `seq`, + /// usually one that you already have seen before, similar to `If-Modified-Since` header in HTTP. + /// + /// # Order + /// + /// The order of [MutableItem]s returned by this iterator is not guaranteed to + /// reflect their `seq` value. You should not assume that the later items are + /// more recent than earlier ones. + /// + /// Consider using [Self::get_mutable_most_recent] if that is what you need. + #[allow(deprecated)] + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn get_mutable( + &self, + public_key: &[u8; 32], + salt: Option<&[u8]>, + more_recent_than: Option, + ) -> GetIterator { + let salt = salt.map(|s| s.into()); + let target = MutableItem::target_from_key(public_key, salt.as_deref()); + let (tx, rx) = flume::unbounded::(); + self.send(ActorMessage::Get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + seq: more_recent_than, + salt, + }), + ResponseSender::Mutable(tx), + )); + + GetIterator(rx.into_iter()) + } + + /// Get the most recent [MutableItem] from the network. + #[allow(deprecated)] + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn get_mutable_most_recent( + &self, + public_key: &[u8; 32], + salt: Option<&[u8]>, + ) -> Option { + self.get_mutable(public_key, salt, None) + .fold(None::, most_recent_mutable_item) + } + + /// Put a mutable data to the DHT. + /// + /// # Lost Update Problem + /// + /// As mainline DHT is a distributed system, it is vulnerable to [Write–write conflict](https://en.wikipedia.org/wiki/Write-write_conflict). + /// + /// ## Read first + /// + /// To mitigate the risk of lost updates, you should call the [Self::get_mutable_most_recent] method + /// then start authoring the new [MutableItem] based on the most recent as in the following example: + /// + ///```rust + /// use mainline::{Dht, MutableItem, SigningKey, Testnet}; + /// use std::net::Ipv4Addr; + /// + /// let testnet = Testnet::builder(3).build().unwrap(); + /// let dht = Dht::builder() + /// .bootstrap(&testnet.bootstrap) + /// .bind_address(Ipv4Addr::LOCALHOST) + /// .build() + /// .unwrap(); + /// + /// let signing_key = SigningKey::from_bytes(&[0; 32]); + /// let key = signing_key.verifying_key().to_bytes(); + /// let salt = Some(b"salt".as_ref()); + /// + /// let (item, cas) = if let Some(most_recent) = dht .get_mutable_most_recent(&key, salt) { + /// // 1. Optionally Create a new value to take the most recent's value in consideration. + /// let mut new_value = most_recent.value().to_vec(); + /// new_value.extend_from_slice(b" more data"); + /// + /// // 2. Increment the sequence number to be higher than the most recent's. + /// let most_recent_seq = most_recent.seq(); + /// let new_seq = most_recent_seq + 1; + /// + /// ( + /// MutableItem::new(signing_key, &new_value, new_seq, salt), + /// // 3. Use the most recent [MutableItem::seq] as a `CAS`. + /// Some(most_recent_seq) + /// ) + /// } else { + /// (MutableItem::new(signing_key, b"first value", 1, salt), None) + /// }; + /// + /// dht.put_mutable(item, cas).unwrap(); + /// ``` + /// + /// ## Errors + /// + /// In addition to the [PutQueryError] common with all PUT queries, PUT mutable item + /// query has other [Concurrency errors][ConcurrencyError], that try to detect write conflict + /// risks or obvious conflicts. + /// + /// If you are lucky to get one of these errors (which is not guaranteed), then you should + /// read the most recent item again, and repeat the steps in the previous example. + #[allow(deprecated)] + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn put_mutable(&self, item: MutableItem, cas: Option) -> Result { + let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, cas)); + + self.put(request, None).map_err(|error| match error { + PutError::Query(err) => PutMutableError::Query(err), + PutError::Concurrency(err) => PutMutableError::Concurrency(err), + }) + } + + // === Raw === + + /// Get closet nodes to a specific target, that support [BEP_0044](https://www.bittorrent.org/beps/bep_0044.html). + /// + /// Useful to [Self::put] a request to nodes further from the 20 closest nodes to the + /// [PutRequestSpecific::target]. Which itself is useful to circumvent [extreme vertical sybil attacks](https://github.com/pubky/mainline/blob/main/docs/censorship-resistance.md#extreme-vertical-sybil-attacks). + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn get_closest_nodes(&self, target: Id) -> Box<[Node]> { + let (tx, rx) = flume::unbounded::>(); + self.send(ActorMessage::Get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + salt: None, + seq: None, + }), + ResponseSender::ClosestNodes(tx), + )); + + rx.recv() + .expect("Query was dropped before sending a response, please open an issue.") + } + + /// Send a PUT request to the closest nodes, and optionally some extra nodes. + /// + /// This is useful to put data to regions of the DHT other than the closest nodes + /// to this request's [target][PutRequestSpecific::target]. + /// + /// You can find nodes close to other regions of the network by calling + /// [Self::get_closest_nodes] with the target that you want to find the closest nodes to. + /// + /// Note: extra nodes need to have [Node::valid_token]. + #[deprecated(note = "use the async API via Dht::as_async() instead")] + pub fn put( + &self, + request: PutRequestSpecific, + extra_nodes: Option>, + ) -> Result { + self.put_inner(request, extra_nodes) + .recv() + .expect("Query was dropped before sending a response, please open an issue.") + .map(|outcome| outcome.target) + } + + // === Private Methods === + + pub(crate) fn put_inner( + &self, + request: PutRequestSpecific, + extra_nodes: Option>, + ) -> flume::Receiver> { + let (tx, rx) = flume::bounded::>(1); + self.send(ActorMessage::Put(request, tx, extra_nodes)); + + rx + } + + pub(crate) fn send(&self, message: ActorMessage) { + self.0 + .send(message) + .expect("actor thrread unexpectedly shutdown"); + } +} + +#[deprecated(note = "use the async stream returned by AsyncDht instead")] +pub struct GetIterator(flume::IntoIter); + +#[allow(deprecated)] +impl Iterator for GetIterator { + type Item = T; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +fn run(config: Config, receiver: Receiver) { + match Rpc::new(config) { + Ok(mut rpc) => { + let address = rpc.local_addr(); + info!(?address, "Mainline DHT listening"); + + let mut put_senders = HashMap::new(); + let mut get_senders = HashMap::new(); + + loop { + match receiver.try_recv() { + Ok(actor_message) => match actor_message { + ActorMessage::Check(sender) => { + let _ = sender.send(Ok(())); + } + ActorMessage::Info(sender) => { + let _ = sender.send(rpc.info()); + } + ActorMessage::Put(request, sender, extra_nodes) => { + let target = *request.target(); + + match rpc.put(request, extra_nodes) { + Ok(()) => { + let senders = put_senders.entry(target).or_insert(vec![]); + + senders.push(sender); + } + Err(error) => { + let _ = sender.send(Err(error)); + } + }; + } + ActorMessage::Get(request, sender) => { + let target = *request.target(); + + if let Some(responses) = rpc.get(request, None) { + for response in responses { + send(&sender, response); + } + }; + + let senders = get_senders.entry(target).or_insert(vec![]); + + senders.push(sender); + } + ActorMessage::ToBootstrap(sender) => { + let _ = sender.send(rpc.routing_table().to_bootstrap()); + } + ActorMessage::SeedRouting(nodes, sender) => { + for node in nodes { + rpc.routing_table_mut().add(node); + } + let _ = sender.send(()); + } + }, + Err(TryRecvError::Disconnected) => { + // Node was dropped, kill this thread. + tracing::debug!("mainline::Dht's actor thread was shutdown after Drop."); + break; + } + Err(TryRecvError::Empty) => { + // No op + } + } + + let report = rpc.tick(); + + // Response for an ongoing GET query + if let Some((target, response)) = report.new_query_response { + if let Some(senders) = get_senders.get(&target) { + for sender in senders { + send(sender, response.clone()); + } + } + } + + // Cleanup done GET queries + for done in report.done_get_queries { + if let Some(senders) = get_senders.remove(&done.id) { + for sender in senders { + match sender { + ResponseSender::ClosestNodes(sender) => { + let _ = sender.send(done.closest_nodes.clone()); + } + ResponseSender::MutableDetailed { outcome, .. } => { + let _ = outcome + .send(done.mutable_outcome.clone().unwrap_or_default()); + } + ResponseSender::Peers(_) + | ResponseSender::Mutable(_) + | ResponseSender::Immutable(_) => {} + } + } + } + } + + // Cleanup done PUT query and send its final result. + for (id, result) in report.done_put_queries { + if let Some(senders) = put_senders.remove(&id) { + for sender in senders { + let _ = sender.send(result.clone()); + } + } + } + } + } + Err(err) => { + if let Ok(ActorMessage::Check(sender)) = receiver.try_recv() { + let _ = sender.send(Err(err)); + } + } + }; +} + +fn send(sender: &ResponseSender, response: Response) { + match (sender, response) { + (ResponseSender::Peers(s), Response::Peers(r)) => { + let _ = s.send(r); + } + (ResponseSender::Mutable(s), Response::Mutable(r)) => { + let _ = s.send(r); + } + (ResponseSender::MutableDetailed { values, .. }, Response::Mutable(r)) => { + let _ = values.send(r); + } + (ResponseSender::Immutable(s), Response::Immutable(r)) => { + let _ = s.send(r); + } + _ => {} + } +} + +#[derive(Debug)] +pub(crate) enum ActorMessage { + Info(Sender), + Put( + PutRequestSpecific, + Sender>, + Option>, + ), + Get(GetRequestSpecific, ResponseSender), + Check(Sender>), + ToBootstrap(Sender>), + SeedRouting(Vec, Sender<()>), +} + +#[derive(Debug, Clone)] +pub enum ResponseSender { + ClosestNodes(Sender>), + Peers(Sender>), + Mutable(Sender), + MutableDetailed { + values: Sender, + outcome: Sender, + }, + Immutable(Sender>), +} + +/// Builder for creating a [Testnet] with custom configuration. +/// +/// # Defaults +/// +/// - `bind_address`: `127.0.0.1` (localhost) +/// - `seeded`: `true` - nodes start with fully populated routing tables +/// +/// # Example +/// +/// ```ignore +/// use std::net::Ipv4Addr; +/// use mainline::Testnet; +/// +/// // Use localhost (default) +/// let testnet = Testnet::builder(3).build().unwrap(); +/// +/// // Use all interfaces (0.0.0.0) +/// let testnet = Testnet::builder(3) +/// .bind_address(Ipv4Addr::UNSPECIFIED) +/// .build() +/// .unwrap(); +/// ``` +#[derive(Debug, Clone)] +pub struct TestnetBuilder { + count: usize, + bind_address: Ipv4Addr, + seeded: bool, +} + +impl TestnetBuilder { + /// Create a new builder with the specified number of nodes. + /// + /// # Defaults + /// + /// - `bind_address`: `127.0.0.1` (localhost) + /// - `seeded`: `true` + pub fn new(count: usize) -> Self { + Self { + count, + bind_address: Ipv4Addr::LOCALHOST, + seeded: true, + } + } + + /// Set the address to bind all nodes to. + /// + /// Defaults to `127.0.0.1` (localhost). + /// Use `Ipv4Addr::UNSPECIFIED` (`0.0.0.0`) to bind to all interfaces. + pub fn bind_address(&mut self, bind_address: Ipv4Addr) -> &mut Self { + self.bind_address = bind_address; + self + } + + /// Whether to pre-seed routing tables with all nodes. + /// + /// Defaults to `true`. + /// + /// When `true`, all nodes start with fully populated routing tables. + /// When `false`, nodes bootstrap from each other which is faster at startup + /// but may not have immediate full connectivity. + pub fn seeded(&mut self, seeded: bool) -> &mut Self { + self.seeded = seeded; + self + } + + /// Build the testnet. + /// + /// Nodes will be bound to the configured `bind_address` (default: `127.0.0.1`). + /// + /// This will block until all nodes are created (and seeded if `seeded` is true). + pub fn build(&self) -> Result { + if self.seeded { + Testnet::build_seeded(self.count, self.bind_address) + } else { + Testnet::build_unseeded(self.count, self.bind_address) + } + } +} + +/// Create a testnet of Dht nodes to run tests against instead of the real mainline network. +/// +/// # Bind Address +/// +/// The convenience methods ([`Self::new`], [`Self::new_unseeded`], etc.) bind to `0.0.0.0` +/// for backwards compatibility. Use [`Self::builder`] to bind to a different address. +// TODO(breaking): In the next major version, change the default bind address from +// `0.0.0.0` to `127.0.0.1` for better cross-platform compatibility (especially macOS). +#[derive(Debug)] +pub struct Testnet { + /// bootstrapping nodes for this testnet. + pub bootstrap: Vec, + /// all nodes in this testnet + pub nodes: Vec, +} + +// TODO(breaking): In the next major version, change `new()` and related methods to bind +// to `127.0.0.1` instead of `0.0.0.0` for better macOS compatibility. The builder already +// defaults to `127.0.0.1`. + +impl Testnet { + /// Returns a builder to configure and create a [Testnet]. + /// + /// The builder defaults to binding to `127.0.0.1` (localhost). + /// + /// # Example + /// + /// ```ignore + /// let testnet = Testnet::builder(3).build().unwrap(); + /// ``` + pub fn builder(count: usize) -> TestnetBuilder { + TestnetBuilder::new(count) + } + + /// Create a new testnet with a certain size. + /// + /// Note: this network will be shutdown as soon as this struct + /// gets dropped, if you want the network to be `'static`, then + /// you should call [Self::leak]. + /// + /// This will block until all nodes are seeded with local peers. + /// If you are using an async runtime, consider using [Self::new_async]. + /// + /// # Bind Address + /// + /// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] to bind to + /// a different address. + pub fn new(count: usize) -> Result { + Testnet::build_seeded(count, Ipv4Addr::UNSPECIFIED) + } + + /// Create a new testnet without pre-seeding routing tables. + /// + /// This is faster at startup, but nodes will not start with fully populated routing tables. + /// Use this when your tests do not require immediate full connectivity. + /// + /// # Bind Address + /// + /// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] with `.seeded(false)` + /// to bind to a different address. + pub fn new_unseeded(count: usize) -> Result { + Testnet::build_unseeded(count, Ipv4Addr::UNSPECIFIED) + } + + #[cfg(feature = "async")] + /// Similar to [Self::new], but available for async contexts. + /// + /// # Bind Address + /// + /// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] to bind to + /// a different address. + pub async fn new_async(count: usize) -> Result { + Testnet::build_seeded(count, Ipv4Addr::UNSPECIFIED) + } + + #[cfg(feature = "async")] + /// Similar to [Self::new_unseeded], but available for async contexts. + /// + /// # Bind Address + /// + /// Nodes are bound to `0.0.0.0` (all interfaces). Use [`Self::builder`] with `.seeded(false)` + /// to bind to a different address. + pub async fn new_unseeded_async(count: usize) -> Result { + Testnet::build_unseeded(count, Ipv4Addr::UNSPECIFIED) + } + + #[allow(deprecated)] + fn build_seeded(count: usize, bind_address: Ipv4Addr) -> Result { + let mut nodes = Vec::with_capacity(count); + + for _ in 0..count { + let node = Dht::builder() + .server_mode() + .no_bootstrap() + .bind_address(bind_address) + .build()?; + nodes.push(node); + } + + let infos: Vec<_> = nodes.iter().map(|node| node.info()).collect(); + let bootstrap = infos + .iter() + .map(|info| info.local_addr().to_string()) + .collect::>(); + let seeded_nodes: Vec<_> = infos + .iter() + .map(|info| Node::new(*info.id(), info.local_addr())) + .collect(); + + for (node, info) in nodes.iter().zip(infos.iter()) { + let peers = seeded_nodes + .iter() + .filter(|peer| peer.id() != info.id()) + .cloned() + .collect::>(); + let (tx, rx) = flume::bounded(1); + node.send(ActorMessage::SeedRouting(peers, tx)); + let _ = rx.recv(); + } + + Ok(Self { bootstrap, nodes }) + } + + #[allow(deprecated)] + fn build_unseeded(count: usize, bind_address: Ipv4Addr) -> Result { + let mut nodes = Vec::with_capacity(count); + let mut bootstrap = Vec::new(); + + for i in 0..count { + if i == 0 { + let node = Dht::builder() + .server_mode() + .no_bootstrap() + .bind_address(bind_address) + .build()?; + + let info = node.info(); + + bootstrap.push(info.local_addr().to_string()); + + nodes.push(node); + } else { + let node = Dht::builder() + .server_mode() + .bootstrap(&bootstrap) + .bind_address(bind_address) + .build()?; + nodes.push(node); + } + } + + Ok(Self { bootstrap, nodes }) + } + + /// By default as soon as this testnet gets dropped, + /// all the nodes get dropped and the entire network is shutdown. + /// + /// This method uses [Box::leak] to keep nodes running, which is + /// useful if you need to keep running the testnet in the process + /// even if this struct gets dropped. + pub fn leak(&self) { + for node in self.nodes.clone() { + Box::leak(Box::new(node)); + } + } +} + +#[derive(thiserror::Error, Debug)] +/// Put MutableItem errors. +pub enum PutMutableError { + #[error(transparent)] + /// Common PutQuery errors + Query(#[from] PutQueryError), + + #[error(transparent)] + /// PutQuery for [crate::MutableItem] errors + Concurrency(#[from] ConcurrencyError), +} + +#[cfg(test)] +mod test { + #![allow(deprecated)] + + use std::net::Ipv4Addr; + use std::str::FromStr; + + use ed25519_dalek::SigningKey; + + use crate::rpc::ConcurrencyError; + + use super::*; + + #[test] + fn bind_twice() { + let a = Dht::client().unwrap(); + let result = Dht::builder() + .port(a.info().local_addr().port()) + .server_mode() + .build(); + + assert!(result.is_err()); + } + + #[test] + fn announce_get_peer() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let info_hash = Id::random(); + + a.announce_peer(info_hash, Some(45555)) + .expect("failed to announce"); + + let peers = b.get_peers(info_hash).next().expect("No peers"); + + assert_eq!(peers.first().unwrap().port(), 45555); + } + + #[test] + fn put_get_immutable() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let value = b"Hello World!"; + let expected_target = Id::from_str("e5f96f6f38320f0f33959cb4d3d656452117aadb").unwrap(); + + let target = a.put_immutable(value).unwrap(); + assert_eq!(target, expected_target); + + let response = b.get_immutable(target).unwrap(); + + assert_eq!(response, value.to_vec().into_boxed_slice()); + } + + #[test] + fn find_node_no_values() { + let client = Dht::builder().no_bootstrap().build().unwrap(); + + client.find_node(Id::random()); + } + + #[test] + fn put_get_immutable_no_values() { + let client = Dht::builder().no_bootstrap().build().unwrap(); + + assert_eq!(client.get_immutable(Id::random()), None); + } + + #[test] + fn put_get_mutable() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + + a.put_mutable(item.clone(), None).unwrap(); + + let response = b + .get_mutable(signer.verifying_key().as_bytes(), None, None) + .next() + .expect("No mutable values"); + + assert_eq!(&response, &item); + } + + #[test] + fn put_get_mutable_no_more_recent_value() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + + a.put_mutable(item.clone(), None).unwrap(); + + let response = b + .get_mutable(signer.verifying_key().as_bytes(), None, Some(seq)) + .next(); + + assert!(&response.is_none()); + } + + #[test] + fn get_mutable_most_recent_prefers_highest_seq() { + let testnet = Testnet::builder(10).build().unwrap(); + + let client = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let newer = MutableItem::new(signer.clone(), b"newer", 1001, None); + client.put_mutable(newer.clone(), None).unwrap(); + + let older = MutableItem::new(signer, b"older", 1000, None); + let (sender, _) = flume::bounded::>(1); + let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(older, None)); + client + .0 + .send(ActorMessage::Put(request, sender, None)) + .unwrap(); + + let most_recent = client + .get_mutable_most_recent(newer.key(), None) + .expect("No mutable values"); + + assert_eq!(most_recent.seq(), newer.seq()); + assert_eq!(most_recent.value(), newer.value()); + } + + #[test] + fn repeated_put_query() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let id = a.put_immutable(&[1, 2, 3]).unwrap(); + + assert_eq!(a.put_immutable(&[1, 2, 3]).unwrap(), id); + } + + #[test] + fn concurrent_get_mutable() { + let testnet = Testnet::builder(10).build().unwrap(); + + let a = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + let b = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let key = signer.verifying_key().to_bytes(); + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + + a.put_mutable(item.clone(), None).unwrap(); + + let _response_first = b + .get_mutable(&key, None, None) + .next() + .expect("No mutable values"); + + let response_second = b + .get_mutable(&key, None, None) + .next() + .expect("No mutable values"); + + assert_eq!(&response_second, &item); + } + + #[test] + fn concurrent_put_mutable_same() { + let testnet = Testnet::builder(10).build().unwrap(); + + let client = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + let value = b"Hello World!"; + + let item = MutableItem::new(signer.clone(), value, seq, None); + + let mut handles = vec![]; + + for _ in 0..2 { + let client = client.clone(); + let item = item.clone(); + + let handle = std::thread::spawn(move || client.put_mutable(item, None).unwrap()); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + } + + #[test] + fn concurrent_put_mutable_different() { + let testnet = Testnet::builder(10).build().unwrap(); + + let client = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let mut handles = vec![]; + + for i in 0..2 { + let client = client.clone(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let seq = 1000; + + let mut value = b"Hello World!".to_vec(); + value.push(i); + + let item = MutableItem::new(signer.clone(), &value, seq, None); + + let handle = std::thread::spawn(move || { + let result = client.put_mutable(item, None); + if i == 0 { + assert!(result.is_ok()) + } else { + assert!(matches!( + result, + Err(PutMutableError::Concurrency(ConcurrencyError::ConflictRisk)) + )) + } + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + } + + #[test] + fn concurrent_put_mutable_different_with_cas() { + let testnet = Testnet::builder(10).build().unwrap(); + + let client = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + // First + { + let item = MutableItem::new(signer.clone(), &[], 1000, None); + + let (sender, _) = flume::bounded::>(1); + let request = + PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, None)); + client + .0 + .send(ActorMessage::Put(request, sender, None)) + .unwrap(); + } + + std::thread::sleep(Duration::from_millis(100)); + + // Second + { + let item = MutableItem::new(signer, &[], 1001, None); + + client.put_mutable(item, Some(1000)).unwrap(); + } + } + + #[test] + fn conflict_302_seq_less_than_current() { + let testnet = Testnet::builder(10).build().unwrap(); + + let client = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + client + .put_mutable(MutableItem::new(signer.clone(), &[], 1001, None), None) + .unwrap(); + + assert!(matches!( + client.put_mutable(MutableItem::new(signer, &[], 1000, None), None), + Err(PutMutableError::Concurrency( + ConcurrencyError::NotMostRecent + )) + )); + } + + #[test] + fn conflict_301_cas() { + let testnet = Testnet::builder(10).build().unwrap(); + + let client = Dht::builder() + .bootstrap(&testnet.bootstrap) + .bind_address(Ipv4Addr::LOCALHOST) + .build() + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + client + .put_mutable(MutableItem::new(signer.clone(), &[], 1001, None), None) + .unwrap(); + + assert!(matches!( + client.put_mutable(MutableItem::new(signer, &[], 1002, None), Some(1000)), + Err(PutMutableError::Concurrency(ConcurrencyError::CasFailed)) + )); + } + + #[test] + fn populate_bootstrapping_node_routing_table() { + let size = 3; + + let testnet = Testnet::builder(size).build().unwrap(); + + assert!(testnet + .nodes + .iter() + .all(|n| n.to_bootstrap().len() == size - 1)); + } +} diff --git a/vendor/mainline/src/lib.rs b/vendor/mainline/src/lib.rs new file mode 100644 index 0000000..302cf60 --- /dev/null +++ b/vendor/mainline/src/lib.rs @@ -0,0 +1,44 @@ +#![doc = include_str!("../README.md")] +//! ## Feature flags +#![doc = document_features::document_features!()] +//! + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] + +mod common; +#[cfg(feature = "node")] +mod dht; +mod rpc; + +// Public modules +#[cfg(feature = "async")] +pub mod async_dht; + +pub use common::{Id, MutableItem, Node, RoutingTable}; + +#[cfg(feature = "node")] +pub use dht::{Dht, DhtBuilder, Testnet, TestnetBuilder}; +#[cfg(feature = "node")] +pub use rpc::{ + config::Config, + messages::{MessageType, PutRequestSpecific, RequestSpecific}, + server::{RequestFilter, ServerSettings, MAX_INFO_HASHES, MAX_PEERS, MAX_VALUES}, + ClosestNodes, GetMutableOutcome, PutOutcome, DEFAULT_BOOTSTRAP_NODES, DEFAULT_REQUEST_TIMEOUT, +}; + +pub use ed25519_dalek::SigningKey; + +pub mod errors { + //! Exported errors + #[cfg(feature = "node")] + pub use super::common::ErrorSpecific; + #[cfg(feature = "node")] + pub use super::dht::PutMutableError; + #[cfg(feature = "node")] + pub use super::rpc::{ConcurrencyError, PutError, PutQueryError}; + + pub use super::common::DecodeIdError; + pub use super::common::MutableError; +} diff --git a/vendor/mainline/src/rpc.rs b/vendor/mainline/src/rpc.rs new file mode 100644 index 0000000..7ecd04b --- /dev/null +++ b/vendor/mainline/src/rpc.rs @@ -0,0 +1,1261 @@ +//! K-RPC implementation. + +mod closest_nodes; +pub(crate) mod config; +mod info; +mod iterative_query; +mod put_query; +pub(crate) mod server; +mod socket; + +use std::collections::HashMap; +use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs}; +use std::num::NonZeroUsize; +use std::time::{Duration, Instant}; + +use lru::LruCache; +use tracing::{debug, error, info}; + +use iterative_query::IterativeQuery; +use put_query::PutQuery; + +use crate::common::{ + validate_immutable, ErrorSpecific, FindNodeRequestArguments, GetImmutableResponseArguments, + GetMutableResponseArguments, GetPeersResponseArguments, GetValueRequestArguments, Id, Message, + MessageType, MutableItem, NoMoreRecentValueResponseArguments, NoValuesResponseArguments, Node, + PutRequestSpecific, RequestSpecific, RequestTypeSpecific, ResponseSpecific, RoutingTable, + MAX_BUCKET_SIZE_K, +}; +use server::Server; + +use self::messages::{GetPeersRequestArguments, PutMutableRequestArguments}; +use server::ServerSettings; +use socket::KrpcSocket; + +pub use crate::common::messages; +pub use closest_nodes::ClosestNodes; +pub use info::Info; +pub use iterative_query::{GetMutableOutcome, GetRequestSpecific}; +pub use put_query::{ConcurrencyError, PutError, PutOutcome, PutQueryError}; +pub use socket::DEFAULT_REQUEST_TIMEOUT; + +/// Default bootstrap nodes used to discover peers when no custom bootstrap list is configured. +pub const DEFAULT_BOOTSTRAP_NODES: [&str; 4] = [ + "router.bittorrent.com:6881", + "dht.transmissionbt.com:6881", + "dht.libtorrent.org:25401", + "relay.pkarr.org:6881", +]; + +const REFRESH_TABLE_INTERVAL: Duration = Duration::from_secs(15 * 60); +const PING_TABLE_INTERVAL: Duration = Duration::from_secs(5 * 60); + +/// Result of `tick_get_queries`: completed queries and whether self-findnode finished. +type GetQueriesResult = (Vec, bool); + +const MAX_CACHED_ITERATIVE_QUERIES: usize = 1000; + +#[derive(Debug)] +/// Internal Rpc called in the Dht thread loop, useful to create your own actor setup. +pub struct Rpc { + // Options + bootstrap: Box<[SocketAddrV4]>, + + socket: KrpcSocket, + + // Routing + /// Closest nodes to this node + routing_table: RoutingTable, + /// Last time we refreshed the routing table with a find_node query. + last_table_refresh: Instant, + /// Last time we pinged nodes in the routing table. + last_table_ping: Instant, + /// Closest responding nodes to specific target + /// + /// as well as the: + /// 1. dht size estimate based on closest claimed nodes, + /// 2. dht size estimate based on closest responding nodes. + /// 3. number of subnets with unique 6 bits prefix in ipv4 + cached_iterative_queries: LruCache, + + // Active IterativeQueries + iterative_queries: HashMap, + /// Put queries are special, since they have to wait for a corresponding + /// get query to finish, update the closest_nodes, then `query_all` these. + put_queries: HashMap, + + /// Sum of Dht size estimates from closest nodes from get queries. + dht_size_estimates_sum: f64, + + /// Sum of Dht size estimates from closest _responding_ nodes from get queries. + responders_based_dht_size_estimates_sum: f64, + responders_based_dht_size_estimates_count: usize, + + /// Sum of the number of subnets with 6 bits prefix in the closest nodes ipv4 + subnets_sum: usize, + + server: Server, + + public_address: Option, + firewalled: bool, +} + +impl Rpc { + /// Creates a new RPC instance and prepares the routing table and socket. + /// + /// This does not perform network IO by itself. Call [`Rpc::tick`] to bootstrap + /// and perform scheduled maintenance. + /// + /// Returns an instance ready to accept `get`/`put` requests and handle incoming + /// messages via [`Rpc::handle_message`] if you integrate it with your socket loop. + pub fn new(config: config::Config) -> Result { + let id = if let Some(ip) = config.public_ip { + Id::from_ip(ip.into()) + } else { + Id::random() + }; + + let socket = KrpcSocket::new(&config)?; + + Ok(Rpc { + bootstrap: config + .bootstrap + .unwrap_or_else(|| to_socket_address(&DEFAULT_BOOTSTRAP_NODES)) + .into(), + socket, + + routing_table: RoutingTable::new(id), + iterative_queries: HashMap::new(), + put_queries: HashMap::new(), + + cached_iterative_queries: LruCache::new( + NonZeroUsize::new(MAX_CACHED_ITERATIVE_QUERIES) + .expect("MAX_CACHED_BUCKETS is NonZeroUsize"), + ), + + last_table_refresh: Instant::now(), + last_table_ping: Instant::now(), + + dht_size_estimates_sum: 0.0, + responders_based_dht_size_estimates_count: 0, + + // Don't store to too many nodes just because you are in a cold start. + responders_based_dht_size_estimates_sum: 1_000_000.0, + subnets_sum: 20, + + server: Server::new(config.server_settings), + + public_address: None, + firewalled: true, + }) + } + + // === Getters === + + /// Returns the node's Id + pub fn id(&self) -> &Id { + self.routing_table.id() + } + + /// Returns the address the server is listening to. + #[inline] + pub fn local_addr(&self) -> SocketAddrV4 { + self.socket.local_addr() + } + + /// Returns the best guess for this node's Public address. + /// + /// If [crate::DhtBuilder::public_ip] was set, this is what will be returned + /// (plus the local port), otherwise it will rely on consensus from + /// responding nodes voting on our public IP and port. + pub fn public_address(&self) -> Option { + self.public_address + } + + /// Returns `true` if we can't confirm that [Self::public_address] is publicly addressable. + /// + /// If this node is firewalled, it won't switch to server mode if it is in adaptive mode, + /// but if [crate::DhtBuilder::server_mode] was set to true, then whether or not this node is firewalled + /// won't matter. + pub fn firewalled(&self) -> bool { + self.firewalled + } + + /// Returns whether or not this node is running in server mode. + pub fn server_mode(&self) -> bool { + self.socket.server_mode + } + + pub fn routing_table(&self) -> &RoutingTable { + &self.routing_table + } + + pub fn routing_table_mut(&mut self) -> &mut RoutingTable { + &mut self.routing_table + } + + /// Returns: + /// 1. Normal Dht size estimate based on all closer `nodes` in query responses. + /// 2. Standard deviaiton as a function of the number of samples used in this estimate. + /// + /// [Read more](https://github.com/pubky/mainline/blob/main/docs/dht_size_estimate.md) + pub fn dht_size_estimate(&self) -> (usize, f64) { + let normal = + self.dht_size_estimates_sum as usize / self.cached_iterative_queries.len().max(1); + + // See https://github.com/pubky/mainline/blob/main/docs/standard-deviation-vs-lookups.png + let std_dev = 0.281 * (self.cached_iterative_queries.len() as f64).powf(-0.529); + + (normal, std_dev) + } + + /// Returns a thread safe and lightweight summary of this node's + /// information and statistics. + pub fn info(&self) -> Info { + Info::from(self) + } + + // === Public Methods === + + /// Advances maintenance and in-flight queries by one step. + /// + /// - Performs routing-table refreshes and liveness checks on schedule. + /// - Progresses outstanding `get`/`put` queries and evicts completed ones. + /// - May emit newly-available query responses. + /// + /// Returns a [`RpcTickReport`] summarizing work done during this call. + /// + /// Call this periodically; typical intervals are tied to IO loop cadence + /// or a fixed timer. Missing calls will delay query completion and degrade + /// the routing table quality. + pub fn tick(&mut self) -> RpcTickReport { + let mut done_put_queries = self.tick_put_queries(); + + let (done_get_queries, finished_self_findnode) = self.tick_get_queries(); + + self.cleanup_done_queries(&done_get_queries, &mut done_put_queries); + + self.periodic_node_maintenance(); + + let new_query_response = self.handle_message(); + + if finished_self_findnode { + self.log_bootstrap(self.id()); + } + + RpcTickReport { + done_get_queries, + done_put_queries, + new_query_response, + } + } + + /// Send a request to the given address and return the transaction_id + pub fn request(&mut self, address: SocketAddrV4, request: RequestSpecific) -> u32 { + self.socket.request(address, request) + } + + /// Send a response to the given address. + pub fn response( + &mut self, + address: SocketAddrV4, + transaction_id: u32, + response: ResponseSpecific, + ) { + self.socket.response(address, transaction_id, response) + } + + /// Send an error to the given address. + pub fn error(&mut self, address: SocketAddrV4, transaction_id: u32, error: ErrorSpecific) { + self.socket.error(address, transaction_id, error) + } + + /// Store a value in the closest nodes, optionally trigger a lookup query if + /// the cached closest_nodes aren't fresh enough. + /// + /// - `request`: the put request. + pub fn put( + &mut self, + request: PutRequestSpecific, + extra_nodes: Option>, + ) -> Result<(), PutError> { + let target = *request.target(); + + if let PutRequestSpecific::PutMutable(PutMutableRequestArguments { + sig, cas, seq, .. + }) = &request + { + if let Some(PutRequestSpecific::PutMutable(inflight_request)) = self + .put_queries + .get(&target) + .map(|existing| &existing.request) + { + debug!(?inflight_request, ?request, "Possible conflict risk"); + + if *sig == inflight_request.sig { + // Noop, the inflight query is sufficient. + return Ok(()); + } else if *seq < inflight_request.seq { + return Err(PutError::Concurrency(ConcurrencyError::NotMostRecent)); + } else if let Some(cas) = cas { + if *cas == inflight_request.seq { + // The user is aware of the inflight query and whiches to overrides it. + // + // Remove the inflight request, and create a new one. + self.put_queries.remove(&target); + } else { + return Err(PutError::Concurrency(ConcurrencyError::CasFailed)); + } + } else { + return Err(PutError::Concurrency(ConcurrencyError::ConflictRisk)); + }; + }; + } + + let mut query = PutQuery::new(target, request.clone(), extra_nodes); + + if let Some(closest_nodes) = self + .cached_iterative_queries + .get(&target) + .map(|cached| cached.closest_responding_nodes.clone()) + .filter(|closest_nodes| { + !closest_nodes.is_empty() && closest_nodes.iter().any(Node::valid_token) + }) + { + query.start(&mut self.socket, &closest_nodes)? + } else { + let salt = match request { + PutRequestSpecific::PutMutable(args) => args.salt, + _ => None, + }; + + self.get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + seq: None, + salt, + }), + None, + ); + }; + + self.put_queries.insert(target, query); + + Ok(()) + } + + /// Send a message to closer and closer nodes until we can't find any more nodes. + /// + /// Queries take few seconds to fully traverse the network, once it is done, it will be removed from + /// self.iterative_queries. But until then, calling [Rpc::get] multiple times, will just return the list + /// of responses seen so far. + /// + /// Subsequent responses can be obtained from the [RpcTickReport::new_query_response] you get after calling [Rpc::tick]. + /// + /// Effectively, we are caching responses and backing off the network for the duration it takes + /// to traverse it. + /// + /// - `request` [RequestTypeSpecific], except [RequestTypeSpecific::Ping] and + /// [RequestTypeSpecific::Put] which will be ignored. + /// - `extra_nodes` option allows the query to visit specific nodes, that won't necessesarily be visited + /// through the query otherwise. + pub fn get( + &mut self, + request: GetRequestSpecific, + extra_nodes: Option<&[SocketAddrV4]>, + ) -> Option> { + let target = match request { + GetRequestSpecific::FindNode(FindNodeRequestArguments { target }) => target, + GetRequestSpecific::GetPeers(GetPeersRequestArguments { info_hash, .. }) => info_hash, + GetRequestSpecific::GetValue(GetValueRequestArguments { target, .. }) => target, + }; + + // If query is still active, no need to create a new one. + if let Some(query) = self.iterative_queries.get(&target) { + return Some(query.responses().to_vec()); + } + + let node_id = self.routing_table.id(); + + if target == *node_id { + debug!(?node_id, "Bootstrapping the routing table"); + } + + let mut query = IterativeQuery::new(*self.id(), target, request); + + // Seed the query either with the closest nodes from the routing table, or the + // bootstrapping nodes if the closest nodes are not enough. + + let routing_table_closest = self.routing_table.closest_secure( + target, + self.responders_based_dht_size_estimate(), + self.average_subnets(), + ); + + // If we don't have enough or any closest nodes, call the bootstrapping nodes. + if routing_table_closest.is_empty() || routing_table_closest.len() < self.bootstrap.len() { + for bootstrapping_node in self.bootstrap.clone() { + query.visit(&mut self.socket, bootstrapping_node); + } + } + + if let Some(extra_nodes) = extra_nodes { + for extra_node in extra_nodes { + query.visit(&mut self.socket, *extra_node) + } + } + + // Seed this query with the closest nodes we know about. + for node in routing_table_closest { + query.add_candidate(node) + } + + if let Some(CachedIterativeQuery { + closest_responding_nodes, + .. + }) = self.cached_iterative_queries.get(&target) + { + for node in closest_responding_nodes { + query.add_candidate(node.clone()) + } + } + + // After adding the nodes, we need to start the query. + query.start(&mut self.socket); + + self.iterative_queries.insert(target, query); + + None + } + + // === Private Methods === + + /// Handles a single inbound KRPC request. + /// + /// Responsibilities: + /// - During initial bootstrap (no known bootstrap nodes), adds the requester of + /// a `FindNode` to the routing table to seed it. + /// - If running in server mode, forwards the request to the embedded server and + /// emits a response or error using the provided `transaction_id`. + /// - Detects successful NAT traversal: when a `Ping` arrives from our own public + /// address, clears `firewalled`. + /// - Ensures node ID/IP consistency: if our ID is invalid for the observed + /// public IPv4, generates a new secure ID, resets the routing table, and + /// initiates a `FindNode` to repopulate it. + /// + /// Parameters: + /// - `from`: Source socket address of the requester. + /// - `transaction_id`: Transaction ID to echo in any response/error. + /// - `request_specific`: Parsed request payload and type. + /// + /// Side effects: + /// - May add `from` to the routing table (bootstrap exception). + /// - May send a protocol response or error. + /// - May set `firewalled = false` on self-`Ping`. + /// - May rotate this node’s ID, reset the routing table, and trigger a + /// rebootstrap query. + /// + /// Returns: Nothing. + fn handle_request( + &mut self, + from: SocketAddrV4, + transaction_id: u32, + request_specific: RequestSpecific, + ) { + // By default we only add nodes that responds to our requests. + // + // This is the only exception; the first node creating the DHT, + // without this exception, the bootstrapping node's routing table + // will never be populated. + if self.bootstrap.is_empty() { + if let RequestTypeSpecific::FindNode(param) = &request_specific.request_type { + self.routing_table.add(Node::new(param.target, from)); + } + } + + let is_ping = matches!(request_specific.request_type, RequestTypeSpecific::Ping); + + if self.server_mode() { + let server = &mut self.server; + + match server.handle_request(&self.routing_table, from, request_specific) { + Some(MessageType::Error(error)) => { + self.error(from, transaction_id, error); + } + Some(MessageType::Response(response)) => { + self.response(from, transaction_id, response); + } + _ => {} + }; + } + + if let Some(our_address) = self.public_address { + if from == our_address && is_ping { + self.firewalled = false; + + let ipv4 = our_address.ip(); + + // Restarting our routing table with new secure Id if necessary. + if !self.id().is_valid_for_ip(*ipv4) { + let new_id = Id::from_ipv4(*ipv4); + + info!( + "Our current id {} is not valid for address {}. Using new id {}", + self.id(), + our_address, + new_id + ); + + self.get( + GetRequestSpecific::FindNode(FindNodeRequestArguments { target: new_id }), + None, + ); + + self.routing_table = RoutingTable::new(new_id); + } + } + } + } + + /// Handles an inbound KRPC response for RPC, updating in-flight queries and optionally + /// returning a final value for the associated target. + /// + /// Behavior: + /// - Ignores responses marked read-only, recording them as invalid responses. + /// - If it matches an in-flight PutQuery, treats `Ping` as a storage ACK (success/error) + /// and stops further handling. + /// - If it matches an in-flight iterative query: + /// - Incorporates network info: adds closer candidates, records responder token, + /// and votes on the observed requester IP. + /// - On value responses: + /// - `GetPeers` → returns `(target, Response::Peers)` + /// - `GetImmutable` → validates content; on success returns `(target, Response::Immutable)` + /// - `GetMutable` → verifies record (sig/seq/salt); on success returns `(target, Response::Mutable)` + /// - Logs and continues on `NoValues` / `NoMoreRecentValue` / `Error`. + /// - On any expected response, adds the responder (by author ID) to the routing table. + /// + /// Parameters: + /// - `from`: Responder socket address. + /// - `message`: Decoded KRPC message. + /// + /// Returns: + /// - `Some((target, Response))` when a terminal value is obtained for the query. + /// - `None` otherwise. + fn handle_response(&mut self, from: SocketAddrV4, message: Message) -> Option<(Id, Response)> { + // If someone claims to be readonly, then let's not store anything even if they respond. + if message.read_only { + if let Some(query) = self + .iterative_queries + .values_mut() + .find(|query| query.is_inflight_query_request(message.transaction_id)) + { + query.record_invalid_response(); + } + + return None; + }; + + // If the response looks like a Ping response, check StoreQueries for the transaction_id. + if let Some(query) = self + .put_queries + .values_mut() + .find(|query| query.inflight(message.transaction_id)) + { + match message.message_type { + MessageType::Response(ResponseSpecific::Ping(_)) => { + // Mark storage at that node as a success. + query.success(); + } + MessageType::Error(error) => query.error(error), + _ => {} + }; + + return None; + } + + let mut should_add_node = false; + let author_id = message.get_author_id(); + let from_version = message.version.to_owned(); + + // Get corresponding query for message.transaction_id + if let Some(query) = self + .iterative_queries + .values_mut() + .find(|query| query.is_inflight(message.transaction_id)) + { + let is_query_response = query.is_inflight_query_request(message.transaction_id); + let is_mutable_get_query_response = is_query_response + && matches!( + &query.request.request_type, + RequestTypeSpecific::GetValue(_) + ); + + // KrpcSocket would not give us a response from the wrong address for the transaction_id + should_add_node = true; + + if let Some(nodes) = message.get_closer_nodes() { + for node in nodes { + query.add_candidate(node.clone()); + } + } + + if let Some((responder_id, token)) = message.get_token() { + query.add_responding_node(Node::new_with_token(responder_id, from, token.into())); + } + + if let Some(proposed_ip) = message.requester_ip { + query.add_address_vote(proposed_ip); + } + + let target = query.target(); + + match message.message_type { + MessageType::Response(ResponseSpecific::GetPeers(GetPeersResponseArguments { + values, + .. + })) => { + if is_mutable_get_query_response { + query.record_invalid_response(); + } + + let response = Response::Peers(values); + query.response(from, response.clone()); + + return Some((target, response)); + } + MessageType::Response(ResponseSpecific::GetImmutable( + GetImmutableResponseArguments { + v, responder_id, .. + }, + )) => { + if is_mutable_get_query_response { + query.record_invalid_response(); + } + + if validate_immutable(&v, query.target()) { + let response = Response::Immutable(v); + query.response(from, response.clone()); + + return Some((target, response)); + } + + let target = query.target(); + debug!( + ?v, + ?target, + ?responder_id, + ?from, + ?from_version, + "Invalid immutable value" + ); + } + MessageType::Response(ResponseSpecific::GetMutable( + GetMutableResponseArguments { + v, + seq, + sig, + k, + responder_id, + .. + }, + )) => { + let salt = match query.request.request_type.clone() { + RequestTypeSpecific::GetValue(args) => args.salt, + _ => None, + }; + let target = query.target(); + + match MutableItem::from_dht_message(query.target(), &k, v, seq, &sig, salt) { + Ok(item) => { + if is_mutable_get_query_response { + query.record_mutable_value(); + } + + let response = Response::Mutable(item); + query.response(from, response.clone()); + + return Some((target, response)); + } + Err(error) => { + if is_mutable_get_query_response { + query.record_invalid_mutable_value(); + } + + debug!( + ?error, + ?from, + ?responder_id, + ?from_version, + "Invalid mutable record" + ); + } + } + } + MessageType::Response(ResponseSpecific::NoMoreRecentValue( + NoMoreRecentValueResponseArguments { + seq, responder_id, .. + }, + )) => { + if is_mutable_get_query_response { + query.record_no_more_recent(); + } + + debug!( + target= ?query.target(), + salt= ?match query.request.request_type.clone() { + RequestTypeSpecific::GetValue(args) => args.salt, + _ => None, + }, + ?seq, + ?from, + ?responder_id, + ?from_version, + "No more recent value" + ); + } + MessageType::Response(ResponseSpecific::NoValues(NoValuesResponseArguments { + responder_id, + .. + })) => { + if is_mutable_get_query_response { + query.record_no_values(); + } + + debug!( + target= ?query.target(), + salt= ?match query.request.request_type.clone() { + RequestTypeSpecific::GetValue(args) => args.salt, + _ => None, + }, + ?from, + ?responder_id, + ?from_version , + "No values" + ); + } + MessageType::Error(error) => { + if is_mutable_get_query_response { + query.record_krpc_error(); + } + + debug!(?error, ?from_version, "Get query got error response"); + } + // Ping response is already handled in add_node() + // FindNode response is already handled in query.add_candidate() + // Requests are handled elsewhere + MessageType::Response(ResponseSpecific::Ping(_)) + | MessageType::Response(ResponseSpecific::FindNode(_)) + | MessageType::Request(_) => { + if is_mutable_get_query_response { + query.record_invalid_response(); + } + } + }; + }; + + if should_add_node { + // Add a node to our routing table on any expected incoming response. + + if let Some(id) = author_id { + self.routing_table.add(Node::new(id, from)); + } + } + + None + } + + /// Periodically maintain the routing table: + /// - Switches to server mode if eligible (and refresh is due) + /// - Pings nodes and purges stale entries when needed + /// - Repopulates via bootstrap if table is empty or refresh is due + /// - Updates last_table_refresh and last_table_ping timers as needed + fn periodic_node_maintenance(&mut self) { + let refresh_is_due = self.last_table_refresh.elapsed() >= REFRESH_TABLE_INTERVAL; + let ping_is_due = self.last_table_ping.elapsed() >= PING_TABLE_INTERVAL; + + // Decide first, act once: avoid double populate in the same tick. + let should_populate = self.routing_table.is_empty() || refresh_is_due; + + if refresh_is_due { + self.try_switching_to_server_mode(); + } + + if ping_is_due { + self.ping_and_purge(); + } + + if should_populate { + self.populate(); + } + } + + /// Attempts to switch this node into server mode if eligible. + /// + /// If the node is not currently operating + /// in server mode and is not detected as being behind a firewall, it will promote the + /// node into server mode (by setting the server_mode field to `true`). + /// + /// Server mode enables the node to answer unsolicited requests and fulfill a key + /// responsibility in the DHT. Nodes that are firewalled, or behind NAT, should not + /// enable server mode unless explicitly configured to do so. + fn try_switching_to_server_mode(&mut self) { + if !self.server_mode() && !self.firewalled() { + info!("Adaptive mode: have been running long enough (not firewalled), switching to server mode"); + self.socket.server_mode = true; + } + } + + /// Purge stale nodes and ping nodes that need probing when due is reached. + /// + /// It will purge stale nodes from the routing table and periodcially ping nodes. + /// It will reset the last_table_ping timer. + fn ping_and_purge(&mut self) { + self.last_table_ping = Instant::now(); + + let (to_purge, to_ping) = self.purge_and_ping_candidates(); + + self.purge_nodes(&to_purge); + self.ping_nodes(&to_ping); + + if to_purge.is_empty() && to_ping.is_empty() { + return; + } + + debug!( + removed = to_purge.len(), + pinged = to_ping.len(), + "Node maintenance executed" + ); + } + + /// Pure decision function: compute which nodes to remove and which to ping. + fn purge_and_ping_candidates(&self) -> (Vec, Vec) { + let mut to_purge = Vec::with_capacity(self.routing_table.size()); + let mut to_ping = Vec::with_capacity(self.routing_table.size()); + + for node in self.routing_table.nodes() { + if node.is_stale() { + to_purge.push(*node.id()) + } else if node.should_ping() { + to_ping.push(node.address()) + } + } + + (to_purge, to_ping) + } + + /// Remove nodes from the routing table. + fn purge_nodes(&mut self, ids: &[Id]) { + for id in ids { + self.routing_table.remove(id); + } + } + + /// Ping nodes. + fn ping_nodes(&mut self, addrs: &[SocketAddrV4]) { + for address in addrs { + self.ping(*address); + } + } + + /// Populate routing table by asking bootstrap nodes to find ourselves, + /// Response will allow to add closest nodes candidates to routing table. + /// + /// Reset the last_table_refresh timer. + fn populate(&mut self) { + self.last_table_refresh = Instant::now(); + + if self.bootstrap.is_empty() { + return; + } + + self.get( + GetRequestSpecific::FindNode(FindNodeRequestArguments { target: *self.id() }), + None, + ); + } + + /// Send a ping request to a node. + fn ping(&mut self, address: SocketAddrV4) { + self.socket.request( + address, + RequestSpecific { + requester_id: *self.id(), + request_type: RequestTypeSpecific::Ping, + }, + ); + } + + fn update_address_votes_from_iterative_query(&mut self, query: &IterativeQuery) { + let Some(new_address) = query.best_address() else { + return; + }; + + let needs_confirm = match self.public_address { + None => true, + Some(current) => current != new_address, + }; + + if needs_confirm { + debug!( + ?new_address, + "Query responses suggest a different public_address, trying to confirm.." + ); + + self.firewalled = true; + self.ping(new_address); + } + + self.public_address = Some(new_address); + } + + fn cache_iterative_query(&mut self, query: &IterativeQuery, closest_responding_nodes: &[Node]) { + if self.cached_iterative_queries.len() >= MAX_CACHED_ITERATIVE_QUERIES { + let q = self.cached_iterative_queries.pop_lru(); + self.decrement_cached_iterative_query_stats(q.map(|q| q.1)); + } + + let closest = query.closest(); + let responders = query.responders(); + + if closest.nodes().is_empty() { + // We are clearly offline. + return; + } + + let dht_size_estimate = closest.dht_size_estimate(); + let responders_dht_size_estimate = responders.dht_size_estimate(); + let subnets_count = closest.subnets_count(); + + let previous = self.cached_iterative_queries.put( + query.target(), + CachedIterativeQuery { + closest_responding_nodes: closest_responding_nodes.into(), + dht_size_estimate, + responders_dht_size_estimate, + subnets: subnets_count, + + is_find_node: matches!( + query.request.request_type, + RequestTypeSpecific::FindNode(_) + ), + }, + ); + + self.decrement_cached_iterative_query_stats(previous); + + self.dht_size_estimates_sum += dht_size_estimate; + self.responders_based_dht_size_estimates_sum += responders_dht_size_estimate; + self.subnets_sum += subnets_count as usize; + self.responders_based_dht_size_estimates_count += 1; + } + + fn responders_based_dht_size_estimate(&self) -> usize { + self.responders_based_dht_size_estimates_sum as usize + / self.responders_based_dht_size_estimates_count.max(1) + } + + fn average_subnets(&self) -> usize { + self.subnets_sum / self.cached_iterative_queries.len().max(1) + } + + fn decrement_cached_iterative_query_stats(&mut self, query: Option) { + if let Some(CachedIterativeQuery { + dht_size_estimate, + responders_dht_size_estimate, + subnets, + is_find_node, + .. + }) = query + { + self.dht_size_estimates_sum -= dht_size_estimate; + self.responders_based_dht_size_estimates_sum -= responders_dht_size_estimate; + self.subnets_sum -= subnets as usize; + + if !is_find_node { + self.responders_based_dht_size_estimates_count -= 1; + } + }; + } + + // === tick() helpers === + + /// Advance all PUT queries, return done ones. + fn tick_put_queries(&mut self) -> Vec<(Id, Result)> { + let mut done_put_queries = Vec::with_capacity(self.put_queries.len()); + + for (id, query) in self.put_queries.iter_mut() { + match query.poll_completion(&self.socket) { + Ok(Some(outcome)) => done_put_queries.push((*id, Ok(outcome))), + Ok(None) => (), + Err(error) => done_put_queries.push((*id, Err(error))), + }; + } + + done_put_queries + } + + /// Advance all GET/FIND_NODE queries, return done ones and whether table refresh/find_node to self is finished. + fn tick_get_queries(&mut self) -> GetQueriesResult { + let self_id = *self.id(); + let responders_based_dht_size_estimate = self.responders_based_dht_size_estimate(); + let average_subnets = self.average_subnets(); + + let mut done_get_queries = Vec::with_capacity(self.iterative_queries.len()); + let mut finished_self_findnode = false; + + for (id, query) in self.iterative_queries.iter_mut() { + if !query.tick(&mut self.socket) { + continue; + } + + let closest_nodes = if let RequestTypeSpecific::FindNode(_) = query.request.request_type + { + finished_self_findnode = *id == self_id; + + query + .closest() + .nodes() + .iter() + .take(MAX_BUCKET_SIZE_K) + .cloned() + .collect::>() + } else { + query + .responders() + .take_until_secure(responders_based_dht_size_estimate, average_subnets) + .to_vec() + .into_boxed_slice() + }; + + done_get_queries.push(GetQueryOutcome { + id: *id, + closest_nodes, + mutable_outcome: matches!( + query.request.request_type, + RequestTypeSpecific::GetValue(_) + ) + .then(|| query.mutable_outcome()), + }); + } + + (done_get_queries, finished_self_findnode) + } + + /// Remove completed GET and PUT queries from internal state. + fn cleanup_done_queries( + &mut self, + done_get: &[GetQueryOutcome], + done_put: &mut Vec<(Id, Result)>, + ) { + // Has to happen _before_ `self.socket.recv_from()`. + for done in done_get { + let query = match self.iterative_queries.remove(&done.id) { + Some(query) => query, + None => continue, + }; + + self.update_address_votes_from_iterative_query(&query); + self.cache_iterative_query(&query, &done.closest_nodes); + + // Only for get queries, not find node. + if matches!(query.request.request_type, RequestTypeSpecific::FindNode(_)) { + continue; + } + + let put_query = match self.put_queries.get_mut(&done.id) { + Some(put_query) => put_query, + None => continue, + }; + + if put_query.started() { + continue; + } + + if let Err(error) = put_query.start(&mut self.socket, &done.closest_nodes) { + done_put.push((done.id, Err(error))) + } + } + + for (id, _) in done_put.iter() { + self.put_queries.remove(id); + } + } + + /// Handle one incoming message, either a request or a response message. One message per tick. + fn handle_message(&mut self) -> Option<(Id, Response)> { + self.socket + .recv_from() + .and_then(|(message, from)| match message.message_type { + MessageType::Request(request_specific) => { + self.handle_request(from, message.transaction_id, request_specific); + None + } + _ => self.handle_response(from, message), + }) + } + + /// Check if routing table is empty and log an error if so. + fn log_bootstrap(&self, self_id: &Id) { + let table_size = self.routing_table.size(); + if table_size == 0 { + error!("Could not bootstrap the routing table"); + } else { + debug!(?self_id, table_size, "Populated the routing table"); + } + } +} + +struct CachedIterativeQuery { + closest_responding_nodes: Box<[Node]>, + dht_size_estimate: f64, + responders_dht_size_estimate: f64, + subnets: u8, + + /// Keeping track of find_node queries, because they shouldn't + /// be counted in `responders_based_dht_size_estimates_count` + is_find_node: bool, +} + +/// State change after a call to [Rpc::tick], including +/// done PUT, GET, and FIND_NODE queries, as well as any +/// incoming value response for any GET query. +#[derive(Debug, Clone)] +/// Completed GET/FIND_NODE query details returned from [Rpc::tick]. +pub struct GetQueryOutcome { + /// Query target id. + pub id: Id, + /// Closest responding nodes discovered by the query. + pub closest_nodes: Box<[Node]>, + /// Mutable GET diagnostics, present for GET value queries. + pub mutable_outcome: Option, +} + +#[derive(Debug, Clone)] +pub struct RpcTickReport { + /// All the [Id]s of the done [Rpc::get] queries. + pub done_get_queries: Vec, + /// All the [Id]s of the done [Rpc::put] queries, + /// and either the successful [PutOutcome] or a [PutError]. + pub done_put_queries: Vec<(Id, Result)>, + /// Received GET query response. + pub new_query_response: Option<(Id, Response)>, +} + +#[derive(Debug, Clone)] +pub enum Response { + Peers(Vec), + Immutable(Box<[u8]>), + Mutable(MutableItem), +} + +pub(crate) fn to_socket_address(bootstrap: &[T]) -> Vec { + bootstrap + .iter() + .flat_map(|s| { + s.to_socket_addrs().map(|addrs| { + addrs + .filter_map(|addr| match addr { + SocketAddr::V4(addr_v4) => Some(addr_v4), + _ => None, + }) + .collect::>() + }) + }) + .flatten() + .collect() +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, SocketAddrV4}; + + use crate::common::FindNodeResponseArguments; + use ed25519_dalek::SigningKey; + + use super::*; + + #[test] + fn get_does_not_echo_inflight_mutable_put() { + let mut rpc = Rpc::new(config::Config { + bootstrap: Some(vec![]), + bind_address: Some(Ipv4Addr::LOCALHOST), + ..Default::default() + }) + .unwrap(); + + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + + let item = MutableItem::new(signer, b"value", 1000, None); + let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from(item, None)); + let target = *request.target(); + + rpc.put(request, None).unwrap(); + + let responses = rpc + .get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + seq: None, + salt: None, + }), + None, + ) + .unwrap(); + + assert!(responses.is_empty()); + } + + #[test] + fn mutable_get_counts_wrong_primary_response_as_invalid() { + let mut rpc = Rpc::new(config::Config { + bootstrap: Some(vec![]), + bind_address: Some(Ipv4Addr::LOCALHOST), + ..Default::default() + }) + .unwrap(); + + let target = Id::random(); + let responder = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 42_000); + + rpc.get( + GetRequestSpecific::GetValue(GetValueRequestArguments { + target, + seq: None, + salt: None, + }), + Some(&[responder]), + ); + + let response = rpc.handle_response( + responder, + Message { + transaction_id: 0, + message_type: MessageType::Response(ResponseSpecific::FindNode( + FindNodeResponseArguments { + responder_id: Id::random(), + nodes: Vec::new().into_boxed_slice(), + }, + )), + version: None, + read_only: false, + requester_ip: None, + }, + ); + + let outcome = rpc + .iterative_queries + .get(&target) + .expect("mutable GET query should still be active") + .mutable_outcome(); + + assert!(response.is_none()); + assert_eq!(outcome.valid_responses(), 0); + assert_eq!(outcome.invalid_responses, 1); + assert_eq!(outcome.responded(), 1); + assert_eq!(outcome.timed_out(), 0); + } +} diff --git a/vendor/mainline/src/rpc/closest_nodes.rs b/vendor/mainline/src/rpc/closest_nodes.rs new file mode 100644 index 0000000..4cf1808 --- /dev/null +++ b/vendor/mainline/src/rpc/closest_nodes.rs @@ -0,0 +1,297 @@ +use std::{collections::HashSet, convert::TryInto}; + +use crate::{common::MAX_BUCKET_SIZE_K, Id, Node}; + +#[derive(Debug, Clone)] +/// Manage closest nodes found in a query. +/// +/// Useful to estimate the Dht size. +pub struct ClosestNodes { + target: Id, + nodes: Vec, +} + +impl ClosestNodes { + /// Create a new instance of [ClosestNodes]. + pub fn new(target: Id) -> Self { + Self { + target, + nodes: Vec::with_capacity(200), + } + } + + // === Getters === + + /// Returns the target of the query for these closest nodes. + pub fn target(&self) -> Id { + self.target + } + + /// Returns a slice of the nodes array. + pub fn nodes(&self) -> &[Node] { + &self.nodes + } + + /// Returns the number of nodes. + pub fn len(&self) -> usize { + self.nodes.len() + } + + /// Returns true if there are no nodes. + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + + // === Public Methods === + + /// Add a node. + pub fn add(&mut self, node: Node) { + let seek = node.id().xor(&self.target); + + if node.already_exists(&self.nodes) { + return; + } + + if let Err(pos) = self.nodes.binary_search_by(|prope| { + if prope.is_secure() && !node.is_secure() { + std::cmp::Ordering::Less + } else if !prope.is_secure() && node.is_secure() { + std::cmp::Ordering::Greater + } else if prope.id() == node.id() { + std::cmp::Ordering::Equal + } else { + prope.id().xor(&self.target).cmp(&seek) + } + }) { + self.nodes.insert(pos, node) + } + } + + /// Take enough nodes closest to the target, until the following are satisfied: + /// 1. At least the closest `k` nodes (20). + /// 2. The last node should be at a distance `edk` which is the expected distance of the 20th + /// node given previous estimations of the DHT size. + /// 3. The number of subnets with unique 6 bits prefix in nodes ipv4 addresses match or exceeds + /// the average from previous queries. + /// + /// If one or more of these conditions are not met, then we just take all responding nodes + /// and store data at them. + pub fn take_until_secure( + &self, + previous_dht_size_estimate: usize, + average_subnets: usize, + ) -> &[Node] { + let mut until_secure = 0; + + // 20 / dht_size_estimate == expected_dk / ID space + // so expected_dk = 20 * ID space / dht_size_estimate + let expected_dk = + (20.0 * u128::MAX as f64 / (previous_dht_size_estimate as f64 + 1.0)) as u128; + + let mut subnets = HashSet::new(); + + for node in &self.nodes { + let distance = distance(&self.target, node); + + subnets.insert(subnet(node)); + + if distance >= expected_dk && subnets.len() >= average_subnets { + break; + } + + until_secure += 1; + } + + &self.nodes[0..until_secure.max(MAX_BUCKET_SIZE_K).min(self.nodes().len())] + } + + /// Count the number of subnets with unique 6 bits prefix in ipv4 + pub fn subnets_count(&self) -> u8 { + if self.nodes.is_empty() { + return 20; + } + + let mut subnets = HashSet::new(); + + for node in self.nodes.iter().take(MAX_BUCKET_SIZE_K) { + subnets.insert(subnet(node)); + } + + subnets.len() as u8 + } + + /// An estimation of the Dht from the distribution of closest nodes + /// responding to a query. + /// + /// [Read more](https://github.com/pubky/mainline/blob/main/docs/dht_size_estimate.md) + pub fn dht_size_estimate(&self) -> f64 { + dht_size_estimate( + self.nodes + .iter() + .take(MAX_BUCKET_SIZE_K) + .map(|node| distance(&self.target, node)), + ) + } +} + +fn subnet(node: &Node) -> u8 { + ((node.address().ip().to_bits() >> 26) & 0b0011_1111) as u8 +} + +fn distance(target: &Id, node: &Node) -> u128 { + let xor = node.id().xor(target); + + // Round up the lower 4 bytes to get a u128 from u160. + u128::from_be_bytes(xor.as_bytes()[0..16].try_into().expect("infallible")) +} + +fn dht_size_estimate(distances: I) -> f64 +where + I: IntoIterator, +{ + let mut sum = 0.0; + let mut count = 0; + + // Ignoring the first node, as that gives the best result in simulations. + for distance in distances { + count += 1; + + sum += count as f64 * distance as f64; + } + + if count == 0 { + return 0.0; + } + + let lsq_constant = (count * (count + 1) * (2 * count + 1) / 6) as f64; + + lsq_constant * u128::MAX as f64 / sum +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, net::SocketAddrV4, str::FromStr, sync::Arc, time::Instant}; + + use crate::common::NodeInner; + + use super::*; + + #[test] + fn add_sorted_by_id() { + let target = Id::random(); + + let mut closest_nodes = ClosestNodes::new(target); + + for i in 0..100 { + let node = Node::unique(i); + closest_nodes.add(node.clone()); + closest_nodes.add(node); + } + + assert_eq!(closest_nodes.nodes().len(), 100); + + let distances = closest_nodes + .nodes() + .iter() + .map(|n| n.id().distance(&target)) + .collect::>(); + + let mut sorted = distances.clone(); + sorted.sort(); + + assert_eq!(sorted, distances); + } + + #[test] + fn order_by_secure_id() { + let unsecure = Node::random(); + let secure = Node(Arc::new(NodeInner { + id: Id::from_str("5a3ce9c14e7a08645677bbd1cfe7d8f956d53256").unwrap(), + address: SocketAddrV4::new([21, 75, 31, 124].into(), 0), + token: None, + last_seen: Instant::now(), + })); + + let mut closest_nodes = ClosestNodes::new(*unsecure.id()); + + closest_nodes.add(unsecure.clone()); + closest_nodes.add(secure.clone()); + + assert_eq!(closest_nodes.nodes(), vec![secure, unsecure]) + } + + #[test] + fn take_until_expected_distance_to_20th_node() { + let target = Id::random(); + let dht_size_estimate = 200; + + let mut closest_nodes = ClosestNodes::new(target); + + let target_bytes = target.as_bytes(); + + for i in 0..dht_size_estimate { + let node = Node::unique(i); + closest_nodes.add(node); + } + + let mut sybil = ClosestNodes::new(target); + + for _ in 0..20 { + let mut bytes = target_bytes.to_vec(); + bytes[18..].copy_from_slice(&Id::random().as_bytes()[18..]); + let node = Node::new(Id::random(), SocketAddrV4::new(0.into(), 0)); + + sybil.add(node.clone()); + closest_nodes.add(node); + } + + let closest = closest_nodes.take_until_secure(dht_size_estimate, 0); + + assert!((closest.len() - sybil.nodes().len()) > 10); + } + + #[test] + fn simulation() { + let lookups = 4; + let acceptable_margin = 0.2; + let sims = 10; + let dht_size = 2500_f64; + + let mean = (0..sims) + .map(|_| simulate(dht_size as usize, lookups) as f64) + .sum::() + / (sims as f64); + + let margin = (mean - dht_size).abs() / dht_size; + + assert!(margin <= acceptable_margin); + } + + fn simulate(dht_size: usize, lookups: usize) -> usize { + let mut nodes = BTreeMap::new(); + for i in 0..dht_size { + let node = Node::unique(i); + nodes.insert(*node.id(), node); + } + + (0..lookups) + .map(|_| { + let target = Id::random(); + + let mut closest_nodes = ClosestNodes::new(target); + + for (_, node) in nodes.range(target..).take(100) { + closest_nodes.add(node.clone()) + } + for (_, node) in nodes.range(..target).rev().take(100) { + closest_nodes.add(node.clone()) + } + + let estimate = closest_nodes.dht_size_estimate(); + + estimate as usize + }) + .sum::() + / lookups + } +} diff --git a/vendor/mainline/src/rpc/config.rs b/vendor/mainline/src/rpc/config.rs new file mode 100644 index 0000000..8bb8aba --- /dev/null +++ b/vendor/mainline/src/rpc/config.rs @@ -0,0 +1,56 @@ +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + time::Duration, +}; + +use super::{ServerSettings, DEFAULT_REQUEST_TIMEOUT}; + +#[derive(Debug, Clone)] +/// Dht Configurations +pub struct Config { + /// Bootstrap nodes + /// + /// Defaults to [super::DEFAULT_BOOTSTRAP_NODES] + pub bootstrap: Option>, + /// Explicit port to listen on. + /// + /// Defaults to None + pub port: Option, + /// UDP socket request timeout duration. + /// + /// The longer this duration is, the longer queries take until they are deemeed "done". + /// The shortet this duration is, the more responses from busy nodes we miss out on, + /// which affects the accuracy of queries trying to find closest nodes to a target. + /// + /// Defaults to [DEFAULT_REQUEST_TIMEOUT] + pub request_timeout: Duration, + /// Server to respond to incoming Requests + pub server_settings: ServerSettings, + /// Whether or not to start in server mode from the get go. + /// + /// Defaults to false where it will run in [Adaptive mode](https://github.com/pubky/mainline?tab=readme-ov-file#adaptive-mode). + pub server_mode: bool, + /// A known public IPv4 address for this node to generate + /// a secure node Id from according to [BEP_0042](https://www.bittorrent.org/beps/bep_0042.html) + /// + /// Defaults to None, where we depend on suggestions from responding nodes. + pub public_ip: Option, + /// Address to bind to. + /// + /// Defaults to 0.0.0.0 (all interfaces) + pub bind_address: Option, +} + +impl Default for Config { + fn default() -> Self { + Self { + bootstrap: None, + port: None, + request_timeout: DEFAULT_REQUEST_TIMEOUT, + server_settings: Default::default(), + server_mode: false, + public_ip: None, + bind_address: None, + } + } +} diff --git a/vendor/mainline/src/rpc/info.rs b/vendor/mainline/src/rpc/info.rs new file mode 100644 index 0000000..6f1063e --- /dev/null +++ b/vendor/mainline/src/rpc/info.rs @@ -0,0 +1,70 @@ +use std::net::SocketAddrV4; + +use crate::Id; + +use super::Rpc; + +/// Information and statistics about this mainline node. +#[derive(Debug, Clone)] +pub struct Info { + id: Id, + local_addr: SocketAddrV4, + public_address: Option, + firewalled: bool, + dht_size_estimate: (usize, f64), + server_mode: bool, +} + +impl Info { + /// This Node's [Id] + pub fn id(&self) -> &Id { + &self.id + } + /// Local UDP Ipv4 socket address that this node is listening on. + pub fn local_addr(&self) -> SocketAddrV4 { + self.local_addr + } + /// Returns the best guess for this node's Public address. + /// + /// If [crate::DhtBuilder::public_ip] was set, this is what will be returned + /// (plus the local port), otherwise it will rely on consensus from + /// responding nodes voting on our public IP and port. + pub fn public_address(&self) -> Option { + self.public_address + } + /// Returns `true` if we can't confirm that [Self::public_address] is publicly addressable. + /// + /// If this node is firewalled, it won't switch to server mode if it is in adaptive mode, + /// but if [crate::DhtBuilder::server_mode] was set to true, then whether or not this node is firewalled + /// won't matter. + pub fn firewalled(&self) -> bool { + self.firewalled + } + + /// Returns whether or not this node is running in server mode. + pub fn server_mode(&self) -> bool { + self.server_mode + } + + /// Returns: + /// 1. Normal Dht size estimate based on all closer `nodes` in query responses. + /// 2. Standard deviaiton as a function of the number of samples used in this estimate. + /// + /// [Read more](https://github.com/pubky/mainline/blob/main/docs/dht_size_estimate.md) + pub fn dht_size_estimate(&self) -> (usize, f64) { + self.dht_size_estimate + } +} + +impl From<&Rpc> for Info { + fn from(rpc: &Rpc) -> Self { + Self { + id: *rpc.id(), + local_addr: rpc.local_addr(), + dht_size_estimate: rpc.dht_size_estimate(), + public_address: rpc.public_address(), + firewalled: rpc.firewalled(), + server_mode: rpc.server_mode(), + } + } +} diff --git a/vendor/mainline/src/rpc/iterative_query.rs b/vendor/mainline/src/rpc/iterative_query.rs new file mode 100644 index 0000000..475a812 --- /dev/null +++ b/vendor/mainline/src/rpc/iterative_query.rs @@ -0,0 +1,310 @@ +//! Manage iterative queries and their corresponding request/response. + +use std::collections::HashMap; +use std::collections::HashSet; +use std::net::SocketAddrV4; + +use tracing::{debug, trace}; + +use super::{socket::KrpcSocket, ClosestNodes}; +use crate::common::{FindNodeRequestArguments, GetPeersRequestArguments, GetValueRequestArguments}; +use crate::{ + common::{Id, Node, RequestSpecific, RequestTypeSpecific, MAX_BUCKET_SIZE_K}, + rpc::Response, +}; + +/// Aggregate diagnostics for a mutable GET query. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct GetMutableOutcome { + /// Number of unique DHT nodes queried. + pub queried: u32, + /// Number of valid mutable values returned. + pub values: u32, + /// Number of `NoValues` responses returned. + pub no_values: u32, + /// Number of `NoMoreRecentValue` responses returned. + pub no_more_recent: u32, + /// Number of mutable value responses that failed validation. + pub invalid_values: u32, + /// Number of invalid response shapes returned. + pub invalid_responses: u32, + /// Number of KRPC error responses returned. + pub krpc_errors: u32, +} + +impl GetMutableOutcome { + /// Return the number of nodes that returned a GET response before timing out. + pub fn responded(&self) -> u32 { + self.valid_responses() + self.invalid_values + self.invalid_responses + self.krpc_errors + } + + /// Return the number of nodes that returned a valid GET response. + pub fn valid_responses(&self) -> u32 { + self.values + self.no_values + self.no_more_recent + } + + /// Return the number of queried nodes that did not return a GET response before timeout. + pub fn timed_out(&self) -> u32 { + self.queried.saturating_sub(self.responded()) + } + + fn record_value(&mut self) { + self.values += 1; + } + + fn record_no_values(&mut self) { + self.no_values += 1; + } + + fn record_no_more_recent(&mut self) { + self.no_more_recent += 1; + } + + fn record_invalid_value(&mut self) { + self.invalid_values += 1; + } + + fn record_invalid_response(&mut self) { + self.invalid_responses += 1; + } + + fn record_krpc_error(&mut self) { + self.krpc_errors += 1; + } + + fn finish(mut self, queried: u32) -> Self { + self.queried = queried; + self + } +} + +/// An iterative process of concurrently sending a request to the closest known nodes to +/// the target, updating the routing table with closer nodes discovered in the responses, and +/// repeating this process until no closer nodes (that aren't already queried) are found. +#[derive(Debug)] +pub(crate) struct IterativeQuery { + pub request: RequestSpecific, + closest: ClosestNodes, + responders: ClosestNodes, + inflight_requests: Vec, + query_requests: Vec, + visited: HashSet, + responses: Vec, + mutable_outcome: GetMutableOutcome, + public_address_votes: HashMap, +} + +#[derive(Debug)] +pub enum GetRequestSpecific { + FindNode(FindNodeRequestArguments), + GetPeers(GetPeersRequestArguments), + GetValue(GetValueRequestArguments), +} + +impl GetRequestSpecific { + pub fn target(&self) -> &Id { + match self { + GetRequestSpecific::FindNode(args) => &args.target, + GetRequestSpecific::GetPeers(args) => &args.info_hash, + GetRequestSpecific::GetValue(args) => &args.target, + } + } +} + +impl IterativeQuery { + pub fn new(requester_id: Id, target: Id, request: GetRequestSpecific) -> Self { + let request_type = match request { + GetRequestSpecific::FindNode(s) => RequestTypeSpecific::FindNode(s), + GetRequestSpecific::GetPeers(s) => RequestTypeSpecific::GetPeers(s), + GetRequestSpecific::GetValue(s) => RequestTypeSpecific::GetValue(s), + }; + + trace!(?target, ?request_type, "New Query"); + + Self { + request: RequestSpecific { + requester_id, + request_type, + }, + + closest: ClosestNodes::new(target), + responders: ClosestNodes::new(target), + + inflight_requests: Vec::new(), + query_requests: Vec::new(), + visited: HashSet::new(), + + responses: Vec::new(), + mutable_outcome: GetMutableOutcome::default(), + + public_address_votes: HashMap::new(), + } + } + + // === Getters === + + pub fn target(&self) -> Id { + self.responders.target() + } + + /// Closest nodes according to other nodes. + pub fn closest(&self) -> &ClosestNodes { + &self.closest + } + + /// Return the closest responding nodes after the query is done. + pub fn responders(&self) -> &ClosestNodes { + &self.responders + } + + pub fn responses(&self) -> &[Response] { + &self.responses + } + + pub fn mutable_outcome(&self) -> GetMutableOutcome { + self.mutable_outcome + .clone() + .finish(self.visited.len() as u32) + } + + pub fn best_address(&self) -> Option { + let mut max = 0_u16; + let mut best_addr = None; + + for (addr, count) in self.public_address_votes.iter() { + if *count > max { + max = *count; + best_addr = Some(*addr); + }; + } + + best_addr + } + + // === Public Methods === + + /// Force start query traversal by visiting closest nodes. + pub fn start(&mut self, socket: &mut KrpcSocket) { + self.visit_closest(socket); + } + + /// Add a candidate node to query on next tick if it is among the closest nodes. + pub fn add_candidate(&mut self, node: Node) { + // ready for a ipv6 routing table? + self.closest.add(node); + } + + /// Add a vote for this node's address. + pub fn add_address_vote(&mut self, address: SocketAddrV4) { + self.public_address_votes + .entry(address) + .and_modify(|counter| *counter += 1) + .or_insert(1); + } + + /// Visit explicitly given addresses, and add them to the visited set. + /// only used from the Rpc when calling bootstrapping nodes. + pub fn visit(&mut self, socket: &mut KrpcSocket, address: SocketAddrV4) { + let tid = socket.request(address, self.request.clone()); + self.inflight_requests.push(tid); + self.query_requests.push(tid); + + let tid = socket.request( + address, + RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::Ping, + }, + ); + self.inflight_requests.push(tid); + + self.visited.insert(address); + } + + /// Return true if a response (by transaction_id) is expected by this query. + pub fn is_inflight(&self, tid: u32) -> bool { + self.inflight_requests.contains(&tid) + } + + /// Return true if the transaction belongs to the primary query request, not the liveness ping. + pub fn is_inflight_query_request(&self, tid: u32) -> bool { + self.query_requests.contains(&tid) + } + + /// Add a node that responded with a token as a probable storage node. + pub fn add_responding_node(&mut self, node: Node) { + self.responders.add(node) + } + + /// Store received response. + pub fn response(&mut self, from: SocketAddrV4, response: Response) { + let target = self.target(); + + debug!(?target, ?response, ?from, "Query got response"); + + self.responses.push(response.to_owned()); + } + + pub fn record_mutable_value(&mut self) { + self.mutable_outcome.record_value(); + } + + pub fn record_no_values(&mut self) { + self.mutable_outcome.record_no_values(); + } + + pub fn record_no_more_recent(&mut self) { + self.mutable_outcome.record_no_more_recent(); + } + + pub fn record_invalid_response(&mut self) { + self.mutable_outcome.record_invalid_response(); + } + + pub fn record_krpc_error(&mut self) { + self.mutable_outcome.record_krpc_error(); + } + + pub fn record_invalid_mutable_value(&mut self) { + self.mutable_outcome.record_invalid_value(); + } + + /// Query closest nodes for this query's target and message. + /// + /// Returns true if it is done. + pub fn tick(&mut self, socket: &mut KrpcSocket) -> bool { + // Visit closest nodes + self.visit_closest(socket); + + // If no more inflight_requests are inflight in the socket (not timed out), + // then the query is done. + let done = !self + .inflight_requests + .iter() + .any(|&tid| socket.inflight(tid)); + + if done { + debug!(id=?self.target(), closest = ?self.closest.len(), visited = ?self.visited.len(), responders = ?self.responders.len(), "Done query"); + }; + + done + } + + // === Private Methods === + + /// Visit the closest candidates and remove them as candidates + fn visit_closest(&mut self, socket: &mut KrpcSocket) { + let to_visit = self + .closest + .nodes() + .iter() + .take(MAX_BUCKET_SIZE_K) + .filter(|node| !self.visited.contains(&node.address())) + .map(|node| node.address()) + .collect::>(); + + for address in to_visit { + self.visit(socket, address); + } + } +} diff --git a/vendor/mainline/src/rpc/put_query.rs b/vendor/mainline/src/rpc/put_query.rs new file mode 100644 index 0000000..492ef47 --- /dev/null +++ b/vendor/mainline/src/rpc/put_query.rs @@ -0,0 +1,309 @@ +use tracing::{debug, trace}; + +use crate::{ + common::{ + ErrorSpecific, Id, PutRequest, PutRequestSpecific, RequestSpecific, RequestTypeSpecific, + }, + Node, +}; + +use super::socket::KrpcSocket; + +/// Stores data at the closest nodes after an [super::IterativeQuery] is done, +/// or when a previous cached query is available. +/// +/// Tracks successful acknowledgements and errors for the PUT query. +#[derive(Debug)] +pub struct PutQuery { + pub target: Id, + /// Nodes that confirmed success + stored_at: u32, + inflight_requests: Vec, + pub request: PutRequestSpecific, + errors: Vec<(u8, ErrorSpecific)>, + extra_nodes: Box<[Node]>, +} + +impl PutQuery { + pub fn new(target: Id, request: PutRequestSpecific, extra_nodes: Option>) -> Self { + Self { + target, + stored_at: 0, + inflight_requests: Vec::new(), + request, + errors: Vec::new(), + extra_nodes: extra_nodes.unwrap_or_default(), + } + } + + pub fn start( + &mut self, + socket: &mut KrpcSocket, + closest_nodes: &[Node], + ) -> Result<(), PutError> { + assert!(!self.started(), "should not call PutQuery::start() twice"); + + let target = self.target; + trace!(?target, "PutQuery start"); + + if closest_nodes.is_empty() { + Err(PutQueryError::NoClosestNodes)?; + } + + assert!( + closest_nodes.len() <= u8::MAX as usize, + "should not send PUT query to more than 256 nodes" + ); + + for node in closest_nodes.iter().chain(self.extra_nodes.iter()) { + // Set correct values to the request placeholders + if let Some(token) = node.token() { + let tid = socket.request( + node.address(), + RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::Put(PutRequest { + token, + put_request_type: self.request.clone(), + }), + }, + ); + + self.inflight_requests.push(tid); + } + } + + Ok(()) + } + + pub fn started(&self) -> bool { + !self.inflight_requests.is_empty() + } + + pub fn inflight(&self, tid: u32) -> bool { + self.inflight_requests.contains(&tid) + } + + pub fn success(&mut self) { + debug!(target = ?self.target, "PutQuery got success response"); + self.stored_at += 1 + } + + pub fn error(&mut self, error: ErrorSpecific) { + debug!(target = ?self.target, ?error, "PutQuery got error"); + + if let Some(pos) = self + .errors + .iter() + .position(|(_, err)| error.code == err.code) + { + // Increment the count of the existing error + self.errors[pos].0 += 1; + + // Move the updated element to maintain the order (highest count first) + let mut i = pos; + while i > 0 && self.errors[i].0 > self.errors[i - 1].0 { + self.errors.swap(i, i - 1); + i -= 1; + } + } else { + // Add the new error with a count of 1 + self.errors.push((1, error)); + } + } + + /// Check if the query has completed, returning the PUT outcome when complete. + pub fn poll_completion(&self, socket: &KrpcSocket) -> Result, PutError> { + if !self.started() { + return Ok(None); + } + + if let Some(most_common_error) = self.majority_nodes_rejected_put_mutable() { + debug!( + target = ?self.target, + ?most_common_error, + nodes_count = self.inflight_requests.len(), + "PutQuery for MutableItem was rejected by most nodes with 3xx code." + ); + + return Err(PutError::from(most_common_error)); + } + + // And all queries got responses or timed out. + if self.is_done(socket) { + let target = self.target; + + if self.stored_at == 0 { + let most_common_error = self.most_common_error(); + + debug!( + ?target, + ?most_common_error, + nodes_count = self.inflight_requests.len(), + "Put Query: failed" + ); + + return Err(most_common_error + .map(|(_, error)| error) + .unwrap_or(PutQueryError::Timeout.into())); + } + + debug!(?target, stored_at = ?self.stored_at, "PutQuery Done successfully"); + + return Ok(Some(PutOutcome { + target: self.target, + stored_at: self.stored_at, + })); + } + + Ok(None) + } + + fn is_done(&self, socket: &KrpcSocket) -> bool { + self.inflight_requests + .iter() + .copied() + .all(|transaction_id| !socket.inflight(transaction_id)) + } + + fn majority_nodes_rejected_put_mutable(&self) -> Option { + if !matches!(self.request, PutRequestSpecific::PutMutable(_)) { + return None; + } + + let (count, error) = self.most_common_error()?; + let half = ((self.inflight_requests.len() / 2) + 1) as u8; + if count < half { + return None; + } + + match error { + PutError::Concurrency(error) => Some(error), + PutError::Query(_) => None, + } + } + + fn most_common_error(&self) -> Option<(u8, PutError)> { + self.errors + .first() + .and_then(|(count, error)| match error.code { + 301 => Some((*count, PutError::from(ConcurrencyError::CasFailed))), + 302 => Some((*count, PutError::from(ConcurrencyError::NotMostRecent))), + _ => None, + }) + } +} + +/// Result details for a successful PUT query. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PutOutcome { + /// DHT target the request was published under. + pub target: Id, + + /// Number of DHT nodes that acknowledged storing the item. + pub stored_at: u32, +} + +/// PutQuery errors +#[derive(thiserror::Error, Debug, Clone)] +pub enum PutError { + /// Common PutQuery errors + #[error(transparent)] + Query(#[from] PutQueryError), + + #[error(transparent)] + /// PutQuery for [crate::MutableItem] errors + Concurrency(#[from] ConcurrencyError), +} + +/// Common PutQuery errors +#[derive(thiserror::Error, Debug, Clone)] +pub enum PutQueryError { + /// Failed to find any nodes close, usually means dht node failed to bootstrap, + /// so the routing table is empty. Check the machine's access to UDP socket, + /// or find better bootstrapping nodes. + #[error("Failed to find any nodes close to store value at")] + NoClosestNodes, + + /// Either Put Query failed to store at any nodes, and most nodes responded + /// with a non `301` nor `302` errors. + /// + /// Either way; contains the most common error response. + #[error("Query Error Response")] + ErrorResponse(ErrorSpecific), + + /// PutQuery timed out with no responses neither success or errors + #[error("PutQuery timed out with no responses neither success or errors")] + Timeout, +} + +/// PutQuery for [crate::MutableItem] errors +#[derive(thiserror::Error, Debug, Clone)] +pub enum ConcurrencyError { + /// Trying to PUT mutable items with the same `key`, and `salt` but different `seq`. + /// + /// Moreover, the more recent item does _NOT_ mention the the earlier + /// item's `seq` in its `cas` field. + /// + /// This risks a [Lost Update Problem](https://en.wikipedia.org/wiki/Write-write_conflict). + /// + /// Try reading most recent mutable item before writing again, + /// and make sure to set the `cas` field. + #[error("Conflict risk, try reading most recent item before writing again.")] + ConflictRisk, + + /// The [crate::MutableItem::seq] is less than or equal the sequence from another signed item. + /// + /// Try reading most recent mutable item before writing again. + #[error("MutableItem::seq is not the most recent, try reading most recent item before writing again.")] + NotMostRecent, + + /// The `CAS` condition does not match the `seq` of the most recent known signed item. + #[error("CAS check failed, try reading most recent item before writing again.")] + CasFailed, +} + +#[cfg(test)] +mod tests { + use crate::{ + common::{PutMutableRequestArguments, PutRequestSpecific}, + MutableItem, SigningKey, + }; + + use super::{ConcurrencyError, PutError, PutQuery}; + use crate::common::ErrorSpecific; + use crate::rpc::socket::KrpcSocket; + + #[test] + fn mutable_majority_cas_failure_wins_over_completed_success() { + let signer = SigningKey::from_bytes(&[ + 56, 171, 62, 85, 105, 58, 155, 209, 189, 8, 59, 109, 137, 84, 84, 201, 221, 115, 7, + 228, 127, 70, 4, 204, 182, 64, 77, 98, 92, 215, 27, 103, + ]); + let item = MutableItem::new(signer, b"value", 1002, None); + let request = PutRequestSpecific::PutMutable(PutMutableRequestArguments::from( + item.clone(), + Some(1000), + )); + let mut query = PutQuery::new(*item.target(), request, None); + + query.inflight_requests = vec![1, 2, 3]; + query.success(); + query.error(cas_failed()); + query.error(cas_failed()); + + let socket = KrpcSocket::client().unwrap(); + + assert!(matches!( + query.poll_completion(&socket), + Err(PutError::Concurrency(ConcurrencyError::CasFailed)) + )); + } + + fn cas_failed() -> ErrorSpecific { + ErrorSpecific { + code: 301, + description: "cas failed".to_string(), + } + } +} diff --git a/vendor/mainline/src/rpc/server.rs b/vendor/mainline/src/rpc/server.rs new file mode 100644 index 0000000..8b05588 --- /dev/null +++ b/vendor/mainline/src/rpc/server.rs @@ -0,0 +1,421 @@ +//! Modules needed only for nodes running in server mode (not read-only). + +pub mod peers; +pub mod tokens; + +use std::{fmt::Debug, net::SocketAddrV4, num::NonZeroUsize}; + +use dyn_clone::DynClone; +use lru::LruCache; +use tracing::debug; + +use crate::common::{ + validate_immutable, AnnouncePeerRequestArguments, ErrorSpecific, FindNodeRequestArguments, + FindNodeResponseArguments, GetImmutableResponseArguments, GetMutableResponseArguments, + GetPeersRequestArguments, GetPeersResponseArguments, GetValueRequestArguments, Id, MutableItem, + NoMoreRecentValueResponseArguments, NoValuesResponseArguments, PingResponseArguments, + PutImmutableRequestArguments, PutMutableRequestArguments, PutRequest, PutRequestSpecific, + RequestTypeSpecific, ResponseSpecific, RoutingTable, +}; + +use peers::PeersStore; +use tokens::Tokens; + +pub use crate::common::{MessageType, RequestSpecific}; + +/// Default maximum number of info_hashes for which to store peers. +pub const MAX_INFO_HASHES: usize = 2000; +/// Default maximum number of peers to store per info_hash. +pub const MAX_PEERS: usize = 500; +/// Default maximum number of Immutable and Mutable items to store. +pub const MAX_VALUES: usize = 1000; + +/// A trait for filtering incoming requests to a DHT node and +/// decide whether to allow handling it or rate limit or ban +/// the requester, or prohibit specific requests' details. +pub trait RequestFilter: Send + Sync + Debug + DynClone { + /// Returns true if the request from this source is allowed. + fn allow_request(&self, request: &RequestSpecific, from: SocketAddrV4) -> bool; +} + +dyn_clone::clone_trait_object!(RequestFilter); + +#[derive(Debug, Clone)] +struct DefaultFilter; + +impl RequestFilter for DefaultFilter { + fn allow_request(&self, _request: &RequestSpecific, _from: SocketAddrV4) -> bool { + true + } +} + +#[derive(Debug)] +/// A server that handles incoming requests. +/// +/// Supports [BEP_005](https://www.bittorrent.org/beps/bep_0005.html) and [BEP_0044](https://www.bittorrent.org/beps/bep_0044.html). +/// +/// But it doesn't implement any rate-limiting or blocking. +pub struct Server { + /// Tokens generator + tokens: Tokens, + /// Peers store + peers: PeersStore, + /// Immutable values store + immutable_values: LruCache>, + /// Mutable values store + mutable_values: LruCache, + /// Filter requests before handling them. + filter: Box, +} + +impl Default for Server { + fn default() -> Self { + Self::new(ServerSettings::default()) + } +} + +#[derive(Debug, Clone)] +/// Settings for the default dht server. +pub struct ServerSettings { + /// The maximum info_hashes for which to store peers. + /// + /// Defaults to [MAX_INFO_HASHES] + pub max_info_hashes: usize, + /// The maximum peers to store per info_hash. + /// + /// Defaults to [MAX_PEERS] + pub max_peers_per_info_hash: usize, + /// Maximum number of immutable values to store. + /// + /// Defaults to [MAX_VALUES] + pub max_immutable_values: usize, + /// Maximum number of mutable values to store. + /// + /// Defaults to [MAX_VALUES] + pub max_mutable_values: usize, + /// Filter requests before handling them. + /// + /// Defaults to a function that always returns true. + pub filter: Box, +} + +impl Default for ServerSettings { + fn default() -> Self { + Self { + max_info_hashes: MAX_INFO_HASHES, + max_peers_per_info_hash: MAX_PEERS, + max_mutable_values: MAX_VALUES, + max_immutable_values: MAX_VALUES, + + filter: Box::new(DefaultFilter), + } + } +} + +impl Server { + /// Creates a new [Server] + pub fn new(settings: ServerSettings) -> Self { + let tokens = Tokens::new(); + + Self { + tokens, + peers: PeersStore::new( + NonZeroUsize::new(settings.max_info_hashes).unwrap_or( + NonZeroUsize::new(MAX_INFO_HASHES).expect("MAX_PEERS is NonZeroUsize"), + ), + NonZeroUsize::new(settings.max_peers_per_info_hash) + .unwrap_or(NonZeroUsize::new(MAX_PEERS).expect("MAX_PEERS is NonZeroUsize")), + ), + + immutable_values: LruCache::new( + NonZeroUsize::new(settings.max_immutable_values) + .unwrap_or(NonZeroUsize::new(MAX_VALUES).expect("MAX_VALUES is NonZeroUsize")), + ), + mutable_values: LruCache::new( + NonZeroUsize::new(settings.max_mutable_values) + .unwrap_or(NonZeroUsize::new(MAX_VALUES).expect("MAX_VALUES is NonZeroUsize")), + ), + filter: settings.filter, + } + } + + /// Returns an optional response or an error for a request. + /// + /// Passed to the Rpc to send back to the requester. + pub fn handle_request( + &mut self, + routing_table: &RoutingTable, + from: SocketAddrV4, + request: RequestSpecific, + ) -> Option { + if !self.filter.allow_request(&request, from) { + return None; + } + + // Lazily rotate secrets before handling a request + if self.tokens.should_update() { + self.tokens.rotate() + } + + let requester_id = request.requester_id; + + Some(match request.request_type { + RequestTypeSpecific::Ping => { + MessageType::Response(ResponseSpecific::Ping(PingResponseArguments { + responder_id: *routing_table.id(), + })) + } + RequestTypeSpecific::FindNode(FindNodeRequestArguments { target, .. }) => { + MessageType::Response(ResponseSpecific::FindNode(FindNodeResponseArguments { + responder_id: *routing_table.id(), + nodes: routing_table.closest(target), + })) + } + RequestTypeSpecific::GetPeers(GetPeersRequestArguments { info_hash, .. }) => { + MessageType::Response(match self.peers.get_random_peers(&info_hash) { + Some(peers) => ResponseSpecific::GetPeers(GetPeersResponseArguments { + responder_id: *routing_table.id(), + token: self.tokens.generate_token(from).into(), + nodes: Some(routing_table.closest(info_hash)), + values: peers, + }), + None => ResponseSpecific::NoValues(NoValuesResponseArguments { + responder_id: *routing_table.id(), + token: self.tokens.generate_token(from).into(), + nodes: Some(routing_table.closest(info_hash)), + }), + }) + } + RequestTypeSpecific::GetValue(GetValueRequestArguments { target, seq, .. }) => { + if seq.is_some() { + MessageType::Response(self.handle_get_mutable(routing_table, from, target, seq)) + } else if let Some(v) = self.immutable_values.get(&target) { + MessageType::Response(ResponseSpecific::GetImmutable( + GetImmutableResponseArguments { + responder_id: *routing_table.id(), + token: self.tokens.generate_token(from).into(), + nodes: Some(routing_table.closest(target)), + v: v.clone(), + }, + )) + } else { + MessageType::Response(self.handle_get_mutable(routing_table, from, target, seq)) + } + } + RequestTypeSpecific::Put(PutRequest { + token, + put_request_type, + }) => match put_request_type { + PutRequestSpecific::AnnouncePeer(AnnouncePeerRequestArguments { + info_hash, + port, + implied_port, + .. + }) => { + if !self.tokens.validate(from, &token) { + debug!( + ?info_hash, + ?requester_id, + ?from, + request_type = "announce_peer", + "Invalid token" + ); + + return Some(MessageType::Error(ErrorSpecific { + code: 203, + description: "Bad token".to_string(), + })); + } + + let peer = match implied_port { + Some(true) => from, + _ => SocketAddrV4::new(*from.ip(), port), + }; + + self.peers + .add_peer(info_hash, (&request.requester_id, peer)); + + return Some(MessageType::Response(ResponseSpecific::Ping( + PingResponseArguments { + responder_id: *routing_table.id(), + }, + ))); + } + PutRequestSpecific::PutImmutable(PutImmutableRequestArguments { + v, + target, + .. + }) => { + if !self.tokens.validate(from, &token) { + debug!( + ?target, + ?requester_id, + ?from, + request_type = "put_immutable", + "Invalid token" + ); + + return Some(MessageType::Error(ErrorSpecific { + code: 203, + description: "Bad token".to_string(), + })); + } + + if v.len() > 1000 { + debug!(?target, ?requester_id, ?from, size = ?v.len(), "Message (v field) too big."); + + return Some(MessageType::Error(ErrorSpecific { + code: 205, + description: "Message (v field) too big.".to_string(), + })); + } + if !validate_immutable(&v, target) { + debug!(?target, ?requester_id, ?from, v = ?v, "Target doesn't match the sha1 hash of v field."); + + return Some(MessageType::Error(ErrorSpecific { + code: 203, + description: "Target doesn't match the sha1 hash of v field" + .to_string(), + })); + } + + self.immutable_values.put(target, v); + + return Some(MessageType::Response(ResponseSpecific::Ping( + PingResponseArguments { + responder_id: *routing_table.id(), + }, + ))); + } + PutRequestSpecific::PutMutable(PutMutableRequestArguments { + target, + v, + k, + seq, + sig, + salt, + cas, + .. + }) => { + if !self.tokens.validate(from, &token) { + debug!( + ?target, + ?requester_id, + ?from, + request_type = "put_mutable", + "Invalid token" + ); + return Some(MessageType::Error(ErrorSpecific { + code: 203, + description: "Bad token".to_string(), + })); + } + if v.len() > 1000 { + return Some(MessageType::Error(ErrorSpecific { + code: 205, + description: "Message (v field) too big.".to_string(), + })); + } + if let Some(ref salt) = salt { + if salt.len() > 64 { + return Some(MessageType::Error(ErrorSpecific { + code: 207, + description: "salt (salt field) too big.".to_string(), + })); + } + } + if let Some(previous) = self.mutable_values.get(&target) { + if let Some(cas) = cas { + if previous.seq() != cas { + debug!( + ?target, + ?requester_id, + ?from, + "CAS mismatched, re-read value and try again." + ); + + return Some(MessageType::Error(ErrorSpecific { + code: 301, + description: "CAS mismatched, re-read value and try again." + .to_string(), + })); + } + }; + + if seq < previous.seq() { + debug!( + ?target, + ?requester_id, + ?from, + "Sequence number less than current." + ); + + return Some(MessageType::Error(ErrorSpecific { + code: 302, + description: "Sequence number less than current.".to_string(), + })); + } + } + + match MutableItem::from_dht_message(target, &k, v, seq, &sig, salt) { + Ok(item) => { + self.mutable_values.put(target, item); + + MessageType::Response(ResponseSpecific::Ping(PingResponseArguments { + responder_id: *routing_table.id(), + })) + } + Err(error) => { + debug!(?target, ?requester_id, ?from, ?error, "Invalid signature"); + + MessageType::Error(ErrorSpecific { + code: 206, + description: "Invalid signature".to_string(), + }) + } + } + } + }, + }) + } + + /// Handle get mutable request + fn handle_get_mutable( + &mut self, + routing_table: &RoutingTable, + from: SocketAddrV4, + target: Id, + seq: Option, + ) -> ResponseSpecific { + match self.mutable_values.get(&target) { + Some(item) => { + let no_more_recent_values = seq.map(|request_seq| item.seq() <= request_seq); + + match no_more_recent_values { + Some(true) => { + ResponseSpecific::NoMoreRecentValue(NoMoreRecentValueResponseArguments { + responder_id: *routing_table.id(), + token: self.tokens.generate_token(from).into(), + nodes: Some(routing_table.closest(target)), + seq: item.seq(), + }) + } + _ => ResponseSpecific::GetMutable(GetMutableResponseArguments { + responder_id: *routing_table.id(), + token: self.tokens.generate_token(from).into(), + nodes: Some(routing_table.closest(target)), + v: item.value().into(), + k: *item.key(), + seq: item.seq(), + sig: *item.signature(), + }), + } + } + None => ResponseSpecific::NoValues(NoValuesResponseArguments { + responder_id: *routing_table.id(), + token: self.tokens.generate_token(from).into(), + nodes: Some(routing_table.closest(target)), + }), + } + } +} diff --git a/vendor/mainline/src/rpc/server/peers.rs b/vendor/mainline/src/rpc/server/peers.rs new file mode 100644 index 0000000..4a4dbc5 --- /dev/null +++ b/vendor/mainline/src/rpc/server/peers.rs @@ -0,0 +1,176 @@ +//! Manage announced peers for info_hashes + +use std::{net::SocketAddrV4, num::NonZeroUsize}; + +use crate::common::Id; + +use lru::LruCache; + +const CHANCE_SCALE: f32 = 2.0 * (1u32 << 31) as f32; + +#[derive(Debug, Clone)] +/// An LRU cache of "Peers" per info hashes. +/// +/// Read [BEP_0005](https://www.bittorrent.org/beps/bep_0005.html) for more information. +pub struct PeersStore { + info_hashes: LruCache>, + max_peers: NonZeroUsize, +} + +impl PeersStore { + /// Create a new store of peers announced on info hashes. + pub fn new(max_info_hashes: NonZeroUsize, max_peers: NonZeroUsize) -> Self { + Self { + info_hashes: LruCache::new(max_info_hashes), + max_peers, + } + } + + /// Add a peer for an info hash. + pub fn add_peer(&mut self, info_hash: Id, peer: (&Id, SocketAddrV4)) { + if let Some(info_hash_lru) = self.info_hashes.get_mut(&info_hash) { + info_hash_lru.put(*peer.0, peer.1); + } else { + let mut info_hash_lru = LruCache::new(self.max_peers); + info_hash_lru.put(*peer.0, peer.1); + self.info_hashes.put(info_hash, info_hash_lru); + }; + } + + /// Returns a random set of peers per an info hash. + pub fn get_random_peers(&mut self, info_hash: &Id) -> Option> { + if let Some(info_hash_lru) = self.info_hashes.get(info_hash) { + let size = info_hash_lru.len(); + let target_size = 10; + + if size == 0 { + return None; + } + if size < target_size { + return Some( + info_hash_lru + .iter() + .map(|n| n.1.to_owned()) + .collect::>(), + ); + } + + let mut results = Vec::with_capacity(10); + + let mut chunk = vec![0_u8; info_hash_lru.iter().len() * 4]; + getrandom::fill(chunk.as_mut_slice()).expect("getrandom"); + + for (index, (_, addr)) in info_hash_lru.iter().enumerate() { + // Calculate the chance of adding the current item based on remaining items and slots + let remaining_slots = target_size - results.len(); + let remaining_items = info_hash_lru.len() - index; + let current_chance = + ((remaining_slots as f32 / remaining_items as f32) * CHANCE_SCALE) as u32; + + // Get random integer from the chunk + let rand_int = + u32::from_le_bytes(chunk[index..index + 4].try_into().expect("infallible")); + + // Randomly decide to add the item based on the current chance + if rand_int < current_chance { + results.push(*addr); + if results.len() == target_size { + break; + } + } + } + + return Some(results); + } + + None + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn max_info_hashes() { + let mut store = PeersStore::new( + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(100).unwrap(), + ); + + let info_hash_a = Id::random(); + let info_hash_b = Id::random(); + + store.add_peer( + info_hash_a, + (&info_hash_a, SocketAddrV4::new([127, 0, 1, 1].into(), 0)), + ); + store.add_peer( + info_hash_b, + (&info_hash_b, SocketAddrV4::new([127, 0, 1, 1].into(), 0)), + ); + + assert_eq!(store.info_hashes.len(), 1); + assert_eq!( + store.get_random_peers(&info_hash_b), + Some([SocketAddrV4::new([127, 0, 1, 1].into(), 0)].into()) + ); + } + + #[test] + fn all_peers() { + let mut store = + PeersStore::new(NonZeroUsize::new(1).unwrap(), NonZeroUsize::new(2).unwrap()); + + let info_hash_a = Id::random(); + let info_hash_b = Id::random(); + let info_hash_c = Id::random(); + + store.add_peer( + info_hash_a, + (&info_hash_a, SocketAddrV4::new([127, 0, 1, 1].into(), 0)), + ); + store.add_peer( + info_hash_a, + (&info_hash_b, SocketAddrV4::new([127, 0, 1, 2].into(), 0)), + ); + store.add_peer( + info_hash_a, + (&info_hash_c, SocketAddrV4::new([127, 0, 1, 3].into(), 0)), + ); + + assert_eq!( + store.get_random_peers(&info_hash_a), + Some( + [ + SocketAddrV4::new([127, 0, 1, 3].into(), 0), + SocketAddrV4::new([127, 0, 1, 2].into(), 0), + ] + .into() + ) + ); + } + + #[test] + fn random_peers_subset() { + let mut store = PeersStore::new( + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(200).unwrap(), + ); + + let info_hash = Id::random(); + + for i in 0..200 { + store.add_peer( + info_hash, + (&Id::random(), SocketAddrV4::new([127, 0, 1, i].into(), 0)), + ) + } + + assert_eq!(store.info_hashes.get(&info_hash).unwrap().len(), 200); + + let sample = store.get_random_peers(&info_hash).unwrap(); + + assert_eq!(sample.len(), 10); + } +} diff --git a/vendor/mainline/src/rpc/server/tokens.rs b/vendor/mainline/src/rpc/server/tokens.rs new file mode 100644 index 0000000..aca8721 --- /dev/null +++ b/vendor/mainline/src/rpc/server/tokens.rs @@ -0,0 +1,119 @@ +//! Manage tokens for remote client IPs. + +use crc::{Crc, CRC_32_ISCSI}; +use std::{ + fmt::{self, Debug, Formatter}, + net::SocketAddrV4, + time::Instant, +}; + +use tracing::trace; + +const SECRET_SIZE: usize = 20; +const TOKEN_SIZE: usize = 4; +const CASTAGNOLI: Crc = Crc::::new(&CRC_32_ISCSI); + +/// Tokens generator. +/// +/// Read [BEP_0005](https://www.bittorrent.org/beps/bep_0005.html) for more information. +#[derive(Clone)] +pub struct Tokens { + prev_secret: [u8; SECRET_SIZE], + curr_secret: [u8; SECRET_SIZE], + last_updated: Instant, +} + +impl Debug for Tokens { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Tokens (_)") + } +} + +impl Tokens { + /// Create a Tokens generator. + pub fn new() -> Self { + Tokens { + prev_secret: random(), + curr_secret: random(), + last_updated: Instant::now(), + } + } + + // === Public Methods === + + /// Returns `true` if the current secret needs to be updated after an interval. + pub fn should_update(&self) -> bool { + self.last_updated.elapsed() > crate::common::TOKEN_ROTATE_INTERVAL + } + + /// Validate that the token was generated within the past 10 minutes + pub fn validate(&mut self, address: SocketAddrV4, token: &[u8]) -> bool { + let prev = self.internal_generate_token(address, self.prev_secret); + let curr = self.internal_generate_token(address, self.curr_secret); + + token == curr || token == prev + } + + /// Rotate the tokens secret. + pub fn rotate(&mut self) { + trace!("Rotating secrets"); + + self.prev_secret = self.curr_secret; + self.curr_secret = random(); + + self.last_updated = Instant::now(); + } + + /// Generates a new token for a remote peer. + pub fn generate_token(&mut self, address: SocketAddrV4) -> [u8; 4] { + self.internal_generate_token(address, self.curr_secret) + } + + // === Private Methods === + + fn internal_generate_token( + &mut self, + address: SocketAddrV4, + secret: [u8; SECRET_SIZE], + ) -> [u8; TOKEN_SIZE] { + let mut digest = CASTAGNOLI.digest(); + + let octets: Box<[u8]> = address.ip().octets().into(); + + digest.update(&octets); + digest.update(&secret); + + let checksum = digest.finalize(); + + checksum.to_be_bytes() + } +} + +impl Default for Tokens { + fn default() -> Self { + Self::new() + } +} + +fn random() -> [u8; SECRET_SIZE] { + let mut bytes = [0_u8; SECRET_SIZE]; + getrandom::fill(&mut bytes).expect("getrandom"); + + bytes +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn valid_tokens() { + let mut tokens = Tokens::new(); + + let address = SocketAddrV4::new([127, 0, 0, 1].into(), 6881); + let token = tokens.generate_token(address); + + assert!(tokens.validate(address, &token)) + } +} diff --git a/vendor/mainline/src/rpc/socket.rs b/vendor/mainline/src/rpc/socket.rs new file mode 100644 index 0000000..55c5e13 --- /dev/null +++ b/vendor/mainline/src/rpc/socket.rs @@ -0,0 +1,428 @@ +//! UDP socket layer managing incoming/outgoing requests and responses. + +mod inflight_requests; +use crate::common::{ErrorSpecific, Message, MessageType, RequestSpecific, ResponseSpecific}; +use inflight_requests::InflightRequests; +use std::io::ErrorKind; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; +use std::time::{Duration, Instant}; +use tracing::{debug, trace, warn}; + +use super::config::Config; + +const VERSION: [u8; 4] = [82, 83, 0, 5]; // "RS" version 05 +const MTU: usize = 2048; + +pub const DEFAULT_PORT: u16 = 6881; +/// Default request timeout before abandoning an inflight request to a non-responding node. +pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_millis(2000); // 2 seconds + +pub const READ_TIMEOUT: Duration = Duration::from_millis(50); + +/// Cleanup interval for expired inflight requests to avoid overhead on every recv +const INFLIGHT_CLEANUP_INTERVAL: Duration = Duration::from_millis(200); + +/// A UdpSocket wrapper that formats and correlates DHT requests and responses. +#[derive(Debug)] +pub struct KrpcSocket { + next_tid: u32, + socket: UdpSocket, + pub(crate) server_mode: bool, + inflight_requests: InflightRequests, + last_cleanup: Instant, + local_addr: SocketAddrV4, + // poll_interval: Duration, +} + +impl KrpcSocket { + pub(crate) fn new(config: &Config) -> Result { + let request_timeout = config.request_timeout; + let port = config.port; + let bind_addr = config.bind_address.unwrap_or(Ipv4Addr::UNSPECIFIED); + + let socket = if let Some(port) = port { + UdpSocket::bind(SocketAddr::from((bind_addr, port)))? + } else { + match UdpSocket::bind(SocketAddr::from((bind_addr, DEFAULT_PORT))) { + Ok(socket) => Ok(socket), + Err(_) => UdpSocket::bind(SocketAddr::from((bind_addr, 0))), + }? + }; + + let local_addr = match socket.local_addr()? { + SocketAddr::V4(addr) => addr, + SocketAddr::V6(_) => unimplemented!("KrpcSocket does not support Ipv6"), + }; + + socket.set_read_timeout(Some(READ_TIMEOUT))?; + + Ok(Self { + socket, + next_tid: 0, + server_mode: config.server_mode, + inflight_requests: InflightRequests::new(request_timeout), + last_cleanup: Instant::now(), + local_addr, + }) + } + + #[cfg(test)] + pub(crate) fn server() -> Result { + Self::new(&Config { + server_mode: true, + bind_address: Some(Ipv4Addr::LOCALHOST), + ..Default::default() + }) + } + + #[cfg(test)] + pub(crate) fn client() -> Result { + Self::new(&Config { + bind_address: Some(Ipv4Addr::LOCALHOST), + ..Default::default() + }) + } + + // === Getters === + + /// Returns the address the server is listening to. + #[inline] + pub fn local_addr(&self) -> SocketAddrV4 { + self.local_addr + } + + // === Public Methods === + + /// Returns true if this message's transaction_id is still inflight + pub fn inflight(&self, transaction_id: u32) -> bool { + self.inflight_requests.contains(transaction_id) + } + + /// Send a request to the given address and return the transaction_id + pub fn request(&mut self, address: SocketAddrV4, request: RequestSpecific) -> u32 { + let message = self.request_message(request); + trace!(context = "socket_message_sending", message = ?message); + + let tid = message.transaction_id; + self.inflight_requests.add(tid, address); + let _ = self.send(address, message).map_err(|e| { + debug!(?e, "Error sending request message"); + }); + tid + } + + /// Send a response to the given address. + pub fn response( + &mut self, + address: SocketAddrV4, + transaction_id: u32, + response: ResponseSpecific, + ) { + let message = + self.response_message(MessageType::Response(response), address, transaction_id); + trace!(context = "socket_message_sending", message = ?message); + let _ = self.send(address, message).map_err(|e| { + debug!(?e, "Error sending response message"); + }); + } + + /// Send an error to the given address. + pub fn error(&mut self, address: SocketAddrV4, transaction_id: u32, error: ErrorSpecific) { + let message = self.response_message(MessageType::Error(error), address, transaction_id); + let _ = self.send(address, message).map_err(|e| { + debug!(?e, "Error sending error message"); + }); + } + + /// Receives a single krpc message on the socket. + /// On success, returns the dht message and the origin. + pub fn recv_from(&mut self) -> Option<(Message, SocketAddrV4)> { + let mut buf = [0u8; MTU]; + + let now = Instant::now(); + if now.duration_since(self.last_cleanup) > INFLIGHT_CLEANUP_INTERVAL { + self.last_cleanup = now; + self.inflight_requests.cleanup(); + } + + match self.socket.recv_from(&mut buf) { + Ok((amt, SocketAddr::V4(from))) => { + let bytes = &buf[..amt]; + + if from.port() == 0 { + trace!( + context = "socket_validation", + message = "Response from port 0" + ); + return None; + } + + match Message::from_bytes(bytes) { + Ok(message) => { + let should_return = match message.message_type { + MessageType::Request(_) => { + trace!( + context = "socket_message_receiving", + ?message, + ?from, + "Received request message" + ); + true + } + MessageType::Response(_) => { + trace!( + context = "socket_message_receiving", + ?message, + ?from, + "Received response message" + ); + self.is_expected_response(&message, &from) + } + MessageType::Error(_) => { + trace!( + context = "socket_message_receiving", + ?message, + ?from, + "Received error message" + ); + self.is_expected_response(&message, &from) + } + }; + + if should_return { + return Some((message, from)); + } + } + Err(error) => { + trace!( + context = "socket_error", + ?error, + ?from, + message = ?String::from_utf8_lossy(bytes), + "Received invalid Bencode message." + ); + } + } + } + Ok((_, SocketAddr::V6(_))) => { + trace!( + context = "socket_validation", + message = "Received IPv6 packet" + ); + } + Err(error) => match error.kind() { + // A read timeout means there was no packet this tick. Unix + // generally returns WouldBlock; Windows returns TimedOut. + ErrorKind::WouldBlock | ErrorKind::TimedOut => {} + _ => { + warn!("IO error {error}") + } + }, + } + + None + } + + // === Private Methods === + + fn is_expected_response(&mut self, message: &Message, from: &SocketAddrV4) -> bool { + // Find and remove the matching inflight request + if let Some(_request) = self.inflight_requests.remove(message.transaction_id, from) { + return true; + } else { + trace!( + context = "socket_validation", + message = "Unexpected response id or wrong address" + ); + } + false + } + + /// Increments self.next_tid and returns the previous value. + fn tid(&mut self) -> u32 { + // We don't bother much with reusing freed transaction ids, + // since the timeout is so short we are unlikely to run out + // of 4294967295 ids in 2 seconds. + let tid = self.next_tid; + self.next_tid = self.next_tid.wrapping_add(1); + tid + } + + /// Set transactin_id, version and read_only + fn request_message(&mut self, message: RequestSpecific) -> Message { + let transaction_id = self.tid(); + + Message { + transaction_id, + message_type: MessageType::Request(message), + version: Some(VERSION), + read_only: !self.server_mode, + requester_ip: None, + } + } + + /// Same as request_message but with request transaction_id and the requester_ip. + fn response_message( + &mut self, + message: MessageType, + requester_ip: SocketAddrV4, + request_tid: u32, + ) -> Message { + Message { + transaction_id: request_tid, + message_type: message, + version: Some(VERSION), + read_only: !self.server_mode, + // BEP_0042 Only relevant in responses. + requester_ip: Some(requester_ip), + } + } + + /// Send a raw dht message + fn send(&mut self, address: SocketAddrV4, message: Message) -> Result<(), SendMessageError> { + self.socket.send_to(&message.to_bytes()?, address)?; + trace!(context = "socket_message_sending", message = ?message); + Ok(()) + } +} + +#[derive(thiserror::Error, Debug)] +/// Mainline crate error enum. +pub enum SendMessageError { + /// Errors related to parsing DHT messages. + #[error("Failed to parse packet bytes: {0}")] + BencodeError(#[from] serde_bencode::Error), + + #[error(transparent)] + /// Transparent [std::io::Error] + IO(#[from] std::io::Error), +} + +#[cfg(test)] +mod test { + use std::thread; + + use crate::common::{Id, PingResponseArguments, RequestTypeSpecific}; + + use super::*; + + #[test] + fn tid() { + let mut socket = KrpcSocket::server().unwrap(); + + assert_eq!(socket.tid(), 0); + assert_eq!(socket.tid(), 1); + assert_eq!(socket.tid(), 2); + + socket.next_tid = u32::MAX; + + assert_eq!(socket.tid(), 4294967295); + assert_eq!(socket.tid(), 0); + } + + #[test] + fn recv_request() { + let mut server = KrpcSocket::server().unwrap(); + let server_address = server.local_addr(); + + let mut client = KrpcSocket::client().unwrap(); + client.next_tid = 120; + + let client_address = client.local_addr(); + let request = RequestSpecific { + requester_id: Id::random(), + request_type: RequestTypeSpecific::Ping, + }; + + let expected_request = request.clone(); + + let server_thread = thread::spawn(move || loop { + if let Some((message, from)) = server.recv_from() { + assert_eq!(from.port(), client_address.port()); + assert_eq!(message.transaction_id, 120); + assert!(message.read_only, "Read-only should be true"); + assert_eq!(message.version, Some(VERSION), "Version should be 'RS'"); + assert_eq!(message.message_type, MessageType::Request(expected_request)); + break; + } + }); + + client.request(server_address, request); + + server_thread.join().unwrap(); + } + + #[test] + fn recv_response() { + let (tx, rx) = flume::bounded(1); + + let mut client = KrpcSocket::client().unwrap(); + let client_address = client.local_addr(); + + let responder_id = Id::random(); + let response = ResponseSpecific::Ping(PingResponseArguments { responder_id }); + + let server_thread = thread::spawn(move || { + let mut server = KrpcSocket::client().unwrap(); + let server_address = server.local_addr(); + tx.send(server_address).unwrap(); + + loop { + server.inflight_requests.add(8, client_address); + + if let Some((message, from)) = server.recv_from() { + assert_eq!(from.port(), client_address.port()); + assert_eq!(message.transaction_id, 8); + assert!(message.read_only, "Read-only should be true"); + assert_eq!(message.version, Some(VERSION), "Version should be 'RS'"); + assert_eq!( + message.message_type, + MessageType::Response(ResponseSpecific::Ping(PingResponseArguments { + responder_id, + })) + ); + break; + } + } + }); + + let server_address = rx.recv().unwrap(); + + client.response(server_address, 8, response); + + server_thread.join().unwrap(); + } + + #[test] + fn ignore_response_from_wrong_address() { + let mut server = KrpcSocket::client().unwrap(); + let server_address = server.local_addr(); + + let mut client = KrpcSocket::client().unwrap(); + + let client_address = client.local_addr(); + + server.inflight_requests.add( + 8, + SocketAddrV4::new([127, 0, 0, 1].into(), client_address.port() + 1), + ); + + let response = ResponseSpecific::Ping(PingResponseArguments { + responder_id: Id::random(), + }); + + let _ = response.clone(); + + let server_thread = thread::spawn(move || { + thread::sleep(Duration::from_millis(5)); + assert!( + server.recv_from().is_none(), + "Should not receive a response from wrong address" + ); + }); + + client.response(server_address, 8, response); + + server_thread.join().unwrap(); + } +} diff --git a/vendor/mainline/src/rpc/socket/inflight_requests.rs b/vendor/mainline/src/rpc/socket/inflight_requests.rs new file mode 100644 index 0000000..7ae5969 --- /dev/null +++ b/vendor/mainline/src/rpc/socket/inflight_requests.rs @@ -0,0 +1,86 @@ +use std::collections::BTreeMap; +use std::net::SocketAddrV4; +use std::time::{Duration, Instant}; + +#[derive(Debug, Clone)] +pub struct InflightRequest { + pub to: SocketAddrV4, + pub sent_at: Instant, +} + +impl InflightRequest { + pub fn does_match(&self, socket: &SocketAddrV4) -> bool { + if self.to.port() != socket.port() { + return false; + } + + if self.to.ip().is_unspecified() { + return true; + } + + self.to.ip() == socket.ip() + } +} + +#[derive(Debug)] +pub struct InflightRequests { + // BTreeMap provides O(log n) lookup, insertion, and deletion keyed by transaction_id. + requests: BTreeMap, + timeout: Duration, +} + +impl InflightRequests { + pub fn new(timeout: Duration) -> Self { + Self { + requests: BTreeMap::new(), + timeout, + } + } + + /// Add a new inflight request O(log n) + pub fn add(&mut self, transaction_id: u32, to: SocketAddrV4) { + self.requests.insert( + transaction_id, + InflightRequest { + to, + sent_at: Instant::now(), + }, + ); + } + + /// Check if a transaction_id is still inflight and not expired O(log n) + pub fn contains(&self, transaction_id: u32) -> bool { + if let Some(request) = self.requests.get(&transaction_id) { + return request.sent_at.elapsed() < self.timeout; + } + false + } + + /// Remove inflight request by transaction_id if it exists and matches the address + /// O(log n) + pub fn remove(&mut self, transaction_id: u32, from: &SocketAddrV4) -> Option { + let request = self.requests.get(&transaction_id)?; + + // Drop immediately if expired; avoid accepting late responses + if request.sent_at.elapsed() >= self.timeout { + self.requests.remove(&transaction_id); + return None; + } + + if !request.does_match(from) { + return None; + } + + self.requests.remove(&transaction_id) + } + + /// Cleanup expired requests based on timeout + /// O(n) scans all requests to remove expired ones + pub fn cleanup(&mut self) { + let now = Instant::now(); + let cutoff = now - self.timeout; + + // Remove expired requests in a single pass using retain + self.requests.retain(|_, request| request.sent_at > cutoff); + } +}