From dc4523ede54eea5c0f5ff5eac238c000296c6040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 21:24:43 +0200 Subject: [PATCH 01/46] udp: start work on HMAC connection ID generation and validation --- Cargo.lock | 37 +++++++++++++++++- aquatic_udp/Cargo.toml | 2 + aquatic_udp/src/common.rs | 79 ++++++++++++++++++++++++++++++++++++++- aquatic_udp/src/lib.rs | 19 +++++----- 4 files changed, 124 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b67af2a..e00690c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,8 +209,10 @@ dependencies = [ "aquatic_common", "aquatic_toml_config", "aquatic_udp_protocol", + "blake3", "cfg-if", "crossbeam-channel", + "getrandom", "hex", "log", "mimalloc", @@ -350,6 +352,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + [[package]] name = "arrayvec" version = "0.4.12" @@ -359,6 +367,12 @@ dependencies = [ "nodrop", ] +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "async-trait" version = "0.1.53" @@ -524,6 +538,20 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "303cec55cd9c5fde944b061b902f142b52a8bb5438cc822481ea1e3ebc96bbcb" +[[package]] +name = "blake3" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" +dependencies = [ + "arrayref", + "arrayvec 0.7.2", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.3", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -655,6 +683,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "cpufeatures" version = "0.2.2" @@ -854,6 +888,7 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer 0.10.2", "crypto-common", + "subtle", ] [[package]] @@ -1756,7 +1791,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ - "arrayvec", + "arrayvec 0.4.12", "itoa 0.4.8", ] diff --git a/aquatic_udp/Cargo.toml b/aquatic_udp/Cargo.toml index d4d5922..304806f 100644 --- a/aquatic_udp/Cargo.toml +++ b/aquatic_udp/Cargo.toml @@ -24,8 +24,10 @@ aquatic_toml_config = { version = "0.2.0", path = "../aquatic_toml_config" } aquatic_udp_protocol = { version = "0.2.0", path = "../aquatic_udp_protocol" } anyhow = "1" +blake3 = { version = "1", features = ["traits-preview"] } cfg-if = "1" crossbeam-channel = "0.5" +getrandom = "0.2" hex = "0.4" log = "0.4" mimalloc = { version = "0.1", default-features = false } diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 36affe7..082e036 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -1,19 +1,94 @@ use std::collections::BTreeMap; use std::hash::Hash; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::atomic::AtomicUsize; use std::sync::Arc; +use std::time::{Duration, Instant}; -use aquatic_common::CanonicalSocketAddr; use crossbeam_channel::{Sender, TrySendError}; +use getrandom::getrandom; use aquatic_common::access_list::AccessListArcSwap; +use aquatic_common::CanonicalSocketAddr; use aquatic_udp_protocol::*; use crate::config::Config; pub const MAX_PACKET_SIZE: usize = 8192; +pub struct ConnectionIdHandler { + start_time: Instant, + max_connection_age: Duration, + hmac: blake3::Hasher, +} + +impl ConnectionIdHandler { + pub fn new(config: &Config) -> anyhow::Result { + let mut key = [0; 32]; + + getrandom(&mut key)?; + + let hmac = blake3::Hasher::new_keyed(&key); + + let start_time = Instant::now(); + let max_connection_age = Duration::from_secs(config.cleaning.max_connection_age); + + Ok(Self { + hmac, + start_time, + max_connection_age, + }) + } + + pub fn create_connection_id(&mut self, source_ip: IpAddr) -> ConnectionId { + // Seconds elapsed since server start, as bytes + let elapsed_time_bytes = (self.start_time.elapsed().as_secs() as u32).to_ne_bytes(); + + self.create_connection_id_inner(elapsed_time_bytes, source_ip) + } + + pub fn connection_id_valid(&mut self, source_ip: IpAddr, connection_id: ConnectionId) -> bool { + let elapsed_time_bytes = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); + + // i64 comparison should be constant-time + let hmac_valid = + connection_id == self.create_connection_id_inner(elapsed_time_bytes, source_ip); + + if !hmac_valid { + return false; + } + + let connection_elapsed_since_start = + Duration::from_secs(u32::from_ne_bytes(elapsed_time_bytes) as u64); + + connection_elapsed_since_start + self.max_connection_age > self.start_time.elapsed() + } + + fn create_connection_id_inner( + &mut self, + elapsed_time_bytes: [u8; 4], + source_ip: IpAddr, + ) -> ConnectionId { + // The first 4 bytes is the elapsed time since server start in seconds. The last 4 is a + // truncated message authentication code. + let mut connection_id_bytes = [0u8; 8]; + + (&mut connection_id_bytes[..4]).copy_from_slice(&elapsed_time_bytes); + + self.hmac.update(&elapsed_time_bytes); + + match source_ip { + IpAddr::V4(ip) => self.hmac.update(&ip.octets()), + IpAddr::V6(ip) => self.hmac.update(&ip.octets()), + }; + + self.hmac.finalize_xof().fill(&mut connection_id_bytes[4..]); + self.hmac.reset(); + + ConnectionId(i64::from_ne_bytes(connection_id_bytes)) + } +} + #[derive(Debug)] pub struct PendingScrapeRequest { pub slab_key: usize, diff --git a/aquatic_udp/src/lib.rs b/aquatic_udp/src/lib.rs index db644b9..ef436d0 100644 --- a/aquatic_udp/src/lib.rs +++ b/aquatic_udp/src/lib.rs @@ -2,25 +2,24 @@ pub mod common; pub mod config; pub mod workers; -use aquatic_common::PanicSentinelWatcher; -use config::Config; - use std::collections::BTreeMap; use std::thread::Builder; use anyhow::Context; -#[cfg(feature = "cpu-pinning")] -use aquatic_common::cpu_pinning::{pin_current_if_configured_to, WorkerIndex}; -use aquatic_common::privileges::PrivilegeDropper; use crossbeam_channel::{bounded, unbounded}; - -use aquatic_common::access_list::update_access_list; use signal_hook::consts::{SIGTERM, SIGUSR1}; use signal_hook::iterator::Signals; -use common::{ConnectedRequestSender, ConnectedResponseSender, SocketWorkerIndex, State}; +use aquatic_common::access_list::update_access_list; +#[cfg(feature = "cpu-pinning")] +use aquatic_common::cpu_pinning::{pin_current_if_configured_to, WorkerIndex}; +use aquatic_common::privileges::PrivilegeDropper; +use aquatic_common::PanicSentinelWatcher; -use crate::common::RequestWorkerIndex; +use common::{ + ConnectedRequestSender, ConnectedResponseSender, RequestWorkerIndex, SocketWorkerIndex, State, +}; +use config::Config; pub const APP_NAME: &str = "aquatic_udp: UDP BitTorrent tracker"; pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); From 8b70034900cb27caae04276b457fd853b1db136b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 22:27:45 +0200 Subject: [PATCH 02/46] udp: use hmac ConnectionValidator in socket workers --- aquatic_udp/src/common.rs | 21 ++++++---- aquatic_udp/src/lib.rs | 7 +++- aquatic_udp/src/workers/socket.rs | 67 +++++-------------------------- 3 files changed, 28 insertions(+), 67 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 082e036..0384177 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -16,13 +16,14 @@ use crate::config::Config; pub const MAX_PACKET_SIZE: usize = 8192; -pub struct ConnectionIdHandler { +#[derive(Clone)] +pub struct ConnectionValidator { start_time: Instant, max_connection_age: Duration, hmac: blake3::Hasher, } -impl ConnectionIdHandler { +impl ConnectionValidator { pub fn new(config: &Config) -> anyhow::Result { let mut key = [0; 32]; @@ -40,19 +41,23 @@ impl ConnectionIdHandler { }) } - pub fn create_connection_id(&mut self, source_ip: IpAddr) -> ConnectionId { + pub fn create_connection_id(&mut self, source_addr: CanonicalSocketAddr) -> ConnectionId { // Seconds elapsed since server start, as bytes let elapsed_time_bytes = (self.start_time.elapsed().as_secs() as u32).to_ne_bytes(); - self.create_connection_id_inner(elapsed_time_bytes, source_ip) + self.create_connection_id_inner(elapsed_time_bytes, source_addr) } - pub fn connection_id_valid(&mut self, source_ip: IpAddr, connection_id: ConnectionId) -> bool { + pub fn connection_id_valid( + &mut self, + source_addr: CanonicalSocketAddr, + connection_id: ConnectionId, + ) -> bool { let elapsed_time_bytes = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); // i64 comparison should be constant-time let hmac_valid = - connection_id == self.create_connection_id_inner(elapsed_time_bytes, source_ip); + connection_id == self.create_connection_id_inner(elapsed_time_bytes, source_addr); if !hmac_valid { return false; @@ -67,7 +72,7 @@ impl ConnectionIdHandler { fn create_connection_id_inner( &mut self, elapsed_time_bytes: [u8; 4], - source_ip: IpAddr, + source_addr: CanonicalSocketAddr, ) -> ConnectionId { // The first 4 bytes is the elapsed time since server start in seconds. The last 4 is a // truncated message authentication code. @@ -77,7 +82,7 @@ impl ConnectionIdHandler { self.hmac.update(&elapsed_time_bytes); - match source_ip { + match source_addr.get().ip() { IpAddr::V4(ip) => self.hmac.update(&ip.octets()), IpAddr::V6(ip) => self.hmac.update(&ip.octets()), }; diff --git a/aquatic_udp/src/lib.rs b/aquatic_udp/src/lib.rs index ef436d0..864104e 100644 --- a/aquatic_udp/src/lib.rs +++ b/aquatic_udp/src/lib.rs @@ -17,7 +17,8 @@ use aquatic_common::privileges::PrivilegeDropper; use aquatic_common::PanicSentinelWatcher; use common::{ - ConnectedRequestSender, ConnectedResponseSender, RequestWorkerIndex, SocketWorkerIndex, State, + ConnectedRequestSender, ConnectedResponseSender, ConnectionValidator, RequestWorkerIndex, + SocketWorkerIndex, State, }; use config::Config; @@ -31,6 +32,8 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { let mut signals = Signals::new([SIGUSR1, SIGTERM])?; + let connection_validator = ConnectionValidator::new(&config)?; + let (sentinel_watcher, sentinel) = PanicSentinelWatcher::create_with_sentinel(); let priv_dropper = PrivilegeDropper::new(config.privileges.clone(), config.socket_workers); @@ -96,6 +99,7 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { let sentinel = sentinel.clone(); let state = state.clone(); let config = config.clone(); + let connection_validator = connection_validator.clone(); let request_sender = ConnectedRequestSender::new(SocketWorkerIndex(i), request_senders.clone()); let response_receiver = response_receivers.remove(&i).unwrap(); @@ -117,6 +121,7 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { state, config, i, + connection_validator, request_sender, response_receiver, priv_dropper, diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index c8b5e05..d67fa40 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -9,7 +9,6 @@ use aquatic_common::privileges::PrivilegeDropper; use crossbeam_channel::Receiver; use mio::net::UdpSocket; use mio::{Events, Interest, Poll, Token}; -use rand::prelude::{Rng, SeedableRng, StdRng}; use slab::Slab; use aquatic_common::access_list::create_access_list_cache; @@ -22,31 +21,6 @@ use socket2::{Domain, Protocol, Socket, Type}; use crate::common::*; use crate::config::Config; -#[derive(Default)] -pub struct ConnectionMap(AmortizedIndexMap<(ConnectionId, CanonicalSocketAddr), ValidUntil>); - -impl ConnectionMap { - pub fn insert( - &mut self, - connection_id: ConnectionId, - socket_addr: CanonicalSocketAddr, - valid_until: ValidUntil, - ) { - self.0.insert((connection_id, socket_addr), valid_until); - } - - pub fn contains(&self, connection_id: ConnectionId, socket_addr: CanonicalSocketAddr) -> bool { - self.0.contains_key(&(connection_id, socket_addr)) - } - - pub fn clean(&mut self) { - let now = Instant::now(); - - self.0.retain(|_, v| v.0 > now); - self.0.shrink_to_fit(); - } -} - #[derive(Debug)] pub struct PendingScrapeResponseSlabEntry { num_pending: usize, @@ -155,11 +129,11 @@ pub fn run_socket_worker( state: State, config: Config, token_num: usize, + mut connection_validator: ConnectionValidator, request_sender: ConnectedRequestSender, response_receiver: Receiver<(ConnectedResponse, CanonicalSocketAddr)>, priv_dropper: PrivilegeDropper, ) { - let mut rng = StdRng::from_entropy(); let mut buffer = [0u8; MAX_PACKET_SIZE]; let mut socket = @@ -173,7 +147,6 @@ pub fn run_socket_worker( .unwrap(); let mut events = Events::with_capacity(config.network.poll_event_capacity); - let mut connections = ConnectionMap::default(); let mut pending_scrape_responses = PendingScrapeResponseSlab::default(); let mut access_list_cache = create_access_list_cache(&state.access_list); @@ -181,15 +154,10 @@ pub fn run_socket_worker( let poll_timeout = Duration::from_millis(config.network.poll_timeout_ms); - let connection_cleaning_duration = - Duration::from_secs(config.cleaning.connection_cleaning_interval); let pending_scrape_cleaning_duration = Duration::from_secs(config.cleaning.pending_scrape_cleaning_interval); - let mut connection_valid_until = ValidUntil::new(config.cleaning.max_connection_age); let mut pending_scrape_valid_until = ValidUntil::new(config.cleaning.max_pending_scrape_age); - - let mut last_connection_cleaning = Instant::now(); let mut last_pending_scrape_cleaning = Instant::now(); let mut iter_counter = 0usize; @@ -205,15 +173,13 @@ pub fn run_socket_worker( read_requests( &config, &state, - &mut connections, + &mut connection_validator, &mut pending_scrape_responses, &mut access_list_cache, - &mut rng, &mut socket, &mut buffer, &request_sender, &mut local_responses, - connection_valid_until, pending_scrape_valid_until, ); } @@ -233,16 +199,9 @@ pub fn run_socket_worker( if iter_counter % 128 == 0 { let now = Instant::now(); - connection_valid_until = - ValidUntil::new_with_now(now, config.cleaning.max_connection_age); pending_scrape_valid_until = ValidUntil::new_with_now(now, config.cleaning.max_pending_scrape_age); - if now > last_connection_cleaning + connection_cleaning_duration { - connections.clean(); - - last_connection_cleaning = now; - } if now > last_pending_scrape_cleaning + pending_scrape_cleaning_duration { pending_scrape_responses.clean(); @@ -258,15 +217,13 @@ pub fn run_socket_worker( fn read_requests( config: &Config, state: &State, - connections: &mut ConnectionMap, + connection_validator: &mut ConnectionValidator, pending_scrape_responses: &mut PendingScrapeResponseSlab, access_list_cache: &mut AccessListCache, - rng: &mut StdRng, socket: &mut UdpSocket, buffer: &mut [u8], request_sender: &ConnectedRequestSender, local_responses: &mut Vec<(Response, CanonicalSocketAddr)>, - connection_valid_until: ValidUntil, pending_scrape_valid_until: ValidUntil, ) { let mut requests_received_ipv4: usize = 0; @@ -297,13 +254,11 @@ fn read_requests( handle_request( config, - connections, + connection_validator, pending_scrape_responses, access_list_cache, - rng, request_sender, local_responses, - connection_valid_until, pending_scrape_valid_until, res_request, src, @@ -341,13 +296,11 @@ fn read_requests( pub fn handle_request( config: &Config, - connections: &mut ConnectionMap, + connection_validator: &mut ConnectionValidator, pending_scrape_responses: &mut PendingScrapeResponseSlab, access_list_cache: &mut AccessListCache, - rng: &mut StdRng, request_sender: &ConnectedRequestSender, local_responses: &mut Vec<(Response, CanonicalSocketAddr)>, - connection_valid_until: ValidUntil, pending_scrape_valid_until: ValidUntil, res_request: Result, src: CanonicalSocketAddr, @@ -356,9 +309,7 @@ pub fn handle_request( match res_request { Ok(Request::Connect(request)) => { - let connection_id = ConnectionId(rng.gen()); - - connections.insert(connection_id, src, connection_valid_until); + let connection_id = connection_validator.create_connection_id(src); let response = Response::Connect(ConnectResponse { connection_id, @@ -368,7 +319,7 @@ pub fn handle_request( local_responses.push((response, src)) } Ok(Request::Announce(request)) => { - if connections.contains(request.connection_id, src) { + if connection_validator.connection_id_valid(src, request.connection_id) { if access_list_cache .load() .allows(access_list_mode, &request.info_hash.0) @@ -392,7 +343,7 @@ pub fn handle_request( } } Ok(Request::Scrape(request)) => { - if connections.contains(request.connection_id, src) { + if connection_validator.connection_id_valid(src, request.connection_id) { let split_requests = pending_scrape_responses.prepare_split_requests( config, request, @@ -417,7 +368,7 @@ pub fn handle_request( err, } = err { - if connections.contains(connection_id, src) { + if connection_validator.connection_id_valid(src, connection_id) { let response = ErrorResponse { transaction_id, message: err.right_or("Parse error").into(), From 0685c9934972640331fa474a9ff5c4afadc17852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 22:31:02 +0200 Subject: [PATCH 03/46] Update TODO --- TODO.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.md b/TODO.md index ef68315..ca90597 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,9 @@ ## High priority +* aquatic_udp + * find out if hmac connection validation solution is actually secure + * aquatic_http_private * Consider not setting Content-type: text/plain for responses and send vec as default octet stream instead * stored procedure From cbcb6277720173484468c51919c175ca5d9a9901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 22:32:30 +0200 Subject: [PATCH 04/46] udp: reorder initializations in lib.rs --- aquatic_udp/src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/aquatic_udp/src/lib.rs b/aquatic_udp/src/lib.rs index 864104e..80ba09f 100644 --- a/aquatic_udp/src/lib.rs +++ b/aquatic_udp/src/lib.rs @@ -26,17 +26,15 @@ pub const APP_NAME: &str = "aquatic_udp: UDP BitTorrent tracker"; pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); pub fn run(config: Config) -> ::anyhow::Result<()> { - let state = State::new(config.request_workers); - - update_access_list(&config.access_list, &state.access_list)?; - let mut signals = Signals::new([SIGUSR1, SIGTERM])?; + let state = State::new(config.request_workers); let connection_validator = ConnectionValidator::new(&config)?; - let (sentinel_watcher, sentinel) = PanicSentinelWatcher::create_with_sentinel(); let priv_dropper = PrivilegeDropper::new(config.privileges.clone(), config.socket_workers); + update_access_list(&config.access_list, &state.access_list)?; + let mut request_senders = Vec::new(); let mut request_receivers = BTreeMap::new(); From 059ef495bf4b2ebfc01a11dfa624ab6b212101c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 22:36:42 +0200 Subject: [PATCH 05/46] udp: config: remove connection_cleaning_interval --- aquatic_udp/src/config.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aquatic_udp/src/config.rs b/aquatic_udp/src/config.rs index eb3f3d1..962b69e 100644 --- a/aquatic_udp/src/config.rs +++ b/aquatic_udp/src/config.rs @@ -163,8 +163,6 @@ impl Default for StatisticsConfig { #[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct CleaningConfig { - /// Clean connections this often (seconds) - pub connection_cleaning_interval: u64, /// Clean torrents this often (seconds) pub torrent_cleaning_interval: u64, /// Clean pending scrape responses this often (seconds) @@ -173,7 +171,7 @@ pub struct CleaningConfig { /// lingering for a long time. However, the cleaning also returns unused /// allocated memory to the OS, so the interval can be configured here. pub pending_scrape_cleaning_interval: u64, - /// Remove connections that are older than this (seconds) + /// Maximum time to use a connection token before it expires (seconds) pub max_connection_age: u64, /// Remove peers who have not announced for this long (seconds) pub max_peer_age: u64, @@ -185,7 +183,6 @@ pub struct CleaningConfig { impl Default for CleaningConfig { fn default() -> Self { Self { - connection_cleaning_interval: 60, torrent_cleaning_interval: 60 * 2, pending_scrape_cleaning_interval: 60 * 10, max_connection_age: 60 * 2, From 4203e86eca1cb7d35221e2b280cf921257c6588a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 23:40:04 +0200 Subject: [PATCH 06/46] udp: optimize/simplify ConnectionValidator --- aquatic_udp/src/common.rs | 44 +++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 0384177..bce0afe 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -3,7 +3,7 @@ use std::hash::Hash; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::atomic::AtomicUsize; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Instant; use crossbeam_channel::{Sender, TrySendError}; use getrandom::getrandom; @@ -19,7 +19,7 @@ pub const MAX_PACKET_SIZE: usize = 8192; #[derive(Clone)] pub struct ConnectionValidator { start_time: Instant, - max_connection_age: Duration, + max_connection_age: u32, hmac: blake3::Hasher, } @@ -31,21 +31,18 @@ impl ConnectionValidator { let hmac = blake3::Hasher::new_keyed(&key); - let start_time = Instant::now(); - let max_connection_age = Duration::from_secs(config.cleaning.max_connection_age); - Ok(Self { hmac, - start_time, - max_connection_age, + start_time: Instant::now(), + max_connection_age: config.cleaning.max_connection_age as u32, }) } pub fn create_connection_id(&mut self, source_addr: CanonicalSocketAddr) -> ConnectionId { - // Seconds elapsed since server start, as bytes - let elapsed_time_bytes = (self.start_time.elapsed().as_secs() as u32).to_ne_bytes(); + let valid_until = + (self.start_time.elapsed().as_secs() as u32 + self.max_connection_age).to_ne_bytes(); - self.create_connection_id_inner(elapsed_time_bytes, source_addr) + self.create_connection_id_inner(valid_until, source_addr) } pub fn connection_id_valid( @@ -53,34 +50,31 @@ impl ConnectionValidator { source_addr: CanonicalSocketAddr, connection_id: ConnectionId, ) -> bool { - let elapsed_time_bytes = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); + let valid_until = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); - // i64 comparison should be constant-time - let hmac_valid = - connection_id == self.create_connection_id_inner(elapsed_time_bytes, source_addr); - - if !hmac_valid { + // Check that recreating ConnectionId with same inputs yields identical HMAC. + // + // I expect i64 comparison to be be constant-time. + if connection_id != self.create_connection_id_inner(valid_until, source_addr) { return false; } - let connection_elapsed_since_start = - Duration::from_secs(u32::from_ne_bytes(elapsed_time_bytes) as u64); - - connection_elapsed_since_start + self.max_connection_age > self.start_time.elapsed() + u32::from_ne_bytes(valid_until) > self.start_time.elapsed().as_secs() as u32 } fn create_connection_id_inner( &mut self, - elapsed_time_bytes: [u8; 4], + valid_until: [u8; 4], source_addr: CanonicalSocketAddr, ) -> ConnectionId { - // The first 4 bytes is the elapsed time since server start in seconds. The last 4 is a - // truncated message authentication code. + // The first 4 bytes is number of seconds since server start until + // connection is no longer valid. The last 4 is the truncated message + // authentication code. let mut connection_id_bytes = [0u8; 8]; - (&mut connection_id_bytes[..4]).copy_from_slice(&elapsed_time_bytes); + (&mut connection_id_bytes[..4]).copy_from_slice(&valid_until); - self.hmac.update(&elapsed_time_bytes); + self.hmac.update(&valid_until); match source_addr.get().ip() { IpAddr::V4(ip) => self.hmac.update(&ip.octets()), From 38962eba6bddf760591f8fb520b1ae5d44ee6fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 23:42:35 +0200 Subject: [PATCH 07/46] udp: config: make max_connection_age a u32, improve its documentation --- aquatic_udp/src/common.rs | 2 +- aquatic_udp/src/config.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index bce0afe..2249961 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -34,7 +34,7 @@ impl ConnectionValidator { Ok(Self { hmac, start_time: Instant::now(), - max_connection_age: config.cleaning.max_connection_age as u32, + max_connection_age: config.cleaning.max_connection_age, }) } diff --git a/aquatic_udp/src/config.rs b/aquatic_udp/src/config.rs index 962b69e..7823ed8 100644 --- a/aquatic_udp/src/config.rs +++ b/aquatic_udp/src/config.rs @@ -171,8 +171,8 @@ pub struct CleaningConfig { /// lingering for a long time. However, the cleaning also returns unused /// allocated memory to the OS, so the interval can be configured here. pub pending_scrape_cleaning_interval: u64, - /// Maximum time to use a connection token before it expires (seconds) - pub max_connection_age: u64, + /// Allow clients to use a connection token for this long (seconds) + pub max_connection_age: u32, /// Remove peers who have not announced for this long (seconds) pub max_peer_age: u64, /// Remove pending scrape responses that have not been returned from request From 70cabfa89c450a9479a3285c4e4d52a8732c1071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 23:45:39 +0200 Subject: [PATCH 08/46] udp: ConnectionValidator: add anyhow context to getrandom call --- aquatic_udp/src/common.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 2249961..6609914 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -5,6 +5,7 @@ use std::sync::atomic::AtomicUsize; use std::sync::Arc; use std::time::Instant; +use anyhow::Context; use crossbeam_channel::{Sender, TrySendError}; use getrandom::getrandom; @@ -27,7 +28,7 @@ impl ConnectionValidator { pub fn new(config: &Config) -> anyhow::Result { let mut key = [0; 32]; - getrandom(&mut key)?; + getrandom(&mut key).with_context(|| "Couldn't get random bytes from system source")?; let hmac = blake3::Hasher::new_keyed(&key); From 38eecaeef27f3652d374fdbf24e89bfd23ada405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 13 Apr 2022 23:46:47 +0200 Subject: [PATCH 09/46] udp: remove traits-preview feature from blake3 dependency --- aquatic_udp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aquatic_udp/Cargo.toml b/aquatic_udp/Cargo.toml index 304806f..d9456d8 100644 --- a/aquatic_udp/Cargo.toml +++ b/aquatic_udp/Cargo.toml @@ -24,7 +24,7 @@ aquatic_toml_config = { version = "0.2.0", path = "../aquatic_toml_config" } aquatic_udp_protocol = { version = "0.2.0", path = "../aquatic_udp_protocol" } anyhow = "1" -blake3 = { version = "1", features = ["traits-preview"] } +blake3 = "1" cfg-if = "1" crossbeam-channel = "0.5" getrandom = "0.2" From 70414ee736fe17edbeae702d0817ad05ca9f1a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:12:16 +0200 Subject: [PATCH 10/46] udp: ConnectionValidator: improve error on failure to gen key --- aquatic_udp/src/common.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 6609914..ddb80f3 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -28,7 +28,8 @@ impl ConnectionValidator { pub fn new(config: &Config) -> anyhow::Result { let mut key = [0; 32]; - getrandom(&mut key).with_context(|| "Couldn't get random bytes from system source")?; + getrandom(&mut key) + .with_context(|| "Couldn't get random bytes for ConnectionValidator key")?; let hmac = blake3::Hasher::new_keyed(&key); From 256975a43cf1066de3c2e121c66c6cd81a2a9a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:14:38 +0200 Subject: [PATCH 11/46] udp: check whether to clean scrape response slab less often --- aquatic_udp/src/workers/socket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index d67fa40..161cf62 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -196,7 +196,7 @@ pub fn run_socket_worker( ); // Run periodic ValidUntil updates and state cleaning - if iter_counter % 128 == 0 { + if iter_counter % 256 == 0 { let now = Instant::now(); pending_scrape_valid_until = From 9479828b4a7b280444a53169b57577bb6636d4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:23:50 +0200 Subject: [PATCH 12/46] udp: PendingScrapeResponseSlab: use normal hashmap instead of amortized --- Cargo.lock | 1 + aquatic_udp/Cargo.toml | 1 + aquatic_udp/src/workers/socket.rs | 8 +++++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e00690c..2d96f21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,6 +213,7 @@ dependencies = [ "cfg-if", "crossbeam-channel", "getrandom", + "hashbrown 0.12.0", "hex", "log", "mimalloc", diff --git a/aquatic_udp/Cargo.toml b/aquatic_udp/Cargo.toml index d9456d8..aaf51c5 100644 --- a/aquatic_udp/Cargo.toml +++ b/aquatic_udp/Cargo.toml @@ -28,6 +28,7 @@ blake3 = "1" cfg-if = "1" crossbeam-channel = "0.5" getrandom = "0.2" +hashbrown = { version = "0.12", default-features = false } hex = "0.4" log = "0.4" mimalloc = { version = "0.1", default-features = false } diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index 161cf62..a571e6c 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -7,13 +7,14 @@ use std::vec::Drain; use anyhow::Context; use aquatic_common::privileges::PrivilegeDropper; use crossbeam_channel::Receiver; +use hashbrown::HashMap; use mio::net::UdpSocket; use mio::{Events, Interest, Poll, Token}; use slab::Slab; use aquatic_common::access_list::create_access_list_cache; use aquatic_common::access_list::AccessListCache; -use aquatic_common::{AmortizedIndexMap, CanonicalSocketAddr}; +use aquatic_common::CanonicalSocketAddr; use aquatic_common::{PanicSentinel, ValidUntil}; use aquatic_udp_protocol::*; use socket2::{Domain, Protocol, Socket, Type}; @@ -39,8 +40,9 @@ impl PendingScrapeResponseSlab { request: ScrapeRequest, valid_until: ValidUntil, ) -> impl IntoIterator { - let mut split_requests: AmortizedIndexMap = - Default::default(); + let capacity = config.request_workers.min(request.info_hashes.len()); + let mut split_requests: HashMap = + HashMap::with_capacity(capacity); if request.info_hashes.is_empty() { ::log::warn!( From 6cbfa468055a4cdb43c34e4964ca65f58c5bce7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:29:56 +0200 Subject: [PATCH 13/46] udp: improve code in PendingScrapeResponseSlab.clean --- aquatic_udp/src/workers/socket.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index a571e6c..b0a9b7d 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -110,18 +110,19 @@ impl PendingScrapeResponseSlab { let now = Instant::now(); self.0.retain(|k, v| { - let keep = v.valid_until.0 > now; - - if !keep { + if v.valid_until.0 > now { + true + } else { ::log::warn!( - "Removing PendingScrapeResponseSlab entry while cleaning. {:?}: {:?}", + "Unconsumed PendingScrapeResponseSlab entry. {:?}: {:?}", k, v ); - } - keep + false + } }); + self.0.shrink_to_fit(); } } From f532ec1875aa8222e64506066e4a3d742f7eb929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:32:31 +0200 Subject: [PATCH 14/46] udp: improve error handling in read_requests --- aquatic_udp/src/workers/socket.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index b0a9b7d..cec9f72 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -267,12 +267,11 @@ fn read_requests( src, ); } + Err(err) if err.kind() == ErrorKind::WouldBlock => { + break; + } Err(err) => { - if err.kind() == ErrorKind::WouldBlock { - break; - } - - ::log::info!("recv_from error: {}", err); + ::log::warn!("recv_from error: {:#}", err); } } } From dcf6ceaec0759699f4405717037df5a834f32a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:34:41 +0200 Subject: [PATCH 15/46] udp socket worker: remove dubious #[inline] hints --- aquatic_udp/src/workers/socket.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index cec9f72..0ebaa7d 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -216,7 +216,6 @@ pub fn run_socket_worker( } } -#[inline] fn read_requests( config: &Config, state: &State, @@ -383,7 +382,6 @@ pub fn handle_request( } } -#[inline] fn send_responses( state: &State, config: &Config, From 5d227428cc27a91be025072c3316255399b0b52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:38:54 +0200 Subject: [PATCH 16/46] udp: send_response: reduce branching (hopefully) --- aquatic_udp/src/workers/socket.rs | 50 ++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index 0ebaa7d..e461332 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -76,7 +76,10 @@ impl PendingScrapeResponseSlab { split_requests } - pub fn add_and_get_finished(&mut self, response: PendingScrapeResponse) -> Option { + pub fn add_and_get_finished( + &mut self, + response: PendingScrapeResponse, + ) -> Option { let finished = if let Some(entry) = self.0.get_mut(response.slab_key) { entry.num_pending -= 1; @@ -97,10 +100,10 @@ impl PendingScrapeResponseSlab { if finished { let entry = self.0.remove(response.slab_key); - Some(Response::Scrape(ScrapeResponse { + Some(ScrapeResponse { transaction_id: entry.transaction_id, torrent_stats: entry.torrent_stats.into_values().collect(), - })) + }) } else { None } @@ -396,15 +399,40 @@ fn send_responses( } for (response, addr) in response_receiver.try_iter() { - let opt_response = match response { - ConnectedResponse::Scrape(r) => pending_scrape_responses.add_and_get_finished(r), - ConnectedResponse::AnnounceIpv4(r) => Some(Response::AnnounceIpv4(r)), - ConnectedResponse::AnnounceIpv6(r) => Some(Response::AnnounceIpv6(r)), + match response { + ConnectedResponse::Scrape(r) => { + if let Some(response) = pending_scrape_responses.add_and_get_finished(r) { + send_response( + state, + config, + socket, + buffer, + Response::Scrape(response), + addr, + ); + } + } + ConnectedResponse::AnnounceIpv4(r) => { + send_response( + state, + config, + socket, + buffer, + Response::AnnounceIpv4(r), + addr, + ); + } + ConnectedResponse::AnnounceIpv6(r) => { + send_response( + state, + config, + socket, + buffer, + Response::AnnounceIpv6(r), + addr, + ); + } }; - - if let Some(response) = opt_response { - send_response(state, config, socket, buffer, response, addr); - } } } From 0f6be84576ed9046eb6507887a171fb5b69e5f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:40:17 +0200 Subject: [PATCH 17/46] udp: log with warn level and with more info if send_to fails --- aquatic_udp/src/workers/socket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index e461332..8c42927 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -488,7 +488,7 @@ fn send_response( } Ok(_) => {} Err(err) => { - ::log::info!("send_to error: {}", err); + ::log::warn!("send_to error: {:#}", err); } } } From ebe612a5601e89a4865c5f721e7e5185e74092a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:44:34 +0200 Subject: [PATCH 18/46] udp: TorrentMap cleaning: improve code, do less work --- aquatic_udp/src/workers/request.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/aquatic_udp/src/workers/request.rs b/aquatic_udp/src/workers/request.rs index 12eb6b9..d6041f9 100644 --- a/aquatic_udp/src/workers/request.rs +++ b/aquatic_udp/src/workers/request.rs @@ -98,9 +98,9 @@ impl TorrentMaps { let num_leechers = &mut torrent.num_leechers; torrent.peers.retain(|_, peer| { - let keep = peer.valid_until.0 > now; - - if !keep { + if peer.valid_until.0 > now { + true + } else { match peer.status { PeerStatus::Seeding => { *num_seeders -= 1; @@ -110,14 +110,18 @@ impl TorrentMaps { } _ => (), }; - } - keep + false + } }); - torrent.peers.shrink_to_fit(); + if torrent.peers.is_empty() { + false + } else { + torrent.peers.shrink_to_fit(); - !torrent.peers.is_empty() + true + } } } From 82e468de35f71ec98d370d5c79220e2e195996e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:49:17 +0200 Subject: [PATCH 19/46] udp: socket worker: send responses: use previous design It seems to perform better --- aquatic_udp/src/workers/socket.rs | 43 +++++++------------------------ 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index 8c42927..d3eda3c 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -399,40 +399,17 @@ fn send_responses( } for (response, addr) in response_receiver.try_iter() { - match response { - ConnectedResponse::Scrape(r) => { - if let Some(response) = pending_scrape_responses.add_and_get_finished(r) { - send_response( - state, - config, - socket, - buffer, - Response::Scrape(response), - addr, - ); - } - } - ConnectedResponse::AnnounceIpv4(r) => { - send_response( - state, - config, - socket, - buffer, - Response::AnnounceIpv4(r), - addr, - ); - } - ConnectedResponse::AnnounceIpv6(r) => { - send_response( - state, - config, - socket, - buffer, - Response::AnnounceIpv6(r), - addr, - ); - } + let opt_response = match response { + ConnectedResponse::Scrape(r) => pending_scrape_responses + .add_and_get_finished(r) + .map(Response::Scrape), + ConnectedResponse::AnnounceIpv4(r) => Some(Response::AnnounceIpv4(r)), + ConnectedResponse::AnnounceIpv6(r) => Some(Response::AnnounceIpv6(r)), }; + + if let Some(response) = opt_response { + send_response(state, config, socket, buffer, response, addr); + } } } From 10cb0849d6bb5de1aa3dfbd8fe608dbc9e6225a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 17:51:15 +0200 Subject: [PATCH 20/46] Update TODO --- TODO.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/TODO.md b/TODO.md index ca90597..ef68315 100644 --- a/TODO.md +++ b/TODO.md @@ -2,9 +2,6 @@ ## High priority -* aquatic_udp - * find out if hmac connection validation solution is actually secure - * aquatic_http_private * Consider not setting Content-type: text/plain for responses and send vec as default octet stream instead * stored procedure From 1e5b98bcf61ea8216fbd13351b4ef37f74fdb09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 22:08:57 +0200 Subject: [PATCH 21/46] udp: ConnectionValidator: add documentation --- aquatic_udp/src/common.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index ddb80f3..43b7358 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -17,6 +17,13 @@ use crate::config::Config; pub const MAX_PACKET_SIZE: usize = 8192; +/// HMAC (BLAKE3) based ConnectionID creator and validator +/// +/// Structure of created ConnectionID (bytes making up inner i64): +/// - &[0..4]: connection expiration time as number of seconds after +/// ConnectionValidator instance was created, encoded as u32 bytes +/// - &[4..8]: truncated keyed BLAKE3 hash of above 4 bytes and octets of +/// client IP address #[derive(Clone)] pub struct ConnectionValidator { start_time: Instant, @@ -25,6 +32,8 @@ pub struct ConnectionValidator { } impl ConnectionValidator { + /// Create new instance. Must be created once and cloned if used in several + /// threads. pub fn new(config: &Config) -> anyhow::Result { let mut key = [0; 32]; @@ -54,7 +63,7 @@ impl ConnectionValidator { ) -> bool { let valid_until = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); - // Check that recreating ConnectionId with same inputs yields identical HMAC. + // Check that recreating ConnectionId with same inputs yields identical hash. // // I expect i64 comparison to be be constant-time. if connection_id != self.create_connection_id_inner(valid_until, source_addr) { @@ -69,9 +78,6 @@ impl ConnectionValidator { valid_until: [u8; 4], source_addr: CanonicalSocketAddr, ) -> ConnectionId { - // The first 4 bytes is number of seconds since server start until - // connection is no longer valid. The last 4 is the truncated message - // authentication code. let mut connection_id_bytes = [0u8; 8]; (&mut connection_id_bytes[..4]).copy_from_slice(&valid_until); From 07630d2e0cb4ebcf007dbd5c8e45be552a9a9d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 22:19:28 +0200 Subject: [PATCH 22/46] udp: add quickcheck test for ConnectionValidator --- aquatic_udp/src/common.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 43b7358..2b42542 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -267,9 +267,13 @@ impl State { #[cfg(test)] mod tests { - use std::net::Ipv6Addr; + use std::net::{Ipv6Addr, SocketAddr}; - use crate::{common::MAX_PACKET_SIZE, config::Config}; + use quickcheck_macros::quickcheck; + + use crate::config::Config; + + use super::*; #[test] fn test_peer_status_from_event_and_bytes_left() { @@ -323,4 +327,25 @@ mod tests { assert!(buf.len() <= MAX_PACKET_SIZE); } + + #[quickcheck] + fn test_connection_validator(addr: IpAddr, max_connection_age: u32) -> bool { + let addr = CanonicalSocketAddr::new(SocketAddr::new(addr, 0)); + + let mut config = Config::default(); + + config.cleaning.max_connection_age = max_connection_age; + + let mut validator = ConnectionValidator::new(&config).unwrap(); + + let connection_id = validator.create_connection_id(addr); + + let valid = validator.connection_id_valid(addr, connection_id); + + if max_connection_age == 0 { + !valid + } else { + valid // Note: depends on that running this test takes less than a second + } + } } From d6e5155acf2c7a649785ed9b4a1068cf42d9f244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 22:23:21 +0200 Subject: [PATCH 23/46] udp: rename MAX_PACKET_SIZE to BUFFER_SIZE --- aquatic_udp/src/common.rs | 6 +++--- aquatic_udp/src/workers/socket.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 2b42542..db7c47c 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -15,7 +15,7 @@ use aquatic_udp_protocol::*; use crate::config::Config; -pub const MAX_PACKET_SIZE: usize = 8192; +pub const BUFFER_SIZE: usize = 8192; /// HMAC (BLAKE3) based ConnectionID creator and validator /// @@ -299,7 +299,7 @@ mod tests { // Assumes that announce response with maximum amount of ipv6 peers will // be the longest #[test] - fn test_max_package_size() { + fn test_buffer_size() { use aquatic_udp_protocol::*; let config = Config::default(); @@ -325,7 +325,7 @@ mod tests { println!("Buffer len: {}", buf.len()); - assert!(buf.len() <= MAX_PACKET_SIZE); + assert!(buf.len() <= BUFFER_SIZE); } #[quickcheck] diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs index d3eda3c..b70107c 100644 --- a/aquatic_udp/src/workers/socket.rs +++ b/aquatic_udp/src/workers/socket.rs @@ -140,7 +140,7 @@ pub fn run_socket_worker( response_receiver: Receiver<(ConnectedResponse, CanonicalSocketAddr)>, priv_dropper: PrivilegeDropper, ) { - let mut buffer = [0u8; MAX_PACKET_SIZE]; + let mut buffer = [0u8; BUFFER_SIZE]; let mut socket = UdpSocket::from_std(create_socket(&config, priv_dropper).expect("create socket")); From f58e2a9bdb42f5249f4b409e72732d6315abb1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 22:40:13 +0200 Subject: [PATCH 24/46] udp: improve test_connection_validator --- aquatic_udp/src/common.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index db7c47c..573b26a 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -329,23 +329,40 @@ mod tests { } #[quickcheck] - fn test_connection_validator(addr: IpAddr, max_connection_age: u32) -> bool { - let addr = CanonicalSocketAddr::new(SocketAddr::new(addr, 0)); + fn test_connection_validator( + original_addr: IpAddr, + different_addr: IpAddr, + max_connection_age: u32, + ) -> quickcheck::TestResult { + let original_addr = CanonicalSocketAddr::new(SocketAddr::new(original_addr, 0)); + let different_addr = CanonicalSocketAddr::new(SocketAddr::new(different_addr, 0)); - let mut config = Config::default(); + if original_addr == different_addr { + return quickcheck::TestResult::discard(); + } - config.cleaning.max_connection_age = max_connection_age; + let mut validator = { + let mut config = Config::default(); - let mut validator = ConnectionValidator::new(&config).unwrap(); + config.cleaning.max_connection_age = max_connection_age; - let connection_id = validator.create_connection_id(addr); + ConnectionValidator::new(&config).unwrap() + }; - let valid = validator.connection_id_valid(addr, connection_id); + let connection_id = validator.create_connection_id(original_addr); + + let original_valid = validator.connection_id_valid(original_addr, connection_id); + let different_valid = validator.connection_id_valid(different_addr, connection_id); + + if different_valid { + return quickcheck::TestResult::failed(); + } if max_connection_age == 0 { - !valid + quickcheck::TestResult::from_bool(!original_valid) } else { - valid // Note: depends on that running this test takes less than a second + // Note: depends on that running this test takes less than a second + quickcheck::TestResult::from_bool(original_valid) } } } From e8cb0c16184c7a7bf037d5db6552c12291f18b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 22:45:29 +0200 Subject: [PATCH 25/46] udp: improve ConnectionValidator doc comment --- aquatic_udp/src/common.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 573b26a..5c8024b 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -21,7 +21,8 @@ pub const BUFFER_SIZE: usize = 8192; /// /// Structure of created ConnectionID (bytes making up inner i64): /// - &[0..4]: connection expiration time as number of seconds after -/// ConnectionValidator instance was created, encoded as u32 bytes +/// ConnectionValidator instance was created, encoded as u32 bytes. +/// Value fits around 136 years. /// - &[4..8]: truncated keyed BLAKE3 hash of above 4 bytes and octets of /// client IP address #[derive(Clone)] From 1e0559f384d0e29ca9741ff2d0ee40f4e75c126c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 22:51:16 +0200 Subject: [PATCH 26/46] udp: rename ConnectionValidator.hmac to .keyed_hasher --- aquatic_udp/src/common.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 5c8024b..0864621 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -29,7 +29,7 @@ pub const BUFFER_SIZE: usize = 8192; pub struct ConnectionValidator { start_time: Instant, max_connection_age: u32, - hmac: blake3::Hasher, + keyed_hasher: blake3::Hasher, } impl ConnectionValidator { @@ -41,10 +41,10 @@ impl ConnectionValidator { getrandom(&mut key) .with_context(|| "Couldn't get random bytes for ConnectionValidator key")?; - let hmac = blake3::Hasher::new_keyed(&key); + let keyed_hasher = blake3::Hasher::new_keyed(&key); Ok(Self { - hmac, + keyed_hasher, start_time: Instant::now(), max_connection_age: config.cleaning.max_connection_age, }) @@ -83,15 +83,17 @@ impl ConnectionValidator { (&mut connection_id_bytes[..4]).copy_from_slice(&valid_until); - self.hmac.update(&valid_until); + self.keyed_hasher.update(&valid_until); match source_addr.get().ip() { - IpAddr::V4(ip) => self.hmac.update(&ip.octets()), - IpAddr::V6(ip) => self.hmac.update(&ip.octets()), + IpAddr::V4(ip) => self.keyed_hasher.update(&ip.octets()), + IpAddr::V6(ip) => self.keyed_hasher.update(&ip.octets()), }; - self.hmac.finalize_xof().fill(&mut connection_id_bytes[4..]); - self.hmac.reset(); + self.keyed_hasher + .finalize_xof() + .fill(&mut connection_id_bytes[4..]); + self.keyed_hasher.reset(); ConnectionId(i64::from_ne_bytes(connection_id_bytes)) } From 69a22db9730c38347a08feb3c9acd43cf1e2debc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 22:53:15 +0200 Subject: [PATCH 27/46] Run cargo update Updating axum v0.5.0 -> v0.5.1 Updating axum-core v0.2.0 -> v0.2.1 Updating httparse v1.6.0 -> v1.7.0 Updating js-sys v0.3.56 -> v0.3.57 Updating libc v0.2.121 -> v0.2.123 Updating proc-macro2 v1.0.36 -> v1.0.37 Updating quote v1.0.17 -> v1.0.18 Updating rayon v1.5.1 -> v1.5.2 Updating rayon-core v1.9.1 -> v1.9.2 Updating simdutf8 v0.1.3 -> v0.1.4 Updating slab v0.4.5 -> v0.4.6 Updating syn v1.0.90 -> v1.0.91 Updating tracing v0.1.32 -> v0.1.33 Updating tracing-core v0.1.23 -> v0.1.25 Updating wasm-bindgen v0.2.79 -> v0.2.80 Updating wasm-bindgen-backend v0.2.79 -> v0.2.80 Updating wasm-bindgen-macro v0.2.79 -> v0.2.80 Updating wasm-bindgen-macro-support v0.2.79 -> v0.2.80 Updating wasm-bindgen-shared v0.2.79 -> v0.2.80 Updating web-sys v0.3.56 -> v0.3.57 --- Cargo.lock | 81 +++++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d96f21..1fd53d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,9 +441,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5611d4977882c5af1c0f7a34d51b5d87f784f86912bb543986b014ea4995ef93" +checksum = "47594e438a243791dba58124b6669561f5baa14cb12046641d8008bf035e5a25" dependencies = [ "async-trait", "axum-core", @@ -471,9 +471,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cd109b3e93c9541dcce5b0219dcf89169dcc58c1bebed65082808324258afb" +checksum = "9a671c9ae99531afdd5d3ee8340b8da547779430689947144c140fc74a740244" dependencies = [ "async-trait", "bytes", @@ -1384,9 +1384,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" [[package]] name = "httpdate" @@ -1517,9 +1517,9 @@ checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "js-sys" -version = "0.3.56" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" dependencies = [ "wasm-bindgen", ] @@ -1545,9 +1545,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.121" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" [[package]] name = "libm" @@ -2092,9 +2092,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" dependencies = [ "unicode-xid", ] @@ -2123,9 +2123,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -2172,9 +2172,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" dependencies = [ "autocfg 1.1.0", "crossbeam-deque", @@ -2184,14 +2184,13 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", "num_cpus", ] @@ -2504,9 +2503,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970da16e7c682fa90a261cf0724dee241c9f7831635ecc4e988ae8f3b505559" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" [[package]] name = "simple_logger" @@ -2529,9 +2528,9 @@ checksum = "76a77a8fd93886010f05e7ea0720e569d6d16c65329dbe3ec033bbbccccb017b" [[package]] name = "slab" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "smallvec" @@ -2717,9 +2716,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ "proc-macro2", "quote", @@ -2962,9 +2961,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" +checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3" dependencies = [ "cfg-if", "log", @@ -2986,9 +2985,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.23" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c" +checksum = "6dfce9f3241b150f36e8e54bb561a742d5daa1a47b5dd9a5ce369fd4a4db2210" dependencies = [ "lazy_static", ] @@ -3152,9 +3151,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3162,9 +3161,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" dependencies = [ "bumpalo", "lazy_static", @@ -3177,9 +3176,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3187,9 +3186,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" dependencies = [ "proc-macro2", "quote", @@ -3200,15 +3199,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" [[package]] name = "web-sys" -version = "0.3.56" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" dependencies = [ "js-sys", "wasm-bindgen", From cad74df689f7bf8629992fb0f60d0e80dd329b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Thu, 14 Apr 2022 23:06:54 +0200 Subject: [PATCH 28/46] Update TODO --- TODO.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/TODO.md b/TODO.md index ef68315..946bd44 100644 --- a/TODO.md +++ b/TODO.md @@ -2,16 +2,15 @@ ## High priority -* aquatic_http_private - * Consider not setting Content-type: text/plain for responses and send vec as default octet stream instead - * stored procedure - * test ip format - * check user token length - * site will likely want num_seeders and num_leechers for all torrents.. +* aquatic_udp + * ConnectionValidator + * Is comparison really constant time? + * Document security aspects of 4-byte BLAKE3 XOR mode output? ## Medium priority * rename request workers to swarm workers + * quit whole program if any thread panics * But it would be nice not to panic in workers, but to return errors instead. Once JoinHandle::is_finished is available in stable Rust (#90470), an @@ -29,6 +28,13 @@ * SinkExt::send maybe doesn't wake up properly? * related to https://github.com/sdroege/async-tungstenite/blob/master/src/compat.rs#L18 ? +* aquatic_http_private + * Consider not setting Content-type: text/plain for responses and send vec as default octet stream instead + * stored procedure + * test ip format + * check user token length + * site will likely want num_seeders and num_leechers for all torrents.. + * extract_response_peers * don't assume requesting peer is in list? From ce2723effacf79609140783160f439c4658feb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Fri, 15 Apr 2022 02:27:13 +0200 Subject: [PATCH 29/46] udp: add WIP constant-time ConnectionID comparison --- aquatic_udp/src/common.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 0864621..8832477 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -65,9 +65,10 @@ impl ConnectionValidator { let valid_until = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); // Check that recreating ConnectionId with same inputs yields identical hash. - // - // I expect i64 comparison to be be constant-time. - if connection_id != self.create_connection_id_inner(valid_until, source_addr) { + if !Self::connection_id_eq_constant_time( + connection_id, + self.create_connection_id_inner(valid_until, source_addr), + ) { return false; } @@ -97,6 +98,27 @@ impl ConnectionValidator { ConnectionId(i64::from_ne_bytes(connection_id_bytes)) } + + /// Compare ConnectionIDs in constant time + /// + /// Use this instead of PartialEq::eq to avoid optimizations breaking constant + /// time HMAC comparison. + #[cfg(target_arch = "x86_64")] + fn connection_id_eq_constant_time(a: ConnectionId, b: ConnectionId) -> bool { + let mut eq = 0u8; + + unsafe { + ::std::arch::asm!( + "cmp {a}, {b}", + "sete {eq}", + a = in(reg) a.0, + b = in(reg) b.0, + eq = inout(reg_byte) eq, + ); + } + + eq != 0 + } } #[derive(Debug)] @@ -368,4 +390,12 @@ mod tests { quickcheck::TestResult::from_bool(original_valid) } } + + #[quickcheck] + fn test_connection_id_eq(a: i64, b: i64) -> bool { + let a = ConnectionId(a); + let b = ConnectionId(b); + + ConnectionValidator::connection_id_eq_constant_time(a, b) == (a == b) + } } From 19c604d4f148404c7b5750b1b2e9f337efac036f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Fri, 15 Apr 2022 02:56:55 +0200 Subject: [PATCH 30/46] udp: ConnectionValidator constant time eq: set nomem and nostack --- aquatic_udp/src/common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 8832477..fa216ed 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -114,6 +114,7 @@ impl ConnectionValidator { a = in(reg) a.0, b = in(reg) b.0, eq = inout(reg_byte) eq, + options(nomem, nostack), ); } From 22fa226f950802cdf09f1b875e0d83c4230b85cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Fri, 15 Apr 2022 22:30:38 +0200 Subject: [PATCH 31/46] udp: ConnectionValidator: rename connection_id_eq_constant_time --- aquatic_udp/src/common.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index fa216ed..5ef7104 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -65,7 +65,7 @@ impl ConnectionValidator { let valid_until = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); // Check that recreating ConnectionId with same inputs yields identical hash. - if !Self::connection_id_eq_constant_time( + if !Self::connection_id_eq( connection_id, self.create_connection_id_inner(valid_until, source_addr), ) { @@ -99,12 +99,12 @@ impl ConnectionValidator { ConnectionId(i64::from_ne_bytes(connection_id_bytes)) } - /// Compare ConnectionIDs in constant time + /// Compare ConnectionIDs without breaking constant time requirements /// /// Use this instead of PartialEq::eq to avoid optimizations breaking constant /// time HMAC comparison. #[cfg(target_arch = "x86_64")] - fn connection_id_eq_constant_time(a: ConnectionId, b: ConnectionId) -> bool { + fn connection_id_eq(a: ConnectionId, b: ConnectionId) -> bool { let mut eq = 0u8; unsafe { @@ -397,6 +397,6 @@ mod tests { let a = ConnectionId(a); let b = ConnectionId(b); - ConnectionValidator::connection_id_eq_constant_time(a, b) == (a == b) + ConnectionValidator::connection_id_eq(a, b) == (a == b) } } From fb9b3459902f1221385a4126b9e4dc53b1355ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Fri, 15 Apr 2022 22:37:58 +0200 Subject: [PATCH 32/46] udp: improve ConnectionValidator documentation --- aquatic_udp/src/common.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 5ef7104..6adbdc0 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -25,6 +25,11 @@ pub const BUFFER_SIZE: usize = 8192; /// Value fits around 136 years. /// - &[4..8]: truncated keyed BLAKE3 hash of above 4 bytes and octets of /// client IP address +/// +/// The purpose of using ConnectionIDs is to prevent IP spoofing, mainly to +/// prevent the tracker from being used as an amplification vector for DDoS +/// attacks. By including 32 bits of BLAKE3 keyed hash output in its contents, +/// such abuse should be rendered impractical. #[derive(Clone)] pub struct ConnectionValidator { start_time: Instant, @@ -102,7 +107,7 @@ impl ConnectionValidator { /// Compare ConnectionIDs without breaking constant time requirements /// /// Use this instead of PartialEq::eq to avoid optimizations breaking constant - /// time HMAC comparison. + /// time HMAC comparison and thus strongly reducing security. #[cfg(target_arch = "x86_64")] fn connection_id_eq(a: ConnectionId, b: ConnectionId) -> bool { let mut eq = 0u8; From 64452503e7ab842de9e8e56df77b72c4a7fc7bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Fri, 15 Apr 2022 23:40:05 +0200 Subject: [PATCH 33/46] aquatic_udp: use constant_time_eq crate for ConnectionValidator Crate is used in official blake3 implementation. Improves speed and removed need for error-prone custom assembly. --- Cargo.lock | 9 ++++- aquatic_udp/Cargo.toml | 1 + aquatic_udp/src/common.rs | 70 +++++++++++---------------------------- 3 files changed, 28 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fd53d3..1cc25ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,7 @@ dependencies = [ "aquatic_udp_protocol", "blake3", "cfg-if", + "constant_time_eq 0.2.1", "crossbeam-channel", "getrandom", "hashbrown 0.12.0", @@ -549,7 +550,7 @@ dependencies = [ "arrayvec 0.7.2", "cc", "cfg-if", - "constant_time_eq", + "constant_time_eq 0.1.5", "digest 0.10.3", ] @@ -690,6 +691,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df04a53a7e91248c27eb6bfc1db165e8f47453e98478e4609f9cce020bb3c65a" + [[package]] name = "cpufeatures" version = "0.2.2" diff --git a/aquatic_udp/Cargo.toml b/aquatic_udp/Cargo.toml index aaf51c5..e8dcb7b 100644 --- a/aquatic_udp/Cargo.toml +++ b/aquatic_udp/Cargo.toml @@ -26,6 +26,7 @@ aquatic_udp_protocol = { version = "0.2.0", path = "../aquatic_udp_protocol" } anyhow = "1" blake3 = "1" cfg-if = "1" +constant_time_eq = "0.2" crossbeam-channel = "0.5" getrandom = "0.2" hashbrown = { version = "0.12", default-features = false } diff --git a/aquatic_udp/src/common.rs b/aquatic_udp/src/common.rs index 6adbdc0..923bd22 100644 --- a/aquatic_udp/src/common.rs +++ b/aquatic_udp/src/common.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use std::time::Instant; use anyhow::Context; +use constant_time_eq::constant_time_eq; use crossbeam_channel::{Sender, TrySendError}; use getrandom::getrandom; @@ -59,7 +60,14 @@ impl ConnectionValidator { let valid_until = (self.start_time.elapsed().as_secs() as u32 + self.max_connection_age).to_ne_bytes(); - self.create_connection_id_inner(valid_until, source_addr) + let hash = self.hash(valid_until, source_addr.get().ip()); + + let mut connection_id_bytes = [0u8; 8]; + + (&mut connection_id_bytes[..4]).copy_from_slice(&valid_until); + (&mut connection_id_bytes[4..]).copy_from_slice(&hash); + + ConnectionId(i64::from_ne_bytes(connection_id_bytes)) } pub fn connection_id_valid( @@ -67,63 +75,31 @@ impl ConnectionValidator { source_addr: CanonicalSocketAddr, connection_id: ConnectionId, ) -> bool { - let valid_until = connection_id.0.to_ne_bytes()[..4].try_into().unwrap(); + let bytes = connection_id.0.to_ne_bytes(); + let (valid_until, hash) = bytes.split_at(4); + let valid_until: [u8; 4] = valid_until.try_into().unwrap(); - // Check that recreating ConnectionId with same inputs yields identical hash. - if !Self::connection_id_eq( - connection_id, - self.create_connection_id_inner(valid_until, source_addr), - ) { + if !constant_time_eq(hash, &self.hash(valid_until, source_addr.get().ip())) { return false; } u32::from_ne_bytes(valid_until) > self.start_time.elapsed().as_secs() as u32 } - fn create_connection_id_inner( - &mut self, - valid_until: [u8; 4], - source_addr: CanonicalSocketAddr, - ) -> ConnectionId { - let mut connection_id_bytes = [0u8; 8]; - - (&mut connection_id_bytes[..4]).copy_from_slice(&valid_until); - + fn hash(&mut self, valid_until: [u8; 4], ip_addr: IpAddr) -> [u8; 4] { self.keyed_hasher.update(&valid_until); - match source_addr.get().ip() { + match ip_addr { IpAddr::V4(ip) => self.keyed_hasher.update(&ip.octets()), IpAddr::V6(ip) => self.keyed_hasher.update(&ip.octets()), }; - self.keyed_hasher - .finalize_xof() - .fill(&mut connection_id_bytes[4..]); + let mut hash = [0u8; 4]; + + self.keyed_hasher.finalize_xof().fill(&mut hash); self.keyed_hasher.reset(); - ConnectionId(i64::from_ne_bytes(connection_id_bytes)) - } - - /// Compare ConnectionIDs without breaking constant time requirements - /// - /// Use this instead of PartialEq::eq to avoid optimizations breaking constant - /// time HMAC comparison and thus strongly reducing security. - #[cfg(target_arch = "x86_64")] - fn connection_id_eq(a: ConnectionId, b: ConnectionId) -> bool { - let mut eq = 0u8; - - unsafe { - ::std::arch::asm!( - "cmp {a}, {b}", - "sete {eq}", - a = in(reg) a.0, - b = in(reg) b.0, - eq = inout(reg_byte) eq, - options(nomem, nostack), - ); - } - - eq != 0 + hash } } @@ -396,12 +372,4 @@ mod tests { quickcheck::TestResult::from_bool(original_valid) } } - - #[quickcheck] - fn test_connection_id_eq(a: i64, b: i64) -> bool { - let a = ConnectionId(a); - let b = ConnectionId(b); - - ConnectionValidator::connection_id_eq(a, b) == (a == b) - } } From 201879c519154f0615b225ef186846c27930bb8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Fri, 15 Apr 2022 23:59:18 +0200 Subject: [PATCH 34/46] Update TODO --- TODO.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 946bd44..7c165ca 100644 --- a/TODO.md +++ b/TODO.md @@ -2,15 +2,13 @@ ## High priority -* aquatic_udp - * ConnectionValidator - * Is comparison really constant time? - * Document security aspects of 4-byte BLAKE3 XOR mode output? - ## Medium priority * rename request workers to swarm workers +* save space by making ValidUntil just contain u32 with seconds, measured + some Instant created at application start + * quit whole program if any thread panics * But it would be nice not to panic in workers, but to return errors instead. Once JoinHandle::is_finished is available in stable Rust (#90470), an From 86fb7f0fb38d1566fdf35923c4bd0a728b2c0348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 00:15:09 +0200 Subject: [PATCH 35/46] udp: split workers/socket.rs into modules --- aquatic_udp/src/workers/socket.rs | 623 -------------------- aquatic_udp/src/workers/socket/common.rs | 221 +++++++ aquatic_udp/src/workers/socket/mod.rs | 158 +++++ aquatic_udp/src/workers/socket/requests.rs | 180 ++++++ aquatic_udp/src/workers/socket/responses.rs | 104 ++++ 5 files changed, 663 insertions(+), 623 deletions(-) delete mode 100644 aquatic_udp/src/workers/socket.rs create mode 100644 aquatic_udp/src/workers/socket/common.rs create mode 100644 aquatic_udp/src/workers/socket/mod.rs create mode 100644 aquatic_udp/src/workers/socket/requests.rs create mode 100644 aquatic_udp/src/workers/socket/responses.rs diff --git a/aquatic_udp/src/workers/socket.rs b/aquatic_udp/src/workers/socket.rs deleted file mode 100644 index b70107c..0000000 --- a/aquatic_udp/src/workers/socket.rs +++ /dev/null @@ -1,623 +0,0 @@ -use std::collections::BTreeMap; -use std::io::{Cursor, ErrorKind}; -use std::sync::atomic::Ordering; -use std::time::{Duration, Instant}; -use std::vec::Drain; - -use anyhow::Context; -use aquatic_common::privileges::PrivilegeDropper; -use crossbeam_channel::Receiver; -use hashbrown::HashMap; -use mio::net::UdpSocket; -use mio::{Events, Interest, Poll, Token}; -use slab::Slab; - -use aquatic_common::access_list::create_access_list_cache; -use aquatic_common::access_list::AccessListCache; -use aquatic_common::CanonicalSocketAddr; -use aquatic_common::{PanicSentinel, ValidUntil}; -use aquatic_udp_protocol::*; -use socket2::{Domain, Protocol, Socket, Type}; - -use crate::common::*; -use crate::config::Config; - -#[derive(Debug)] -pub struct PendingScrapeResponseSlabEntry { - num_pending: usize, - valid_until: ValidUntil, - torrent_stats: BTreeMap, - transaction_id: TransactionId, -} - -#[derive(Default)] -pub struct PendingScrapeResponseSlab(Slab); - -impl PendingScrapeResponseSlab { - pub fn prepare_split_requests( - &mut self, - config: &Config, - request: ScrapeRequest, - valid_until: ValidUntil, - ) -> impl IntoIterator { - let capacity = config.request_workers.min(request.info_hashes.len()); - let mut split_requests: HashMap = - HashMap::with_capacity(capacity); - - if request.info_hashes.is_empty() { - ::log::warn!( - "Attempted to prepare PendingScrapeResponseSlab entry with zero info hashes" - ); - - return split_requests; - } - - let vacant_entry = self.0.vacant_entry(); - let slab_key = vacant_entry.key(); - - for (i, info_hash) in request.info_hashes.into_iter().enumerate() { - let split_request = split_requests - .entry(RequestWorkerIndex::from_info_hash(&config, info_hash)) - .or_insert_with(|| PendingScrapeRequest { - slab_key, - info_hashes: BTreeMap::new(), - }); - - split_request.info_hashes.insert(i, info_hash); - } - - vacant_entry.insert(PendingScrapeResponseSlabEntry { - num_pending: split_requests.len(), - valid_until, - torrent_stats: Default::default(), - transaction_id: request.transaction_id, - }); - - split_requests - } - - pub fn add_and_get_finished( - &mut self, - response: PendingScrapeResponse, - ) -> Option { - let finished = if let Some(entry) = self.0.get_mut(response.slab_key) { - entry.num_pending -= 1; - - entry - .torrent_stats - .extend(response.torrent_stats.into_iter()); - - entry.num_pending == 0 - } else { - ::log::warn!( - "PendingScrapeResponseSlab.add didn't find entry for key {:?}", - response.slab_key - ); - - false - }; - - if finished { - let entry = self.0.remove(response.slab_key); - - Some(ScrapeResponse { - transaction_id: entry.transaction_id, - torrent_stats: entry.torrent_stats.into_values().collect(), - }) - } else { - None - } - } - - pub fn clean(&mut self) { - let now = Instant::now(); - - self.0.retain(|k, v| { - if v.valid_until.0 > now { - true - } else { - ::log::warn!( - "Unconsumed PendingScrapeResponseSlab entry. {:?}: {:?}", - k, - v - ); - - false - } - }); - - self.0.shrink_to_fit(); - } -} - -pub fn run_socket_worker( - _sentinel: PanicSentinel, - state: State, - config: Config, - token_num: usize, - mut connection_validator: ConnectionValidator, - request_sender: ConnectedRequestSender, - response_receiver: Receiver<(ConnectedResponse, CanonicalSocketAddr)>, - priv_dropper: PrivilegeDropper, -) { - let mut buffer = [0u8; BUFFER_SIZE]; - - let mut socket = - UdpSocket::from_std(create_socket(&config, priv_dropper).expect("create socket")); - let mut poll = Poll::new().expect("create poll"); - - let interests = Interest::READABLE; - - poll.registry() - .register(&mut socket, Token(token_num), interests) - .unwrap(); - - let mut events = Events::with_capacity(config.network.poll_event_capacity); - let mut pending_scrape_responses = PendingScrapeResponseSlab::default(); - let mut access_list_cache = create_access_list_cache(&state.access_list); - - let mut local_responses: Vec<(Response, CanonicalSocketAddr)> = Vec::new(); - - let poll_timeout = Duration::from_millis(config.network.poll_timeout_ms); - - let pending_scrape_cleaning_duration = - Duration::from_secs(config.cleaning.pending_scrape_cleaning_interval); - - let mut pending_scrape_valid_until = ValidUntil::new(config.cleaning.max_pending_scrape_age); - let mut last_pending_scrape_cleaning = Instant::now(); - - let mut iter_counter = 0usize; - - loop { - poll.poll(&mut events, Some(poll_timeout)) - .expect("failed polling"); - - for event in events.iter() { - let token = event.token(); - - if (token.0 == token_num) & event.is_readable() { - read_requests( - &config, - &state, - &mut connection_validator, - &mut pending_scrape_responses, - &mut access_list_cache, - &mut socket, - &mut buffer, - &request_sender, - &mut local_responses, - pending_scrape_valid_until, - ); - } - } - - send_responses( - &state, - &config, - &mut socket, - &mut buffer, - &response_receiver, - &mut pending_scrape_responses, - local_responses.drain(..), - ); - - // Run periodic ValidUntil updates and state cleaning - if iter_counter % 256 == 0 { - let now = Instant::now(); - - pending_scrape_valid_until = - ValidUntil::new_with_now(now, config.cleaning.max_pending_scrape_age); - - if now > last_pending_scrape_cleaning + pending_scrape_cleaning_duration { - pending_scrape_responses.clean(); - - last_pending_scrape_cleaning = now; - } - } - - iter_counter = iter_counter.wrapping_add(1); - } -} - -fn read_requests( - config: &Config, - state: &State, - connection_validator: &mut ConnectionValidator, - pending_scrape_responses: &mut PendingScrapeResponseSlab, - access_list_cache: &mut AccessListCache, - socket: &mut UdpSocket, - buffer: &mut [u8], - request_sender: &ConnectedRequestSender, - local_responses: &mut Vec<(Response, CanonicalSocketAddr)>, - pending_scrape_valid_until: ValidUntil, -) { - let mut requests_received_ipv4: usize = 0; - let mut requests_received_ipv6: usize = 0; - let mut bytes_received_ipv4: usize = 0; - let mut bytes_received_ipv6 = 0; - - loop { - match socket.recv_from(&mut buffer[..]) { - Ok((amt, src)) => { - let res_request = - Request::from_bytes(&buffer[..amt], config.protocol.max_scrape_torrents); - - let src = CanonicalSocketAddr::new(src); - - // Update statistics for converted address - if src.is_ipv4() { - if res_request.is_ok() { - requests_received_ipv4 += 1; - } - bytes_received_ipv4 += amt; - } else { - if res_request.is_ok() { - requests_received_ipv6 += 1; - } - bytes_received_ipv6 += amt; - } - - handle_request( - config, - connection_validator, - pending_scrape_responses, - access_list_cache, - request_sender, - local_responses, - pending_scrape_valid_until, - res_request, - src, - ); - } - Err(err) if err.kind() == ErrorKind::WouldBlock => { - break; - } - Err(err) => { - ::log::warn!("recv_from error: {:#}", err); - } - } - } - - if config.statistics.active() { - state - .statistics_ipv4 - .requests_received - .fetch_add(requests_received_ipv4, Ordering::Release); - state - .statistics_ipv6 - .requests_received - .fetch_add(requests_received_ipv6, Ordering::Release); - state - .statistics_ipv4 - .bytes_received - .fetch_add(bytes_received_ipv4, Ordering::Release); - state - .statistics_ipv6 - .bytes_received - .fetch_add(bytes_received_ipv6, Ordering::Release); - } -} - -pub fn handle_request( - config: &Config, - connection_validator: &mut ConnectionValidator, - pending_scrape_responses: &mut PendingScrapeResponseSlab, - access_list_cache: &mut AccessListCache, - request_sender: &ConnectedRequestSender, - local_responses: &mut Vec<(Response, CanonicalSocketAddr)>, - pending_scrape_valid_until: ValidUntil, - res_request: Result, - src: CanonicalSocketAddr, -) { - let access_list_mode = config.access_list.mode; - - match res_request { - Ok(Request::Connect(request)) => { - let connection_id = connection_validator.create_connection_id(src); - - let response = Response::Connect(ConnectResponse { - connection_id, - transaction_id: request.transaction_id, - }); - - local_responses.push((response, src)) - } - Ok(Request::Announce(request)) => { - if connection_validator.connection_id_valid(src, request.connection_id) { - if access_list_cache - .load() - .allows(access_list_mode, &request.info_hash.0) - { - let worker_index = - RequestWorkerIndex::from_info_hash(config, request.info_hash); - - request_sender.try_send_to( - worker_index, - ConnectedRequest::Announce(request), - src, - ); - } else { - let response = Response::Error(ErrorResponse { - transaction_id: request.transaction_id, - message: "Info hash not allowed".into(), - }); - - local_responses.push((response, src)) - } - } - } - Ok(Request::Scrape(request)) => { - if connection_validator.connection_id_valid(src, request.connection_id) { - let split_requests = pending_scrape_responses.prepare_split_requests( - config, - request, - pending_scrape_valid_until, - ); - - for (request_worker_index, request) in split_requests { - request_sender.try_send_to( - request_worker_index, - ConnectedRequest::Scrape(request), - src, - ); - } - } - } - Err(err) => { - ::log::debug!("Request::from_bytes error: {:?}", err); - - if let RequestParseError::Sendable { - connection_id, - transaction_id, - err, - } = err - { - if connection_validator.connection_id_valid(src, connection_id) { - let response = ErrorResponse { - transaction_id, - message: err.right_or("Parse error").into(), - }; - - local_responses.push((response.into(), src)); - } - } - } - } -} - -fn send_responses( - state: &State, - config: &Config, - socket: &mut UdpSocket, - buffer: &mut [u8], - response_receiver: &Receiver<(ConnectedResponse, CanonicalSocketAddr)>, - pending_scrape_responses: &mut PendingScrapeResponseSlab, - local_responses: Drain<(Response, CanonicalSocketAddr)>, -) { - for (response, addr) in local_responses { - send_response(state, config, socket, buffer, response, addr); - } - - for (response, addr) in response_receiver.try_iter() { - let opt_response = match response { - ConnectedResponse::Scrape(r) => pending_scrape_responses - .add_and_get_finished(r) - .map(Response::Scrape), - ConnectedResponse::AnnounceIpv4(r) => Some(Response::AnnounceIpv4(r)), - ConnectedResponse::AnnounceIpv6(r) => Some(Response::AnnounceIpv6(r)), - }; - - if let Some(response) = opt_response { - send_response(state, config, socket, buffer, response, addr); - } - } -} - -fn send_response( - state: &State, - config: &Config, - socket: &mut UdpSocket, - buffer: &mut [u8], - response: Response, - addr: CanonicalSocketAddr, -) { - let mut cursor = Cursor::new(buffer); - - let canonical_addr_is_ipv4 = addr.is_ipv4(); - - let addr = if config.network.address.is_ipv4() { - addr.get_ipv4() - .expect("found peer ipv6 address while running bound to ipv4 address") - } else { - addr.get_ipv6_mapped() - }; - - match response.write(&mut cursor) { - Ok(()) => { - let amt = cursor.position() as usize; - - match socket.send_to(&cursor.get_ref()[..amt], addr) { - Ok(amt) if config.statistics.active() => { - let stats = if canonical_addr_is_ipv4 { - &state.statistics_ipv4 - } else { - &state.statistics_ipv6 - }; - - stats.bytes_sent.fetch_add(amt, Ordering::Relaxed); - - match response { - Response::Connect(_) => { - stats.responses_sent_connect.fetch_add(1, Ordering::Relaxed); - } - Response::AnnounceIpv4(_) | Response::AnnounceIpv6(_) => { - stats - .responses_sent_announce - .fetch_add(1, Ordering::Relaxed); - } - Response::Scrape(_) => { - stats.responses_sent_scrape.fetch_add(1, Ordering::Relaxed); - } - Response::Error(_) => { - stats.responses_sent_error.fetch_add(1, Ordering::Relaxed); - } - } - } - Ok(_) => {} - Err(err) => { - ::log::warn!("send_to error: {:#}", err); - } - } - } - Err(err) => { - ::log::error!("Response::write error: {:?}", err); - } - } -} - -pub fn create_socket( - config: &Config, - priv_dropper: PrivilegeDropper, -) -> anyhow::Result<::std::net::UdpSocket> { - let socket = if config.network.address.is_ipv4() { - Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))? - } else { - Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))? - }; - - if config.network.only_ipv6 { - socket - .set_only_v6(true) - .with_context(|| "socket: set only ipv6")?; - } - - socket - .set_reuse_port(true) - .with_context(|| "socket: set reuse port")?; - - socket - .set_nonblocking(true) - .with_context(|| "socket: set nonblocking")?; - - let recv_buffer_size = config.network.socket_recv_buffer_size; - - if recv_buffer_size != 0 { - if let Err(err) = socket.set_recv_buffer_size(recv_buffer_size) { - ::log::error!( - "socket: failed setting recv buffer to {}: {:?}", - recv_buffer_size, - err - ); - } - } - - socket - .bind(&config.network.address.into()) - .with_context(|| format!("socket: bind to {}", config.network.address))?; - - priv_dropper.after_socket_creation()?; - - Ok(socket.into()) -} - -#[cfg(test)] -mod tests { - use quickcheck::TestResult; - use quickcheck_macros::quickcheck; - - use super::*; - - #[quickcheck] - fn test_pending_scrape_response_map( - request_data: Vec<(i32, i64, u8)>, - request_workers: u8, - ) -> TestResult { - if request_workers == 0 { - return TestResult::discard(); - } - - let mut config = Config::default(); - - config.request_workers = request_workers as usize; - - let valid_until = ValidUntil::new(1); - - let mut map = PendingScrapeResponseSlab::default(); - - let mut requests = Vec::new(); - - for (t, c, b) in request_data { - if b == 0 { - return TestResult::discard(); - } - - let mut info_hashes = Vec::new(); - - for i in 0..b { - let info_hash = InfoHash([i; 20]); - - info_hashes.push(info_hash); - } - - let request = ScrapeRequest { - transaction_id: TransactionId(t), - connection_id: ConnectionId(c), - info_hashes, - }; - - requests.push(request); - } - - let mut all_split_requests = Vec::new(); - - for request in requests.iter() { - let split_requests = - map.prepare_split_requests(&config, request.to_owned(), valid_until); - - all_split_requests.push( - split_requests - .into_iter() - .collect::>(), - ); - } - - assert_eq!(map.0.len(), requests.len()); - - let mut responses = Vec::new(); - - for split_requests in all_split_requests { - for (worker_index, split_request) in split_requests { - assert!(worker_index.0 < request_workers as usize); - - let torrent_stats = split_request - .info_hashes - .into_iter() - .map(|(i, info_hash)| { - ( - i, - TorrentScrapeStatistics { - seeders: NumberOfPeers((info_hash.0[0]) as i32), - leechers: NumberOfPeers(0), - completed: NumberOfDownloads(0), - }, - ) - }) - .collect(); - - let response = PendingScrapeResponse { - slab_key: split_request.slab_key, - torrent_stats, - }; - - if let Some(response) = map.add_and_get_finished(response) { - responses.push(response); - } - } - } - - assert!(map.0.is_empty()); - assert_eq!(responses.len(), requests.len()); - - TestResult::from_bool(true) - } -} diff --git a/aquatic_udp/src/workers/socket/common.rs b/aquatic_udp/src/workers/socket/common.rs new file mode 100644 index 0000000..1d248d7 --- /dev/null +++ b/aquatic_udp/src/workers/socket/common.rs @@ -0,0 +1,221 @@ +use std::collections::BTreeMap; +use std::time::Instant; + +use hashbrown::HashMap; +use slab::Slab; + +use aquatic_common::ValidUntil; +use aquatic_udp_protocol::*; + +use crate::common::*; +use crate::config::Config; + +#[derive(Debug)] +pub struct PendingScrapeResponseSlabEntry { + num_pending: usize, + valid_until: ValidUntil, + torrent_stats: BTreeMap, + transaction_id: TransactionId, +} + +#[derive(Default)] +pub struct PendingScrapeResponseSlab(Slab); + +impl PendingScrapeResponseSlab { + pub fn prepare_split_requests( + &mut self, + config: &Config, + request: ScrapeRequest, + valid_until: ValidUntil, + ) -> impl IntoIterator { + let capacity = config.request_workers.min(request.info_hashes.len()); + let mut split_requests: HashMap = + HashMap::with_capacity(capacity); + + if request.info_hashes.is_empty() { + ::log::warn!( + "Attempted to prepare PendingScrapeResponseSlab entry with zero info hashes" + ); + + return split_requests; + } + + let vacant_entry = self.0.vacant_entry(); + let slab_key = vacant_entry.key(); + + for (i, info_hash) in request.info_hashes.into_iter().enumerate() { + let split_request = split_requests + .entry(RequestWorkerIndex::from_info_hash(&config, info_hash)) + .or_insert_with(|| PendingScrapeRequest { + slab_key, + info_hashes: BTreeMap::new(), + }); + + split_request.info_hashes.insert(i, info_hash); + } + + vacant_entry.insert(PendingScrapeResponseSlabEntry { + num_pending: split_requests.len(), + valid_until, + torrent_stats: Default::default(), + transaction_id: request.transaction_id, + }); + + split_requests + } + + pub fn add_and_get_finished( + &mut self, + response: PendingScrapeResponse, + ) -> Option { + let finished = if let Some(entry) = self.0.get_mut(response.slab_key) { + entry.num_pending -= 1; + + entry + .torrent_stats + .extend(response.torrent_stats.into_iter()); + + entry.num_pending == 0 + } else { + ::log::warn!( + "PendingScrapeResponseSlab.add didn't find entry for key {:?}", + response.slab_key + ); + + false + }; + + if finished { + let entry = self.0.remove(response.slab_key); + + Some(ScrapeResponse { + transaction_id: entry.transaction_id, + torrent_stats: entry.torrent_stats.into_values().collect(), + }) + } else { + None + } + } + + pub fn clean(&mut self) { + let now = Instant::now(); + + self.0.retain(|k, v| { + if v.valid_until.0 > now { + true + } else { + ::log::warn!( + "Unconsumed PendingScrapeResponseSlab entry. {:?}: {:?}", + k, + v + ); + + false + } + }); + + self.0.shrink_to_fit(); + } +} + +#[cfg(test)] +mod tests { + use quickcheck::TestResult; + use quickcheck_macros::quickcheck; + + use super::*; + + #[quickcheck] + fn test_pending_scrape_response_map( + request_data: Vec<(i32, i64, u8)>, + request_workers: u8, + ) -> TestResult { + if request_workers == 0 { + return TestResult::discard(); + } + + let mut config = Config::default(); + + config.request_workers = request_workers as usize; + + let valid_until = ValidUntil::new(1); + + let mut map = PendingScrapeResponseSlab::default(); + + let mut requests = Vec::new(); + + for (t, c, b) in request_data { + if b == 0 { + return TestResult::discard(); + } + + let mut info_hashes = Vec::new(); + + for i in 0..b { + let info_hash = InfoHash([i; 20]); + + info_hashes.push(info_hash); + } + + let request = ScrapeRequest { + transaction_id: TransactionId(t), + connection_id: ConnectionId(c), + info_hashes, + }; + + requests.push(request); + } + + let mut all_split_requests = Vec::new(); + + for request in requests.iter() { + let split_requests = + map.prepare_split_requests(&config, request.to_owned(), valid_until); + + all_split_requests.push( + split_requests + .into_iter() + .collect::>(), + ); + } + + assert_eq!(map.0.len(), requests.len()); + + let mut responses = Vec::new(); + + for split_requests in all_split_requests { + for (worker_index, split_request) in split_requests { + assert!(worker_index.0 < request_workers as usize); + + let torrent_stats = split_request + .info_hashes + .into_iter() + .map(|(i, info_hash)| { + ( + i, + TorrentScrapeStatistics { + seeders: NumberOfPeers((info_hash.0[0]) as i32), + leechers: NumberOfPeers(0), + completed: NumberOfDownloads(0), + }, + ) + }) + .collect(); + + let response = PendingScrapeResponse { + slab_key: split_request.slab_key, + torrent_stats, + }; + + if let Some(response) = map.add_and_get_finished(response) { + responses.push(response); + } + } + } + + assert!(map.0.is_empty()); + assert_eq!(responses.len(), requests.len()); + + TestResult::from_bool(true) + } +} diff --git a/aquatic_udp/src/workers/socket/mod.rs b/aquatic_udp/src/workers/socket/mod.rs new file mode 100644 index 0000000..78e79bd --- /dev/null +++ b/aquatic_udp/src/workers/socket/mod.rs @@ -0,0 +1,158 @@ +mod common; +mod requests; +mod responses; + +use std::time::{Duration, Instant}; + +use anyhow::Context; +use aquatic_common::privileges::PrivilegeDropper; +use crossbeam_channel::Receiver; +use mio::net::UdpSocket; +use mio::{Events, Interest, Poll, Token}; + +use aquatic_common::access_list::create_access_list_cache; +use aquatic_common::CanonicalSocketAddr; +use aquatic_common::{PanicSentinel, ValidUntil}; +use aquatic_udp_protocol::*; +use socket2::{Domain, Protocol, Socket, Type}; + +use crate::common::*; +use crate::config::Config; + +use self::common::PendingScrapeResponseSlab; +use self::requests::read_requests; +use self::responses::send_responses; + +pub fn run_socket_worker( + _sentinel: PanicSentinel, + state: State, + config: Config, + token_num: usize, + mut connection_validator: ConnectionValidator, + request_sender: ConnectedRequestSender, + response_receiver: Receiver<(ConnectedResponse, CanonicalSocketAddr)>, + priv_dropper: PrivilegeDropper, +) { + let mut buffer = [0u8; BUFFER_SIZE]; + + let mut socket = + UdpSocket::from_std(create_socket(&config, priv_dropper).expect("create socket")); + let mut poll = Poll::new().expect("create poll"); + + let interests = Interest::READABLE; + + poll.registry() + .register(&mut socket, Token(token_num), interests) + .unwrap(); + + let mut events = Events::with_capacity(config.network.poll_event_capacity); + let mut pending_scrape_responses = PendingScrapeResponseSlab::default(); + let mut access_list_cache = create_access_list_cache(&state.access_list); + + let mut local_responses: Vec<(Response, CanonicalSocketAddr)> = Vec::new(); + + let poll_timeout = Duration::from_millis(config.network.poll_timeout_ms); + + let pending_scrape_cleaning_duration = + Duration::from_secs(config.cleaning.pending_scrape_cleaning_interval); + + let mut pending_scrape_valid_until = ValidUntil::new(config.cleaning.max_pending_scrape_age); + let mut last_pending_scrape_cleaning = Instant::now(); + + let mut iter_counter = 0usize; + + loop { + poll.poll(&mut events, Some(poll_timeout)) + .expect("failed polling"); + + for event in events.iter() { + let token = event.token(); + + if (token.0 == token_num) & event.is_readable() { + read_requests( + &config, + &state, + &mut connection_validator, + &mut pending_scrape_responses, + &mut access_list_cache, + &mut socket, + &mut buffer, + &request_sender, + &mut local_responses, + pending_scrape_valid_until, + ); + } + } + + send_responses( + &state, + &config, + &mut socket, + &mut buffer, + &response_receiver, + &mut pending_scrape_responses, + local_responses.drain(..), + ); + + // Run periodic ValidUntil updates and state cleaning + if iter_counter % 256 == 0 { + let now = Instant::now(); + + pending_scrape_valid_until = + ValidUntil::new_with_now(now, config.cleaning.max_pending_scrape_age); + + if now > last_pending_scrape_cleaning + pending_scrape_cleaning_duration { + pending_scrape_responses.clean(); + + last_pending_scrape_cleaning = now; + } + } + + iter_counter = iter_counter.wrapping_add(1); + } +} + +fn create_socket( + config: &Config, + priv_dropper: PrivilegeDropper, +) -> anyhow::Result<::std::net::UdpSocket> { + let socket = if config.network.address.is_ipv4() { + Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))? + } else { + Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))? + }; + + if config.network.only_ipv6 { + socket + .set_only_v6(true) + .with_context(|| "socket: set only ipv6")?; + } + + socket + .set_reuse_port(true) + .with_context(|| "socket: set reuse port")?; + + socket + .set_nonblocking(true) + .with_context(|| "socket: set nonblocking")?; + + let recv_buffer_size = config.network.socket_recv_buffer_size; + + if recv_buffer_size != 0 { + if let Err(err) = socket.set_recv_buffer_size(recv_buffer_size) { + ::log::error!( + "socket: failed setting recv buffer to {}: {:?}", + recv_buffer_size, + err + ); + } + } + + socket + .bind(&config.network.address.into()) + .with_context(|| format!("socket: bind to {}", config.network.address))?; + + priv_dropper.after_socket_creation()?; + + Ok(socket.into()) +} diff --git a/aquatic_udp/src/workers/socket/requests.rs b/aquatic_udp/src/workers/socket/requests.rs new file mode 100644 index 0000000..9eead98 --- /dev/null +++ b/aquatic_udp/src/workers/socket/requests.rs @@ -0,0 +1,180 @@ +use std::io::ErrorKind; +use std::sync::atomic::Ordering; + +use mio::net::UdpSocket; + +use aquatic_common::access_list::AccessListCache; +use aquatic_common::CanonicalSocketAddr; +use aquatic_common::ValidUntil; +use aquatic_udp_protocol::*; + +use crate::common::*; +use crate::config::Config; + +use super::common::PendingScrapeResponseSlab; + +pub fn read_requests( + config: &Config, + state: &State, + connection_validator: &mut ConnectionValidator, + pending_scrape_responses: &mut PendingScrapeResponseSlab, + access_list_cache: &mut AccessListCache, + socket: &mut UdpSocket, + buffer: &mut [u8], + request_sender: &ConnectedRequestSender, + local_responses: &mut Vec<(Response, CanonicalSocketAddr)>, + pending_scrape_valid_until: ValidUntil, +) { + let mut requests_received_ipv4: usize = 0; + let mut requests_received_ipv6: usize = 0; + let mut bytes_received_ipv4: usize = 0; + let mut bytes_received_ipv6 = 0; + + loop { + match socket.recv_from(&mut buffer[..]) { + Ok((amt, src)) => { + let res_request = + Request::from_bytes(&buffer[..amt], config.protocol.max_scrape_torrents); + + let src = CanonicalSocketAddr::new(src); + + // Update statistics for converted address + if src.is_ipv4() { + if res_request.is_ok() { + requests_received_ipv4 += 1; + } + bytes_received_ipv4 += amt; + } else { + if res_request.is_ok() { + requests_received_ipv6 += 1; + } + bytes_received_ipv6 += amt; + } + + handle_request( + config, + connection_validator, + pending_scrape_responses, + access_list_cache, + request_sender, + local_responses, + pending_scrape_valid_until, + res_request, + src, + ); + } + Err(err) if err.kind() == ErrorKind::WouldBlock => { + break; + } + Err(err) => { + ::log::warn!("recv_from error: {:#}", err); + } + } + } + + if config.statistics.active() { + state + .statistics_ipv4 + .requests_received + .fetch_add(requests_received_ipv4, Ordering::Release); + state + .statistics_ipv6 + .requests_received + .fetch_add(requests_received_ipv6, Ordering::Release); + state + .statistics_ipv4 + .bytes_received + .fetch_add(bytes_received_ipv4, Ordering::Release); + state + .statistics_ipv6 + .bytes_received + .fetch_add(bytes_received_ipv6, Ordering::Release); + } +} + +fn handle_request( + config: &Config, + connection_validator: &mut ConnectionValidator, + pending_scrape_responses: &mut PendingScrapeResponseSlab, + access_list_cache: &mut AccessListCache, + request_sender: &ConnectedRequestSender, + local_responses: &mut Vec<(Response, CanonicalSocketAddr)>, + pending_scrape_valid_until: ValidUntil, + res_request: Result, + src: CanonicalSocketAddr, +) { + let access_list_mode = config.access_list.mode; + + match res_request { + Ok(Request::Connect(request)) => { + let connection_id = connection_validator.create_connection_id(src); + + let response = Response::Connect(ConnectResponse { + connection_id, + transaction_id: request.transaction_id, + }); + + local_responses.push((response, src)) + } + Ok(Request::Announce(request)) => { + if connection_validator.connection_id_valid(src, request.connection_id) { + if access_list_cache + .load() + .allows(access_list_mode, &request.info_hash.0) + { + let worker_index = + RequestWorkerIndex::from_info_hash(config, request.info_hash); + + request_sender.try_send_to( + worker_index, + ConnectedRequest::Announce(request), + src, + ); + } else { + let response = Response::Error(ErrorResponse { + transaction_id: request.transaction_id, + message: "Info hash not allowed".into(), + }); + + local_responses.push((response, src)) + } + } + } + Ok(Request::Scrape(request)) => { + if connection_validator.connection_id_valid(src, request.connection_id) { + let split_requests = pending_scrape_responses.prepare_split_requests( + config, + request, + pending_scrape_valid_until, + ); + + for (request_worker_index, request) in split_requests { + request_sender.try_send_to( + request_worker_index, + ConnectedRequest::Scrape(request), + src, + ); + } + } + } + Err(err) => { + ::log::debug!("Request::from_bytes error: {:?}", err); + + if let RequestParseError::Sendable { + connection_id, + transaction_id, + err, + } = err + { + if connection_validator.connection_id_valid(src, connection_id) { + let response = ErrorResponse { + transaction_id, + message: err.right_or("Parse error").into(), + }; + + local_responses.push((response.into(), src)); + } + } + } + } +} diff --git a/aquatic_udp/src/workers/socket/responses.rs b/aquatic_udp/src/workers/socket/responses.rs new file mode 100644 index 0000000..112c2fe --- /dev/null +++ b/aquatic_udp/src/workers/socket/responses.rs @@ -0,0 +1,104 @@ +use std::io::Cursor; +use std::sync::atomic::Ordering; +use std::vec::Drain; + +use crossbeam_channel::Receiver; +use mio::net::UdpSocket; + +use aquatic_common::CanonicalSocketAddr; +use aquatic_udp_protocol::*; + +use crate::common::*; +use crate::config::Config; + +use super::common::PendingScrapeResponseSlab; + +pub fn send_responses( + state: &State, + config: &Config, + socket: &mut UdpSocket, + buffer: &mut [u8], + response_receiver: &Receiver<(ConnectedResponse, CanonicalSocketAddr)>, + pending_scrape_responses: &mut PendingScrapeResponseSlab, + local_responses: Drain<(Response, CanonicalSocketAddr)>, +) { + for (response, addr) in local_responses { + send_response(state, config, socket, buffer, response, addr); + } + + for (response, addr) in response_receiver.try_iter() { + let opt_response = match response { + ConnectedResponse::Scrape(r) => pending_scrape_responses + .add_and_get_finished(r) + .map(Response::Scrape), + ConnectedResponse::AnnounceIpv4(r) => Some(Response::AnnounceIpv4(r)), + ConnectedResponse::AnnounceIpv6(r) => Some(Response::AnnounceIpv6(r)), + }; + + if let Some(response) = opt_response { + send_response(state, config, socket, buffer, response, addr); + } + } +} + +fn send_response( + state: &State, + config: &Config, + socket: &mut UdpSocket, + buffer: &mut [u8], + response: Response, + addr: CanonicalSocketAddr, +) { + let mut cursor = Cursor::new(buffer); + + let canonical_addr_is_ipv4 = addr.is_ipv4(); + + let addr = if config.network.address.is_ipv4() { + addr.get_ipv4() + .expect("found peer ipv6 address while running bound to ipv4 address") + } else { + addr.get_ipv6_mapped() + }; + + match response.write(&mut cursor) { + Ok(()) => { + let amt = cursor.position() as usize; + + match socket.send_to(&cursor.get_ref()[..amt], addr) { + Ok(amt) if config.statistics.active() => { + let stats = if canonical_addr_is_ipv4 { + &state.statistics_ipv4 + } else { + &state.statistics_ipv6 + }; + + stats.bytes_sent.fetch_add(amt, Ordering::Relaxed); + + match response { + Response::Connect(_) => { + stats.responses_sent_connect.fetch_add(1, Ordering::Relaxed); + } + Response::AnnounceIpv4(_) | Response::AnnounceIpv6(_) => { + stats + .responses_sent_announce + .fetch_add(1, Ordering::Relaxed); + } + Response::Scrape(_) => { + stats.responses_sent_scrape.fetch_add(1, Ordering::Relaxed); + } + Response::Error(_) => { + stats.responses_sent_error.fetch_add(1, Ordering::Relaxed); + } + } + } + Ok(_) => {} + Err(err) => { + ::log::warn!("send_to error: {:#}", err); + } + } + } + Err(err) => { + ::log::error!("Response::write error: {:?}", err); + } + } +} From 313b73daef3c9e3462b6e67b5ab45aa911d7b892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 00:25:36 +0200 Subject: [PATCH 36/46] udp: split workers/request.rs into modules --- .../workers/{request.rs => request/mod.rs} | 112 +---------------- aquatic_udp/src/workers/request/storage.rs | 116 ++++++++++++++++++ 2 files changed, 120 insertions(+), 108 deletions(-) rename aquatic_udp/src/workers/{request.rs => request/mod.rs} (78%) create mode 100644 aquatic_udp/src/workers/request/storage.rs diff --git a/aquatic_udp/src/workers/request.rs b/aquatic_udp/src/workers/request/mod.rs similarity index 78% rename from aquatic_udp/src/workers/request.rs rename to aquatic_udp/src/workers/request/mod.rs index d6041f9..052ca4c 100644 --- a/aquatic_udp/src/workers/request.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -1,15 +1,11 @@ +mod storage; + use std::collections::BTreeMap; use std::net::IpAddr; -use std::net::Ipv4Addr; -use std::net::Ipv6Addr; use std::sync::atomic::Ordering; -use std::sync::Arc; use std::time::Duration; use std::time::Instant; -use aquatic_common::access_list::create_access_list_cache; -use aquatic_common::access_list::AccessListArcSwap; -use aquatic_common::AmortizedIndexMap; use aquatic_common::CanonicalSocketAddr; use aquatic_common::PanicSentinel; use aquatic_common::ValidUntil; @@ -23,107 +19,7 @@ use aquatic_udp_protocol::*; use crate::common::*; use crate::config::Config; -#[derive(Clone, Debug)] -struct Peer { - pub ip_address: I, - pub port: Port, - pub status: PeerStatus, - pub valid_until: ValidUntil, -} - -impl Peer { - fn to_response_peer(&self) -> ResponsePeer { - ResponsePeer { - ip_address: self.ip_address, - port: self.port, - } - } -} - -type PeerMap = AmortizedIndexMap>; - -struct TorrentData { - pub peers: PeerMap, - pub num_seeders: usize, - pub num_leechers: usize, -} - -impl Default for TorrentData { - fn default() -> Self { - Self { - peers: Default::default(), - num_seeders: 0, - num_leechers: 0, - } - } -} - -type TorrentMap = AmortizedIndexMap>; - -#[derive(Default)] -struct TorrentMaps { - pub ipv4: TorrentMap, - pub ipv6: TorrentMap, -} - -impl TorrentMaps { - /// Remove disallowed and inactive torrents - pub fn clean(&mut self, config: &Config, access_list: &Arc) { - let now = Instant::now(); - let access_list_mode = config.access_list.mode; - - let mut access_list_cache = create_access_list_cache(access_list); - - self.ipv4.retain(|info_hash, torrent| { - access_list_cache - .load() - .allows(access_list_mode, &info_hash.0) - && Self::clean_torrent_and_peers(now, torrent) - }); - self.ipv4.shrink_to_fit(); - - self.ipv6.retain(|info_hash, torrent| { - access_list_cache - .load() - .allows(access_list_mode, &info_hash.0) - && Self::clean_torrent_and_peers(now, torrent) - }); - self.ipv6.shrink_to_fit(); - } - - /// Returns true if torrent is to be kept - #[inline] - fn clean_torrent_and_peers(now: Instant, torrent: &mut TorrentData) -> bool { - let num_seeders = &mut torrent.num_seeders; - let num_leechers = &mut torrent.num_leechers; - - torrent.peers.retain(|_, peer| { - if peer.valid_until.0 > now { - true - } else { - match peer.status { - PeerStatus::Seeding => { - *num_seeders -= 1; - } - PeerStatus::Leeching => { - *num_leechers -= 1; - } - _ => (), - }; - - false - } - }); - - if torrent.peers.is_empty() { - false - } else { - torrent.peers.shrink_to_fit(); - - true - } - } -} +use storage::{Peer, TorrentMap, TorrentMaps}; pub fn run_request_worker( _sentinel: PanicSentinel, @@ -388,7 +284,7 @@ mod tests { let gen_num_peers = data.0 as u32; let req_num_peers = data.1 as usize; - let mut peer_map: PeerMap = Default::default(); + let mut peer_map: storage::PeerMap = Default::default(); let mut opt_sender_key = None; let mut opt_sender_peer = None; diff --git a/aquatic_udp/src/workers/request/storage.rs b/aquatic_udp/src/workers/request/storage.rs new file mode 100644 index 0000000..e558ad7 --- /dev/null +++ b/aquatic_udp/src/workers/request/storage.rs @@ -0,0 +1,116 @@ +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::sync::Arc; +use std::time::Instant; + +use aquatic_common::access_list::create_access_list_cache; +use aquatic_common::access_list::AccessListArcSwap; +use aquatic_common::AmortizedIndexMap; +use aquatic_common::ValidUntil; + +use aquatic_udp_protocol::*; + +use crate::common::*; +use crate::config::Config; + +#[derive(Clone, Debug)] +pub struct Peer { + pub ip_address: I, + pub port: Port, + pub status: PeerStatus, + pub valid_until: ValidUntil, +} + +impl Peer { + pub fn to_response_peer(&self) -> ResponsePeer { + ResponsePeer { + ip_address: self.ip_address, + port: self.port, + } + } +} + +pub type PeerMap = AmortizedIndexMap>; + +pub struct TorrentData { + pub peers: PeerMap, + pub num_seeders: usize, + pub num_leechers: usize, +} + +impl Default for TorrentData { + fn default() -> Self { + Self { + peers: Default::default(), + num_seeders: 0, + num_leechers: 0, + } + } +} + +pub type TorrentMap = AmortizedIndexMap>; + +#[derive(Default)] +pub struct TorrentMaps { + pub ipv4: TorrentMap, + pub ipv6: TorrentMap, +} + +impl TorrentMaps { + /// Remove disallowed and inactive torrents + pub fn clean(&mut self, config: &Config, access_list: &Arc) { + let now = Instant::now(); + let access_list_mode = config.access_list.mode; + + let mut access_list_cache = create_access_list_cache(access_list); + + self.ipv4.retain(|info_hash, torrent| { + access_list_cache + .load() + .allows(access_list_mode, &info_hash.0) + && Self::clean_torrent_and_peers(now, torrent) + }); + self.ipv4.shrink_to_fit(); + + self.ipv6.retain(|info_hash, torrent| { + access_list_cache + .load() + .allows(access_list_mode, &info_hash.0) + && Self::clean_torrent_and_peers(now, torrent) + }); + self.ipv6.shrink_to_fit(); + } + + /// Returns true if torrent is to be kept + #[inline] + fn clean_torrent_and_peers(now: Instant, torrent: &mut TorrentData) -> bool { + let num_seeders = &mut torrent.num_seeders; + let num_leechers = &mut torrent.num_leechers; + + torrent.peers.retain(|_, peer| { + if peer.valid_until.0 > now { + true + } else { + match peer.status { + PeerStatus::Seeding => { + *num_seeders -= 1; + } + PeerStatus::Leeching => { + *num_leechers -= 1; + } + _ => (), + }; + + false + } + }); + + if torrent.peers.is_empty() { + false + } else { + torrent.peers.shrink_to_fit(); + + true + } + } +} From d0c6fb0e298c5a9efb8a9280f7560bce275e3c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 00:29:50 +0200 Subject: [PATCH 37/46] udp: request workers: move some code into run_request_worker --- aquatic_udp/src/workers/request/mod.rs | 58 +++++++++----------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/aquatic_udp/src/workers/request/mod.rs b/aquatic_udp/src/workers/request/mod.rs index 052ca4c..6c06d69 100644 --- a/aquatic_udp/src/workers/request/mod.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -30,7 +30,7 @@ pub fn run_request_worker( worker_index: RequestWorkerIndex, ) { let mut torrents = TorrentMaps::default(); - let mut small_rng = SmallRng::from_entropy(); + let mut rng = SmallRng::from_entropy(); let timeout = Duration::from_millis(config.request_channel_recv_timeout_ms); let mut peer_valid_until = ValidUntil::new(config.cleaning.max_peer_age); @@ -46,14 +46,24 @@ pub fn run_request_worker( loop { if let Ok((sender_index, request, src)) = request_receiver.recv_timeout(timeout) { let response = match request { - ConnectedRequest::Announce(request) => handle_announce_request( - &config, - &mut small_rng, - &mut torrents, - request, - src, - peer_valid_until, - ), + ConnectedRequest::Announce(request) => match src.get().ip() { + IpAddr::V4(ip) => ConnectedResponse::AnnounceIpv4(handle_announce_request( + &config, + &mut rng, + &mut torrents.ipv4, + request, + ip, + peer_valid_until, + )), + IpAddr::V6(ip) => ConnectedResponse::AnnounceIpv6(handle_announce_request( + &config, + &mut rng, + &mut torrents.ipv6, + request, + ip, + peer_valid_until, + )), + }, ConnectedRequest::Scrape(request) => { ConnectedResponse::Scrape(handle_scrape_request(&mut torrents, src, request)) } @@ -98,35 +108,7 @@ pub fn run_request_worker( } } -fn handle_announce_request( - config: &Config, - rng: &mut SmallRng, - torrents: &mut TorrentMaps, - request: AnnounceRequest, - src: CanonicalSocketAddr, - peer_valid_until: ValidUntil, -) -> ConnectedResponse { - match src.get().ip() { - IpAddr::V4(ip) => ConnectedResponse::AnnounceIpv4(handle_announce_request_inner( - config, - rng, - &mut torrents.ipv4, - request, - ip, - peer_valid_until, - )), - IpAddr::V6(ip) => ConnectedResponse::AnnounceIpv6(handle_announce_request_inner( - config, - rng, - &mut torrents.ipv6, - request, - ip, - peer_valid_until, - )), - } -} - -fn handle_announce_request_inner( +fn handle_announce_request( config: &Config, rng: &mut SmallRng, torrents: &mut TorrentMap, From 043649d122ba4e777c56b1eb07fdcced7ef61103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 00:34:45 +0200 Subject: [PATCH 38/46] udp: fix name of test_pending_scrape_response_map --- aquatic_udp/src/workers/socket/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aquatic_udp/src/workers/socket/common.rs b/aquatic_udp/src/workers/socket/common.rs index 1d248d7..4717daf 100644 --- a/aquatic_udp/src/workers/socket/common.rs +++ b/aquatic_udp/src/workers/socket/common.rs @@ -126,7 +126,7 @@ mod tests { use super::*; #[quickcheck] - fn test_pending_scrape_response_map( + fn test_pending_scrape_response_slab( request_data: Vec<(i32, i64, u8)>, request_workers: u8, ) -> TestResult { From 1851886992f4eae587b8d2bcce26797a564ab3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 00:36:43 +0200 Subject: [PATCH 39/46] udp: socket workers: rename common.rs to storage.rs --- aquatic_udp/src/workers/socket/mod.rs | 4 ++-- aquatic_udp/src/workers/socket/requests.rs | 2 +- aquatic_udp/src/workers/socket/responses.rs | 2 +- aquatic_udp/src/workers/socket/{common.rs => storage.rs} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename aquatic_udp/src/workers/socket/{common.rs => storage.rs} (100%) diff --git a/aquatic_udp/src/workers/socket/mod.rs b/aquatic_udp/src/workers/socket/mod.rs index 78e79bd..cf8290d 100644 --- a/aquatic_udp/src/workers/socket/mod.rs +++ b/aquatic_udp/src/workers/socket/mod.rs @@ -1,6 +1,6 @@ -mod common; mod requests; mod responses; +mod storage; use std::time::{Duration, Instant}; @@ -19,9 +19,9 @@ use socket2::{Domain, Protocol, Socket, Type}; use crate::common::*; use crate::config::Config; -use self::common::PendingScrapeResponseSlab; use self::requests::read_requests; use self::responses::send_responses; +use self::storage::PendingScrapeResponseSlab; pub fn run_socket_worker( _sentinel: PanicSentinel, diff --git a/aquatic_udp/src/workers/socket/requests.rs b/aquatic_udp/src/workers/socket/requests.rs index 9eead98..cc71eab 100644 --- a/aquatic_udp/src/workers/socket/requests.rs +++ b/aquatic_udp/src/workers/socket/requests.rs @@ -11,7 +11,7 @@ use aquatic_udp_protocol::*; use crate::common::*; use crate::config::Config; -use super::common::PendingScrapeResponseSlab; +use super::storage::PendingScrapeResponseSlab; pub fn read_requests( config: &Config, diff --git a/aquatic_udp/src/workers/socket/responses.rs b/aquatic_udp/src/workers/socket/responses.rs index 112c2fe..7bb5510 100644 --- a/aquatic_udp/src/workers/socket/responses.rs +++ b/aquatic_udp/src/workers/socket/responses.rs @@ -11,7 +11,7 @@ use aquatic_udp_protocol::*; use crate::common::*; use crate::config::Config; -use super::common::PendingScrapeResponseSlab; +use super::storage::PendingScrapeResponseSlab; pub fn send_responses( state: &State, diff --git a/aquatic_udp/src/workers/socket/common.rs b/aquatic_udp/src/workers/socket/storage.rs similarity index 100% rename from aquatic_udp/src/workers/socket/common.rs rename to aquatic_udp/src/workers/socket/storage.rs From 29f97e881ecf6d819e0d3045fd1281210a65fdb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 00:43:23 +0200 Subject: [PATCH 40/46] udp: improve formatting of imports --- aquatic_udp/src/workers/request/mod.rs | 5 +---- aquatic_udp/src/workers/request/storage.rs | 8 ++++---- aquatic_udp/src/workers/socket/mod.rs | 18 +++++++++--------- aquatic_udp/src/workers/socket/requests.rs | 4 +--- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/aquatic_udp/src/workers/request/mod.rs b/aquatic_udp/src/workers/request/mod.rs index 6c06d69..b45b8be 100644 --- a/aquatic_udp/src/workers/request/mod.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -6,13 +6,10 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; -use aquatic_common::CanonicalSocketAddr; -use aquatic_common::PanicSentinel; -use aquatic_common::ValidUntil; use crossbeam_channel::Receiver; use rand::{rngs::SmallRng, SeedableRng}; -use aquatic_common::extract_response_peers; +use aquatic_common::{extract_response_peers, CanonicalSocketAddr, PanicSentinel, ValidUntil}; use aquatic_udp_protocol::*; diff --git a/aquatic_udp/src/workers/request/storage.rs b/aquatic_udp/src/workers/request/storage.rs index e558ad7..05052ba 100644 --- a/aquatic_udp/src/workers/request/storage.rs +++ b/aquatic_udp/src/workers/request/storage.rs @@ -3,10 +3,10 @@ use std::net::Ipv6Addr; use std::sync::Arc; use std::time::Instant; -use aquatic_common::access_list::create_access_list_cache; -use aquatic_common::access_list::AccessListArcSwap; -use aquatic_common::AmortizedIndexMap; -use aquatic_common::ValidUntil; +use aquatic_common::{ + access_list::{create_access_list_cache, AccessListArcSwap}, + AmortizedIndexMap, ValidUntil, +}; use aquatic_udp_protocol::*; diff --git a/aquatic_udp/src/workers/socket/mod.rs b/aquatic_udp/src/workers/socket/mod.rs index cf8290d..d53d8b4 100644 --- a/aquatic_udp/src/workers/socket/mod.rs +++ b/aquatic_udp/src/workers/socket/mod.rs @@ -5,23 +5,23 @@ mod storage; use std::time::{Duration, Instant}; use anyhow::Context; -use aquatic_common::privileges::PrivilegeDropper; use crossbeam_channel::Receiver; use mio::net::UdpSocket; use mio::{Events, Interest, Poll, Token}; - -use aquatic_common::access_list::create_access_list_cache; -use aquatic_common::CanonicalSocketAddr; -use aquatic_common::{PanicSentinel, ValidUntil}; -use aquatic_udp_protocol::*; use socket2::{Domain, Protocol, Socket, Type}; +use aquatic_common::{ + access_list::create_access_list_cache, privileges::PrivilegeDropper, CanonicalSocketAddr, + PanicSentinel, ValidUntil, +}; +use aquatic_udp_protocol::*; + use crate::common::*; use crate::config::Config; -use self::requests::read_requests; -use self::responses::send_responses; -use self::storage::PendingScrapeResponseSlab; +use requests::read_requests; +use responses::send_responses; +use storage::PendingScrapeResponseSlab; pub fn run_socket_worker( _sentinel: PanicSentinel, diff --git a/aquatic_udp/src/workers/socket/requests.rs b/aquatic_udp/src/workers/socket/requests.rs index cc71eab..610f0ae 100644 --- a/aquatic_udp/src/workers/socket/requests.rs +++ b/aquatic_udp/src/workers/socket/requests.rs @@ -3,9 +3,7 @@ use std::sync::atomic::Ordering; use mio::net::UdpSocket; -use aquatic_common::access_list::AccessListCache; -use aquatic_common::CanonicalSocketAddr; -use aquatic_common::ValidUntil; +use aquatic_common::{access_list::AccessListCache, CanonicalSocketAddr, ValidUntil}; use aquatic_udp_protocol::*; use crate::common::*; From 78266fd3e7dbf50527b920b1ac3701b99e0f8b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 00:52:34 +0200 Subject: [PATCH 41/46] udp: move some TorrentMap cleaning code to TorrentData impl --- aquatic_udp/src/workers/request/storage.rs | 67 +++++++++++----------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/aquatic_udp/src/workers/request/storage.rs b/aquatic_udp/src/workers/request/storage.rs index 05052ba..b5eaafd 100644 --- a/aquatic_udp/src/workers/request/storage.rs +++ b/aquatic_udp/src/workers/request/storage.rs @@ -38,6 +38,36 @@ pub struct TorrentData { pub num_leechers: usize, } +impl TorrentData { + fn clean_and_check_if_has_peers(&mut self, now: Instant) -> bool { + self.peers.retain(|_, peer| { + if peer.valid_until.0 > now { + true + } else { + match peer.status { + PeerStatus::Seeding => { + self.num_seeders -= 1; + } + PeerStatus::Leeching => { + self.num_leechers -= 1; + } + _ => (), + }; + + false + } + }); + + if self.peers.is_empty() { + false + } else { + self.peers.shrink_to_fit(); + + true + } + } +} + impl Default for TorrentData { fn default() -> Self { Self { @@ -68,7 +98,7 @@ impl TorrentMaps { access_list_cache .load() .allows(access_list_mode, &info_hash.0) - && Self::clean_torrent_and_peers(now, torrent) + && torrent.clean_and_check_if_has_peers(now) }); self.ipv4.shrink_to_fit(); @@ -76,41 +106,8 @@ impl TorrentMaps { access_list_cache .load() .allows(access_list_mode, &info_hash.0) - && Self::clean_torrent_and_peers(now, torrent) + && torrent.clean_and_check_if_has_peers(now) }); self.ipv6.shrink_to_fit(); } - - /// Returns true if torrent is to be kept - #[inline] - fn clean_torrent_and_peers(now: Instant, torrent: &mut TorrentData) -> bool { - let num_seeders = &mut torrent.num_seeders; - let num_leechers = &mut torrent.num_leechers; - - torrent.peers.retain(|_, peer| { - if peer.valid_until.0 > now { - true - } else { - match peer.status { - PeerStatus::Seeding => { - *num_seeders -= 1; - } - PeerStatus::Leeching => { - *num_leechers -= 1; - } - _ => (), - }; - - false - } - }); - - if torrent.peers.is_empty() { - false - } else { - torrent.peers.shrink_to_fit(); - - true - } - } } From 9fedf82113cb53b32754907953ddd3e78a6ba2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 01:59:36 +0200 Subject: [PATCH 42/46] udp: improve TorrentMap cleaning code, always count peers --- aquatic_udp/src/workers/request/mod.rs | 22 ++--- aquatic_udp/src/workers/request/storage.rs | 97 +++++++++++++++------- 2 files changed, 74 insertions(+), 45 deletions(-) diff --git a/aquatic_udp/src/workers/request/mod.rs b/aquatic_udp/src/workers/request/mod.rs index b45b8be..4b47f4a 100644 --- a/aquatic_udp/src/workers/request/mod.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -69,22 +69,18 @@ pub fn run_request_worker( response_sender.try_send_to(sender_index, response, src); } + // Run periodic tasks if iter_counter % 128 == 0 { let now = Instant::now(); peer_valid_until = ValidUntil::new_with_now(now, config.cleaning.max_peer_age); if now > last_cleaning + cleaning_interval { - torrents.clean(&config, &state.access_list); + let (ipv4, ipv6) = torrents.clean_and_get_num_peers(&config, &state.access_list); if config.statistics.active() { - let peers_ipv4 = torrents.ipv4.values().map(|t| t.peers.len()).sum(); - let peers_ipv6 = torrents.ipv6.values().map(|t| t.peers.len()).sum(); - - state.statistics_ipv4.peers[worker_index.0] - .store(peers_ipv4, Ordering::Release); - state.statistics_ipv6.peers[worker_index.0] - .store(peers_ipv6, Ordering::Release); + state.statistics_ipv4.peers[worker_index.0].store(ipv4, Ordering::Release); + state.statistics_ipv6.peers[worker_index.0].store(ipv6, Ordering::Release); } last_cleaning = now; @@ -93,9 +89,9 @@ pub fn run_request_worker( && now > last_statistics_update + statistics_update_interval { state.statistics_ipv4.torrents[worker_index.0] - .store(torrents.ipv4.len(), Ordering::Release); + .store(torrents.ipv4.num_torrents(), Ordering::Release); state.statistics_ipv6.torrents[worker_index.0] - .store(torrents.ipv6.len(), Ordering::Release); + .store(torrents.ipv6.num_torrents(), Ordering::Release); last_statistics_update = now; } @@ -122,7 +118,7 @@ fn handle_announce_request( valid_until: peer_valid_until, }; - let torrent_data = torrents.entry(request.info_hash).or_default(); + let torrent_data = torrents.0.entry(request.info_hash).or_default(); let opt_removed_peer = match peer_status { PeerStatus::Leeching => { @@ -190,7 +186,7 @@ fn handle_scrape_request( if src.is_ipv4() { torrent_stats.extend(request.info_hashes.into_iter().map(|(i, info_hash)| { - let s = if let Some(torrent_data) = torrents.ipv4.get(&info_hash) { + let s = if let Some(torrent_data) = torrents.ipv4.0.get(&info_hash) { create_torrent_scrape_statistics( torrent_data.num_seeders as i32, torrent_data.num_leechers as i32, @@ -203,7 +199,7 @@ fn handle_scrape_request( })); } else { torrent_stats.extend(request.info_hashes.into_iter().map(|(i, info_hash)| { - let s = if let Some(torrent_data) = torrents.ipv6.get(&info_hash) { + let s = if let Some(torrent_data) = torrents.ipv6.0.get(&info_hash) { create_torrent_scrape_statistics( torrent_data.num_seeders as i32, torrent_data.num_leechers as i32, diff --git a/aquatic_udp/src/workers/request/storage.rs b/aquatic_udp/src/workers/request/storage.rs index b5eaafd..8280be7 100644 --- a/aquatic_udp/src/workers/request/storage.rs +++ b/aquatic_udp/src/workers/request/storage.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::time::Instant; use aquatic_common::{ - access_list::{create_access_list_cache, AccessListArcSwap}, + access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache, AccessListMode}, AmortizedIndexMap, ValidUntil, }; @@ -39,7 +39,8 @@ pub struct TorrentData { } impl TorrentData { - fn clean_and_check_if_has_peers(&mut self, now: Instant) -> bool { + /// Remove inactive peers and reclaim space + fn clean(&mut self, now: Instant) { self.peers.retain(|_, peer| { if peer.valid_until.0 > now { true @@ -58,12 +59,8 @@ impl TorrentData { } }); - if self.peers.is_empty() { - false - } else { + if !self.peers.is_empty() { self.peers.shrink_to_fit(); - - true } } } @@ -78,36 +75,72 @@ impl Default for TorrentData { } } -pub type TorrentMap = AmortizedIndexMap>; - #[derive(Default)] +pub struct TorrentMap(pub AmortizedIndexMap>); + +impl TorrentMap { + /// Remove forbidden or inactive torrents, reclaim space and return number of remaining peers + fn clean_and_get_num_peers( + &mut self, + access_list_cache: &mut AccessListCache, + access_list_mode: AccessListMode, + now: Instant, + ) -> usize { + let mut num_peers = 0; + + self.0.retain(|info_hash, torrent| { + if !access_list_cache + .load() + .allows(access_list_mode, &info_hash.0) + { + return false; + } + + torrent.clean(now); + + num_peers += torrent.peers.len(); + + !torrent.peers.is_empty() + }); + + self.0.shrink_to_fit(); + + num_peers + } + + pub fn num_torrents(&self) -> usize { + self.0.len() + } +} + pub struct TorrentMaps { pub ipv4: TorrentMap, pub ipv6: TorrentMap, } -impl TorrentMaps { - /// Remove disallowed and inactive torrents - pub fn clean(&mut self, config: &Config, access_list: &Arc) { - let now = Instant::now(); - let access_list_mode = config.access_list.mode; - - let mut access_list_cache = create_access_list_cache(access_list); - - self.ipv4.retain(|info_hash, torrent| { - access_list_cache - .load() - .allows(access_list_mode, &info_hash.0) - && torrent.clean_and_check_if_has_peers(now) - }); - self.ipv4.shrink_to_fit(); - - self.ipv6.retain(|info_hash, torrent| { - access_list_cache - .load() - .allows(access_list_mode, &info_hash.0) - && torrent.clean_and_check_if_has_peers(now) - }); - self.ipv6.shrink_to_fit(); +impl Default for TorrentMaps { + fn default() -> Self { + Self { + ipv4: TorrentMap(Default::default()), + ipv6: TorrentMap(Default::default()), + } + } +} + +impl TorrentMaps { + /// Remove forbidden or inactive torrents, reclaim space and return number of remaining peers + pub fn clean_and_get_num_peers( + &mut self, + config: &Config, + access_list: &Arc, + ) -> (usize, usize) { + let mut cache = create_access_list_cache(access_list); + let mode = config.access_list.mode; + let now = Instant::now(); + + let ipv4 = self.ipv4.clean_and_get_num_peers(&mut cache, mode, now); + let ipv6 = self.ipv6.clean_and_get_num_peers(&mut cache, mode, now); + + (ipv4, ipv6) } } From 1025391e4fe65caa6ce50362c142c247db5aceb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 02:10:52 +0200 Subject: [PATCH 43/46] udp: request worker: remove layer of branching --- aquatic_udp/src/workers/request/mod.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/aquatic_udp/src/workers/request/mod.rs b/aquatic_udp/src/workers/request/mod.rs index 4b47f4a..fb83988 100644 --- a/aquatic_udp/src/workers/request/mod.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -42,27 +42,33 @@ pub fn run_request_worker( loop { if let Ok((sender_index, request, src)) = request_receiver.recv_timeout(timeout) { - let response = match request { - ConnectedRequest::Announce(request) => match src.get().ip() { - IpAddr::V4(ip) => ConnectedResponse::AnnounceIpv4(handle_announce_request( + let response = match (request, src.get().ip()) { + (ConnectedRequest::Announce(request), IpAddr::V4(ip)) => { + let response = handle_announce_request( &config, &mut rng, &mut torrents.ipv4, request, ip, peer_valid_until, - )), - IpAddr::V6(ip) => ConnectedResponse::AnnounceIpv6(handle_announce_request( + ); + + ConnectedResponse::AnnounceIpv4(response) + } + (ConnectedRequest::Announce(request), IpAddr::V6(ip)) => { + let response = handle_announce_request( &config, &mut rng, &mut torrents.ipv6, request, ip, peer_valid_until, - )), - }, - ConnectedRequest::Scrape(request) => { - ConnectedResponse::Scrape(handle_scrape_request(&mut torrents, src, request)) + ); + + ConnectedResponse::AnnounceIpv6(response) + } + (ConnectedRequest::Scrape(request), ip) => { + ConnectedResponse::Scrape(handle_scrape_request(&mut torrents, ip, request)) } }; @@ -177,7 +183,7 @@ fn calc_max_num_peers_to_take(config: &Config, peers_wanted: i32) -> usize { fn handle_scrape_request( torrents: &mut TorrentMaps, - src: CanonicalSocketAddr, + src: IpAddr, request: PendingScrapeRequest, ) -> PendingScrapeResponse { const EMPTY_STATS: TorrentScrapeStatistics = create_torrent_scrape_statistics(0, 0); From b8a74f0724a26b1a59ca88fc6de320086946d467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 02:21:19 +0200 Subject: [PATCH 44/46] udp: make handle_scrape_request take protocol-specific TorrentMap --- aquatic_udp/src/workers/request/mod.rs | 40 +++++++++----------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/aquatic_udp/src/workers/request/mod.rs b/aquatic_udp/src/workers/request/mod.rs index fb83988..39b1517 100644 --- a/aquatic_udp/src/workers/request/mod.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -1,6 +1,5 @@ mod storage; -use std::collections::BTreeMap; use std::net::IpAddr; use std::sync::atomic::Ordering; use std::time::Duration; @@ -67,8 +66,11 @@ pub fn run_request_worker( ConnectedResponse::AnnounceIpv6(response) } - (ConnectedRequest::Scrape(request), ip) => { - ConnectedResponse::Scrape(handle_scrape_request(&mut torrents, ip, request)) + (ConnectedRequest::Scrape(request), IpAddr::V4(_)) => { + ConnectedResponse::Scrape(handle_scrape_request(&mut torrents.ipv4, request)) + } + (ConnectedRequest::Scrape(request), IpAddr::V6(_)) => { + ConnectedResponse::Scrape(handle_scrape_request(&mut torrents.ipv6, request)) } }; @@ -181,18 +183,17 @@ fn calc_max_num_peers_to_take(config: &Config, peers_wanted: i32) -> usize { } } -fn handle_scrape_request( - torrents: &mut TorrentMaps, - src: IpAddr, +fn handle_scrape_request( + torrents: &mut TorrentMap, request: PendingScrapeRequest, ) -> PendingScrapeResponse { const EMPTY_STATS: TorrentScrapeStatistics = create_torrent_scrape_statistics(0, 0); - let mut torrent_stats: BTreeMap = BTreeMap::new(); - - if src.is_ipv4() { - torrent_stats.extend(request.info_hashes.into_iter().map(|(i, info_hash)| { - let s = if let Some(torrent_data) = torrents.ipv4.0.get(&info_hash) { + let torrent_stats = request + .info_hashes + .into_iter() + .map(|(i, info_hash)| { + let s = if let Some(torrent_data) = torrents.0.get(&info_hash) { create_torrent_scrape_statistics( torrent_data.num_seeders as i32, torrent_data.num_leechers as i32, @@ -202,21 +203,8 @@ fn handle_scrape_request( }; (i, s) - })); - } else { - torrent_stats.extend(request.info_hashes.into_iter().map(|(i, info_hash)| { - let s = if let Some(torrent_data) = torrents.ipv6.0.get(&info_hash) { - create_torrent_scrape_statistics( - torrent_data.num_seeders as i32, - torrent_data.num_leechers as i32, - ) - } else { - EMPTY_STATS - }; - - (i, s) - })); - } + }) + .collect(); PendingScrapeResponse { slab_key: request.slab_key, From 80171170c856d7d8623828cd900c50e20f4367f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 02:28:03 +0200 Subject: [PATCH 45/46] udp: request workers: improve handle_scrape_request code --- aquatic_udp/src/workers/request/mod.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/aquatic_udp/src/workers/request/mod.rs b/aquatic_udp/src/workers/request/mod.rs index 39b1517..78cd2e3 100644 --- a/aquatic_udp/src/workers/request/mod.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -193,16 +193,18 @@ fn handle_scrape_request( .info_hashes .into_iter() .map(|(i, info_hash)| { - let s = if let Some(torrent_data) = torrents.0.get(&info_hash) { - create_torrent_scrape_statistics( - torrent_data.num_seeders as i32, - torrent_data.num_leechers as i32, - ) - } else { - EMPTY_STATS - }; + let stats = torrents + .0 + .get(&info_hash) + .map(|torrent_data| { + create_torrent_scrape_statistics( + torrent_data.num_seeders as i32, + torrent_data.num_leechers as i32, + ) + }) + .unwrap_or(EMPTY_STATS); - (i, s) + (i, stats) }) .collect(); From 8451b2c50f4949ee9050567c915458fca59ae2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 16 Apr 2022 03:07:23 +0200 Subject: [PATCH 46/46] udp: request workers: handle TorrentData updates in methods --- aquatic_udp/src/workers/request/mod.rs | 163 ++------------------- aquatic_udp/src/workers/request/storage.rs | 159 +++++++++++++++++++- 2 files changed, 171 insertions(+), 151 deletions(-) diff --git a/aquatic_udp/src/workers/request/mod.rs b/aquatic_udp/src/workers/request/mod.rs index 78cd2e3..1b8335a 100644 --- a/aquatic_udp/src/workers/request/mod.rs +++ b/aquatic_udp/src/workers/request/mod.rs @@ -8,7 +8,7 @@ use std::time::Instant; use crossbeam_channel::Receiver; use rand::{rngs::SmallRng, SeedableRng}; -use aquatic_common::{extract_response_peers, CanonicalSocketAddr, PanicSentinel, ValidUntil}; +use aquatic_common::{CanonicalSocketAddr, PanicSentinel, ValidUntil}; use aquatic_udp_protocol::*; @@ -117,72 +117,38 @@ fn handle_announce_request( peer_ip: I, peer_valid_until: ValidUntil, ) -> AnnounceResponse { - let peer_status = PeerStatus::from_event_and_bytes_left(request.event, request.bytes_left); + let max_num_peers_to_take = if request.peers_wanted.0 <= 0 { + config.protocol.max_response_peers as usize + } else { + ::std::cmp::min( + config.protocol.max_response_peers as usize, + request.peers_wanted.0.try_into().unwrap(), + ) + }; let peer = Peer { ip_address: peer_ip, port: request.port, - status: peer_status, + status: PeerStatus::from_event_and_bytes_left(request.event, request.bytes_left), valid_until: peer_valid_until, }; let torrent_data = torrents.0.entry(request.info_hash).or_default(); - let opt_removed_peer = match peer_status { - PeerStatus::Leeching => { - torrent_data.num_leechers += 1; + torrent_data.update_peer(request.peer_id, peer); - torrent_data.peers.insert(request.peer_id, peer) - } - PeerStatus::Seeding => { - torrent_data.num_seeders += 1; - - torrent_data.peers.insert(request.peer_id, peer) - } - PeerStatus::Stopped => torrent_data.peers.remove(&request.peer_id), - }; - - match opt_removed_peer.map(|peer| peer.status) { - Some(PeerStatus::Leeching) => { - torrent_data.num_leechers -= 1; - } - Some(PeerStatus::Seeding) => { - torrent_data.num_seeders -= 1; - } - _ => {} - } - - let max_num_peers_to_take = calc_max_num_peers_to_take(config, request.peers_wanted.0); - - let response_peers = extract_response_peers( - rng, - &torrent_data.peers, - max_num_peers_to_take, - request.peer_id, - Peer::to_response_peer, - ); + let response_peers = + torrent_data.extract_response_peers(rng, request.peer_id, max_num_peers_to_take); AnnounceResponse { transaction_id: request.transaction_id, announce_interval: AnnounceInterval(config.protocol.peer_announce_interval), - leechers: NumberOfPeers(torrent_data.num_leechers as i32), - seeders: NumberOfPeers(torrent_data.num_seeders as i32), + leechers: NumberOfPeers(torrent_data.num_leechers() as i32), + seeders: NumberOfPeers(torrent_data.num_seeders() as i32), peers: response_peers, } } -#[inline] -fn calc_max_num_peers_to_take(config: &Config, peers_wanted: i32) -> usize { - if peers_wanted <= 0 { - config.protocol.max_response_peers as usize - } else { - ::std::cmp::min( - config.protocol.max_response_peers as usize, - peers_wanted as usize, - ) - } -} - fn handle_scrape_request( torrents: &mut TorrentMap, request: PendingScrapeRequest, @@ -196,12 +162,7 @@ fn handle_scrape_request( let stats = torrents .0 .get(&info_hash) - .map(|torrent_data| { - create_torrent_scrape_statistics( - torrent_data.num_seeders as i32, - torrent_data.num_leechers as i32, - ) - }) + .map(|torrent_data| torrent_data.scrape_statistics()) .unwrap_or(EMPTY_STATS); (i, stats) @@ -222,95 +183,3 @@ const fn create_torrent_scrape_statistics(seeders: i32, leechers: i32) -> Torren leechers: NumberOfPeers(leechers), } } - -#[cfg(test)] -mod tests { - use std::collections::HashSet; - use std::net::Ipv4Addr; - - use quickcheck::{quickcheck, TestResult}; - use rand::thread_rng; - - use super::*; - - fn gen_peer_id(i: u32) -> PeerId { - let mut peer_id = PeerId([0; 20]); - - peer_id.0[0..4].copy_from_slice(&i.to_ne_bytes()); - - peer_id - } - fn gen_peer(i: u32) -> Peer { - Peer { - ip_address: Ipv4Addr::from(i.to_be_bytes()), - port: Port(1), - status: PeerStatus::Leeching, - valid_until: ValidUntil::new(0), - } - } - - #[test] - fn test_extract_response_peers() { - fn prop(data: (u16, u16)) -> TestResult { - let gen_num_peers = data.0 as u32; - let req_num_peers = data.1 as usize; - - let mut peer_map: storage::PeerMap = Default::default(); - - let mut opt_sender_key = None; - let mut opt_sender_peer = None; - - for i in 0..gen_num_peers { - let key = gen_peer_id(i); - let peer = gen_peer((i << 16) + i); - - if i == 0 { - opt_sender_key = Some(key); - opt_sender_peer = Some(peer.to_response_peer()); - } - - peer_map.insert(key, peer); - } - - let mut rng = thread_rng(); - - let peers = extract_response_peers( - &mut rng, - &peer_map, - req_num_peers, - opt_sender_key.unwrap_or_else(|| gen_peer_id(1)), - Peer::to_response_peer, - ); - - // Check that number of returned peers is correct - - let mut success = peers.len() <= req_num_peers; - - if req_num_peers >= gen_num_peers as usize { - success &= peers.len() == gen_num_peers as usize - || peers.len() + 1 == gen_num_peers as usize; - } - - // Check that returned peers are unique (no overlap) and that sender - // isn't returned - - let mut ip_addresses = HashSet::with_capacity(peers.len()); - - for peer in peers { - if peer == opt_sender_peer.clone().unwrap() - || ip_addresses.contains(&peer.ip_address) - { - success = false; - - break; - } - - ip_addresses.insert(peer.ip_address); - } - - TestResult::from_bool(success) - } - - quickcheck(prop as fn((u16, u16)) -> TestResult); - } -} diff --git a/aquatic_udp/src/workers/request/storage.rs b/aquatic_udp/src/workers/request/storage.rs index 8280be7..75bc311 100644 --- a/aquatic_udp/src/workers/request/storage.rs +++ b/aquatic_udp/src/workers/request/storage.rs @@ -5,14 +5,17 @@ use std::time::Instant; use aquatic_common::{ access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache, AccessListMode}, - AmortizedIndexMap, ValidUntil, + extract_response_peers, AmortizedIndexMap, ValidUntil, }; use aquatic_udp_protocol::*; +use rand::prelude::SmallRng; use crate::common::*; use crate::config::Config; +use super::create_torrent_scrape_statistics; + #[derive(Clone, Debug)] pub struct Peer { pub ip_address: I, @@ -33,12 +36,68 @@ impl Peer { pub type PeerMap = AmortizedIndexMap>; pub struct TorrentData { - pub peers: PeerMap, - pub num_seeders: usize, - pub num_leechers: usize, + peers: PeerMap, + num_seeders: usize, + num_leechers: usize, } impl TorrentData { + pub fn update_peer(&mut self, peer_id: PeerId, peer: Peer) { + let opt_removed_peer = match peer.status { + PeerStatus::Leeching => { + self.num_leechers += 1; + + self.peers.insert(peer_id, peer) + } + PeerStatus::Seeding => { + self.num_seeders += 1; + + self.peers.insert(peer_id, peer) + } + PeerStatus::Stopped => self.peers.remove(&peer_id), + }; + + match opt_removed_peer.map(|peer| peer.status) { + Some(PeerStatus::Leeching) => { + self.num_leechers -= 1; + } + Some(PeerStatus::Seeding) => { + self.num_seeders -= 1; + } + _ => {} + } + } + + pub fn extract_response_peers( + &self, + rng: &mut SmallRng, + peer_id: PeerId, + max_num_peers_to_take: usize, + ) -> Vec> { + extract_response_peers( + rng, + &self.peers, + max_num_peers_to_take, + peer_id, + Peer::to_response_peer, + ) + } + + pub fn num_leechers(&self) -> usize { + self.num_leechers + } + + pub fn num_seeders(&self) -> usize { + self.num_seeders + } + + pub fn scrape_statistics(&self) -> TorrentScrapeStatistics { + create_torrent_scrape_statistics( + self.num_seeders.try_into().unwrap_or(i32::MAX), + self.num_leechers.try_into().unwrap_or(i32::MAX), + ) + } + /// Remove inactive peers and reclaim space fn clean(&mut self, now: Instant) { self.peers.retain(|_, peer| { @@ -144,3 +203,95 @@ impl TorrentMaps { (ipv4, ipv6) } } + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + use std::net::Ipv4Addr; + + use quickcheck::{quickcheck, TestResult}; + use rand::thread_rng; + + use super::*; + + fn gen_peer_id(i: u32) -> PeerId { + let mut peer_id = PeerId([0; 20]); + + peer_id.0[0..4].copy_from_slice(&i.to_ne_bytes()); + + peer_id + } + fn gen_peer(i: u32) -> Peer { + Peer { + ip_address: Ipv4Addr::from(i.to_be_bytes()), + port: Port(1), + status: PeerStatus::Leeching, + valid_until: ValidUntil::new(0), + } + } + + #[test] + fn test_extract_response_peers() { + fn prop(data: (u16, u16)) -> TestResult { + let gen_num_peers = data.0 as u32; + let req_num_peers = data.1 as usize; + + let mut peer_map: PeerMap = Default::default(); + + let mut opt_sender_key = None; + let mut opt_sender_peer = None; + + for i in 0..gen_num_peers { + let key = gen_peer_id(i); + let peer = gen_peer((i << 16) + i); + + if i == 0 { + opt_sender_key = Some(key); + opt_sender_peer = Some(peer.to_response_peer()); + } + + peer_map.insert(key, peer); + } + + let mut rng = thread_rng(); + + let peers = extract_response_peers( + &mut rng, + &peer_map, + req_num_peers, + opt_sender_key.unwrap_or_else(|| gen_peer_id(1)), + Peer::to_response_peer, + ); + + // Check that number of returned peers is correct + + let mut success = peers.len() <= req_num_peers; + + if req_num_peers >= gen_num_peers as usize { + success &= peers.len() == gen_num_peers as usize + || peers.len() + 1 == gen_num_peers as usize; + } + + // Check that returned peers are unique (no overlap) and that sender + // isn't returned + + let mut ip_addresses = HashSet::with_capacity(peers.len()); + + for peer in peers { + if peer == opt_sender_peer.clone().unwrap() + || ip_addresses.contains(&peer.ip_address) + { + success = false; + + break; + } + + ip_addresses.insert(peer.ip_address); + } + + TestResult::from_bool(success) + } + + quickcheck(prop as fn((u16, u16)) -> TestResult); + } +}