From 23b241bb6dbf16f3f62f46e063c0573e9e8da7a2 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Mon, 12 Sep 2022 01:35:20 +0200 Subject: [PATCH] dev: use clock in minimal connection cookie implementation --- src/lib.rs | 18 +++ src/protocol/clock.rs | 5 + src/protocol/crypto.rs | 84 ++++++++++++++ src/protocol/mod.rs | 1 + src/protocol/utils.rs | 46 +------- src/udp/connection_cookie.rs | 214 +++++++++++++++++++++++++++++++++++ src/udp/handlers.rs | 7 +- src/udp/mod.rs | 1 + src/udp/server.rs | 3 + 9 files changed, 330 insertions(+), 49 deletions(-) create mode 100644 src/protocol/crypto.rs create mode 100644 src/udp/connection_cookie.rs diff --git a/src/lib.rs b/src/lib.rs index 882e126bc..22a4cf260 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,3 +27,21 @@ pub mod static_time { pub static ref TIME_AT_APP_START: SystemTime = SystemTime::now(); } } + +pub mod static_keys { + pub mod static_seeds { + use rand::rngs::ThreadRng; + use rand::Rng; + + pub type SeedSize = [u8; 32]; + + lazy_static! { + pub static ref INSTANCE_SEED: SeedSize = Rng::gen(&mut ThreadRng::default()); + } + + #[cfg(test)] + lazy_static! { + pub static ref ZEROED_TEST_SEED: SeedSize = [0u8; 32]; + } + } +} diff --git a/src/protocol/clock.rs b/src/protocol/clock.rs index 55f9a7c82..96c6fde09 100644 --- a/src/protocol/clock.rs +++ b/src/protocol/clock.rs @@ -53,8 +53,10 @@ pub trait StoppedTime: Time { fn local_reset(); } +#[derive(Debug)] pub struct Clock; +#[derive(Debug)] pub enum ClockType { WorkingClock, StoppedClock, @@ -153,6 +155,7 @@ impl StoppedTime for StoppedClock { #[cfg(test)] mod tests { + use std::any::TypeId; use std::thread; use super::*; @@ -160,11 +163,13 @@ mod tests { #[test] fn the_stopped_time_and_default_time_should_be_the_same_when_testing() { // We are testing, so we should default to the fixed time. + assert_eq!(TypeId::of::(), TypeId::of::()); assert_eq!(StoppedClock::now(), DefaultClock::now()) } #[test] fn the_stopped_time_and_working_time_should_be_different() { + assert_ne!(TypeId::of::(), TypeId::of::()); assert_ne!(StoppedClock::now(), WorkingClock::now()) } diff --git a/src/protocol/crypto.rs b/src/protocol/crypto.rs new file mode 100644 index 000000000..6367b7d29 --- /dev/null +++ b/src/protocol/crypto.rs @@ -0,0 +1,84 @@ +pub mod keys { + + pub mod seeds { + use crate::static_keys::static_seeds::{SeedSize, INSTANCE_SEED}; + + pub trait Seed { + type Seed: Sized + Default + AsMut<[u8]>; + fn seed() -> &'static Self::Seed; + } + + pub struct InstanceSeed; + pub struct DefaultSeed; + + impl Seed for InstanceSeed { + type Seed = SeedSize; + fn seed() -> &'static Self::Seed { + &*INSTANCE_SEED + } + } + + impl Seed for DefaultSeed { + type Seed = SeedSize; + fn seed() -> &'static Self::Seed { + &*self::detail::DEFAULT_SEED + } + } + + pub fn initialize_default_seed() { + lazy_static::initialize(&self::detail::DEFAULT_SEED); + } + + mod detail { + + #[cfg(not(test))] + pub use INSTANCE_SEED as DEFAULT_SEED; + + pub use crate::static_keys::static_seeds::INSTANCE_SEED; + #[cfg(test)] + pub use crate::static_keys::static_seeds::ZEROED_TEST_SEED as DEFAULT_SEED; + + #[cfg(test)] + mod tests { + use std::convert::TryInto; + + use crate::static_keys::static_seeds::{INSTANCE_SEED, ZEROED_TEST_SEED}; + + #[test] + fn it_should_have_a_zero_test_seed() { + assert_eq!(*ZEROED_TEST_SEED, [0u8; 32]) + } + + #[test] + fn it_should_have_a_large_random_seed() { + assert!(u128::from_ne_bytes((*INSTANCE_SEED)[..16].try_into().unwrap()) > u64::MAX as u128); + assert!(u128::from_ne_bytes((*INSTANCE_SEED)[16..].try_into().unwrap()) > u64::MAX as u128); + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + pub struct ZeroedTestSeed; + + impl Seed for ZeroedTestSeed { + type Seed = SeedSize; + fn seed() -> &'static Self::Seed { + &*crate::static_keys::static_seeds::ZEROED_TEST_SEED + } + } + + #[test] + fn the_default_seed_and_the_zeroed_seed_should_be_the_same_when_testing() { + assert_eq!(DefaultSeed::seed(), ZeroedTestSeed::seed()) + } + + #[test] + fn the_default_seed_and_the_instance_seed_should_be_different_when_testing() { + assert_ne!(DefaultSeed::seed(), InstanceSeed::seed()) + } + } + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index fcb28b3b2..85e4f90ad 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,3 +1,4 @@ pub mod clock; pub mod common; +pub mod crypto; pub mod utils; diff --git a/src/protocol/utils.rs b/src/protocol/utils.rs index cd015dc5e..f532bbe15 100644 --- a/src/protocol/utils.rs +++ b/src/protocol/utils.rs @@ -1,48 +1,4 @@ -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use std::net::SocketAddr; -use std::ops::Mul; -use std::time::Duration; - -use aquatic_udp_protocol::ConnectionId; - -use super::clock::{DefaultClock, SinceUnixEpoch, Time}; -use crate::udp::ServerError; - -pub fn make_connection_cookie(lifetime: &Duration, remote_address: &SocketAddr) -> ConnectionId { - let period = DefaultClock::now_add_periods(&lifetime, &lifetime); - - let mut hasher = DefaultHasher::new(); - - remote_address.hash(&mut hasher); - period.hash(&mut hasher); - - let connection_id_cookie = i64::from_le_bytes(hasher.finish().to_le_bytes()); - - ConnectionId(connection_id_cookie) -} - -pub fn check_connection_cookie( - lifetime: &Duration, - remote_address: &SocketAddr, - connection_id: &ConnectionId, -) -> Result<(), ServerError> { - for n in 0..=1 { - let period = DefaultClock::now_add_periods(&lifetime.mul(n), &lifetime); - - let mut hasher = DefaultHasher::new(); - - remote_address.hash(&mut hasher); - period.hash(&mut hasher); - - let connection_id_cookie = i64::from_le_bytes(hasher.finish().to_le_bytes()); - - if (*connection_id).0 == connection_id_cookie { - return Ok(()); - } - } - Err(ServerError::InvalidConnectionId) -} +use super::clock::SinceUnixEpoch; pub fn ser_unix_time_value(unix_time_value: &SinceUnixEpoch, ser: S) -> Result { ser.serialize_u64(unix_time_value.as_millis() as u64) diff --git a/src/udp/connection_cookie.rs b/src/udp/connection_cookie.rs new file mode 100644 index 000000000..84d423e0f --- /dev/null +++ b/src/udp/connection_cookie.rs @@ -0,0 +1,214 @@ +use std::net::SocketAddr; + +use aquatic_udp_protocol::ConnectionId; + +use crate::protocol::clock::SinceUnixEpoch; +use crate::udp::ServerError; + +pub fn make_connection_cookie(remote_address: &SocketAddr) -> ConnectionId { + let period = detail::get_period_now(); + + let cookie = detail::make_cookie_internal(remote_address, &period); + + detail::make_connection_id(&cookie) +} + +pub fn check_connection_cookie(remote_address: &SocketAddr, connection_id: &ConnectionId) -> Result { + // we loop backwards testing each period until we find one that matches. + // (or the lifetime of periods is exhausted) + + for offset in 0..=detail::PERIODS_IN_LIFETIME { + let checking_period = detail::get_period_now().saturating_sub(offset as u128); + + let cookie = detail::make_cookie_internal(remote_address, &checking_period); + + if *connection_id == detail::make_connection_id(&cookie) { + return Ok(detail::get_time_at_period_start(&checking_period)); + } + } + Err(ServerError::InvalidConnectionId) +} + +#[cfg(test)] +mod tests { + + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::time::Duration; + + use self::super::*; + use crate::protocol::clock::{StoppedClock, StoppedTime}; + + type PeriodLength = Duration; + + fn get_period_length() -> PeriodLength { + Duration::from_secs(detail::PERIOD_LENGTH) + } + + fn get_last_valid_period_now() -> detail::Period { + detail::get_period_now() + detail::PERIODS_IN_LIFETIME as u128 + } + + fn get_test_socket_addr() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) + } + + fn make_test_cookie(remote_address: Option<&SocketAddr>) -> ConnectionId { + make_connection_cookie(remote_address.unwrap_or(&get_test_socket_addr())) + } + + #[test] + fn it_should_make_a_connection_cookie() { + // remote_address: 127.0.0.1:8080, period: 0, + // seed: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + const ID_STABLE: i64 = 8313098377115836164; + const ID_NIGHTLY: i64 = -4342702349221980886; + + let test_cookie = make_test_cookie(None); + assert!((test_cookie == ConnectionId(ID_STABLE) || (test_cookie == ConnectionId(ID_NIGHTLY)))) + } + + #[test] + fn it_should_make_different_connection_cookie_with_different_remote_addresses() { + let test_remote_address_1 = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 1); + let test_remote_address_2 = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 2); + let test_remote_address_3 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 1); + + assert_ne!( + make_test_cookie(Some(&test_remote_address_1)), + make_test_cookie(Some(&test_remote_address_2)) + ); + + assert_ne!( + make_test_cookie(Some(&test_remote_address_1)), + make_test_cookie(Some(&test_remote_address_3)) + ); + + assert_ne!( + make_test_cookie(Some(&test_remote_address_2)), + make_test_cookie(Some(&test_remote_address_3)) + ) + } + + #[test] + fn it_should_make_different_cookies_for_the_next_period() { + let cookie_now = make_test_cookie(None); + + StoppedClock::local_add(&get_period_length()).unwrap(); + + let cookie_next = make_test_cookie(None); + + assert_ne!(cookie_now, cookie_next) + } + + #[test] + fn it_cookies_should_be_valid_for_this_and_next_periods() { + let cookie_now = make_test_cookie(None); + + check_connection_cookie(&get_test_socket_addr(), &cookie_now).unwrap(); + + StoppedClock::local_add(&get_period_length()).unwrap(); + + check_connection_cookie(&get_test_socket_addr(), &cookie_now).unwrap(); + } + + #[test] + fn it_cookies_should_be_valid_for_the_last_period() { + let cookie_now = make_test_cookie(None); + + let last_period = get_last_valid_period_now(); + + StoppedClock::local_set(&detail::get_time_at_period_start(&(last_period))); + + check_connection_cookie(&get_test_socket_addr(), &cookie_now).unwrap(); + } + + #[test] + #[should_panic] + fn it_cookies_should_be_not_valid_after_their_periods_has_expired() { + let cookie_now = make_test_cookie(None); + + let last_period = get_last_valid_period_now(); + + StoppedClock::local_set(&detail::get_time_at_period_end(&(last_period))); + + check_connection_cookie(&get_test_socket_addr(), &cookie_now).unwrap(); + } +} + +mod detail { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + use std::time::Duration; + + use super::*; + use crate::protocol::clock::{DefaultClock, Time}; + use crate::protocol::crypto::keys::seeds::{DefaultSeed, Seed}; + + pub(crate) type Period = u128; + pub(crate) type Periods = u64; + + pub(crate) type Cookie = u64; + pub(crate) type Seconds = u64; + + pub(crate) const PERIODS_IN_LIFETIME: Periods = 60; + + pub(crate) const PERIOD_LENGTH: Seconds = 2; + + pub(crate) fn get_period_now() -> Period { + DefaultClock::now_periods(&Duration::from_secs(PERIOD_LENGTH)).unwrap() + } + + pub(crate) fn get_time_at_period_start(period: &Period) -> SinceUnixEpoch { + let periods_sec = (*period).saturating_mul(PERIOD_LENGTH as u128); + SinceUnixEpoch::from_secs(u64::try_from(periods_sec).unwrap()) + } + + #[allow(dead_code)] + pub(crate) fn get_time_at_period_end(period: &Period) -> SinceUnixEpoch { + get_time_at_period_start(&(period + 1)) + } + + pub(crate) fn make_connection_id(cookie: &Cookie) -> ConnectionId { + ConnectionId(i64::from_le_bytes(cookie.to_le_bytes())) + } + + pub(crate) fn make_cookie_internal(remote_address: &SocketAddr, period: &Period) -> Cookie { + let seed = DefaultSeed::seed(); + + let mut hasher = DefaultHasher::new(); + + remote_address.hash(&mut hasher); + period.hash(&mut hasher); + seed.hash(&mut hasher); + + //println!("remote_address: {remote_address:?}, period: {period:?}, seed: {seed:?}"); + + hasher.finish() + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::protocol::clock::StoppedTime; + + #[test] + fn it_should_get_the_periods_now() { + assert_eq!(get_period_now(), 0); + + DefaultClock::local_set(&SinceUnixEpoch::from_secs(12387687123)); + + assert_eq!(get_period_now(), 6193843561); + } + + #[test] + fn it_should_get_the_time_at_a_period() { + let time = SinceUnixEpoch::ZERO; + assert_eq!(get_time_at_period_start(&0), time); + + let time = SinceUnixEpoch::from_secs(12387687123); + assert!(get_time_at_period_start(&6193843561).as_nanos() <= time.as_nanos()); + assert!(get_time_at_period_end(&6193843561).as_nanos() >= time.as_nanos()); + } + } +} diff --git a/src/udp/handlers.rs b/src/udp/handlers.rs index 9b578d8b6..acdd017ec 100644 --- a/src/udp/handlers.rs +++ b/src/udp/handlers.rs @@ -1,14 +1,13 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; -use std::time::Duration; use aquatic_udp_protocol::{ AnnounceInterval, AnnounceRequest, AnnounceResponse, ConnectRequest, ConnectResponse, ErrorResponse, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, }; +use super::connection_cookie::{check_connection_cookie, make_connection_cookie}; use crate::peer::TorrentPeer; -use crate::protocol::utils::{check_connection_cookie, make_connection_cookie}; use crate::tracker::statistics::TrackerStatisticsEvent; use crate::tracker::torrent::TorrentError; use crate::tracker::tracker::TorrentTracker; @@ -70,7 +69,7 @@ pub async fn handle_connect( request: &ConnectRequest, tracker: Arc, ) -> Result { - let connection_id = make_connection_cookie(&Duration::from_secs(120), &remote_addr); + let connection_id = make_connection_cookie(&remote_addr); let response = Response::from(ConnectResponse { transaction_id: request.transaction_id, @@ -95,7 +94,7 @@ pub async fn handle_announce( announce_request: &AnnounceRequest, tracker: Arc, ) -> Result { - match check_connection_cookie(&Duration::from_secs(120), &remote_addr, &announce_request.connection_id) { + match check_connection_cookie(&remote_addr, &announce_request.connection_id) { Ok(_) => {} Err(e) => { return Err(e); diff --git a/src/udp/mod.rs b/src/udp/mod.rs index ae87973f1..4c98875c5 100644 --- a/src/udp/mod.rs +++ b/src/udp/mod.rs @@ -3,6 +3,7 @@ pub use self::handlers::*; pub use self::request::*; pub use self::server::*; +pub mod connection_cookie; pub mod errors; pub mod handlers; pub mod request; diff --git a/src/udp/server.rs b/src/udp/server.rs index 11cb61d99..de0636602 100644 --- a/src/udp/server.rs +++ b/src/udp/server.rs @@ -25,6 +25,9 @@ impl UdpServer { } pub async fn start(&self) { + // generate the crypto seed before we do anything + crate::protocol::crypto::keys::seeds::initialize_default_seed(); + loop { let mut data = [0; MAX_PACKET_SIZE]; let socket = self.socket.clone();