From cfdc4a5b2bba1f25320807ac59ab40f3053e2fbd Mon Sep 17 00:00:00 2001 From: Monica Moniot Date: Wed, 20 Sep 2023 11:40:38 -0400 Subject: [PATCH] saving experimental changes --- performance/src/frag_cache.rs | 69 ++++++++++++++++++++---------- performance/src/handshake_cache.rs | 35 +++++++++------ performance/src/zeta.rs | 61 ++++++++++++++++---------- performance/src/zssp.rs | 50 ++++++++++++++++------ 4 files changed, 142 insertions(+), 73 deletions(-) diff --git a/performance/src/frag_cache.rs b/performance/src/frag_cache.rs index 5fdd804..2ec326c 100644 --- a/performance/src/frag_cache.rs +++ b/performance/src/frag_cache.rs @@ -2,6 +2,7 @@ use std::collections::hash_map::RandomState; use std::hash::{BuildHasher, Hash, Hasher}; use std::mem::MaybeUninit; +use crate::application::CryptoLayer; use crate::crypto::AES_GCM_NONCE_SIZE; use crate::fragged::Assembled; use crate::proto::{MAX_FRAGMENTS, MAX_UNASSOCIATED_FRAGMENTS, MAX_UNASSOCIATED_PACKETS, MAX_UNASSOCIATED_PACKET_SIZE}; @@ -15,18 +16,18 @@ struct PacketMetadata { creation_time: i64, } -pub(crate) struct UnassociatedFragCache { +pub(crate) struct UnassociatedFragCache { dos_salt: RandomState, frags_first_unused: usize, frags_unused_size: usize, map: [PacketMetadata; MAX_UNASSOCIATED_PACKETS], - frags: [MaybeUninit; MAX_UNASSOCIATED_FRAGMENTS], + frags: [MaybeUninit; MAX_UNASSOCIATED_FRAGMENTS], map_idx: [u32; MAX_UNASSOCIATED_FRAGMENTS], } /// A combination of a hash table cache and a ring buffer for unassociated fragments. /// Designed specifically to be extremely DDOS resistant. /// This datastructure takes raw unauthenticated fragments straight from the network. -impl UnassociatedFragCache { +impl UnassociatedFragCache { pub(crate) fn new() -> Self { Self { dos_salt: RandomState::new(), @@ -46,24 +47,24 @@ impl UnassociatedFragCache { } /// Add a fragment and return an assembled packet container if all fragments have been received. /// Will check that aad is the same for all fragments. + /// Returns true if the fragment is a part of a new fragment. pub(crate) fn assemble( &mut self, nonce: &[u8; AES_GCM_NONCE_SIZE], remote_address: impl Hash, fragment_size: usize, - fragment: Fragment, + fragment: Crypto::IncomingPacketBuffer, fragment_no: usize, fragment_count: usize, - timeout_interval: i64, current_time: i64, - ret_assembled: &mut Assembled, - ) { + ret_assembled: &mut Assembled, + ) -> Option { debug_assert!(MAX_FRAGMENTS < MAX_UNASSOCIATED_FRAGMENTS); if fragment_no >= fragment_count || fragment_count > MAX_FRAGMENTS || fragment_size > MAX_UNASSOCIATED_PACKET_SIZE { - return; + return None; } let mut hasher = self.dos_salt.build_hasher(); @@ -94,7 +95,7 @@ impl UnassociatedFragCache { } else if self.map[idx0].key == 0 || self.map[idx1].key == 0 { if (fragment_count as usize) > self.frags_unused_size { // There are not enough free fragment slots so attempt to expire a bunch of entries. - self.check_for_expiry(timeout_interval, current_time); + let _ = self.check_for_expiry_inner(Crypto::SETTINGS.resend_time as i64, current_time); } if self.map[idx0].key == 0 { idx0 @@ -103,20 +104,21 @@ impl UnassociatedFragCache { } } else { // No room for a new entry so attempt to expire a bunch of entries. - self.check_for_expiry(timeout_interval, current_time); + let _ = self.check_for_expiry_inner(Crypto::SETTINGS.resend_time as i64, current_time); if self.map[idx0].key == 0 { idx0 } else if self.map[idx1].key == 0 { idx1 } else { // Give up and drop the fragment. - return; + return None; } }; - + let mut new_expiry = None; if self.map[idx].key == 0 { // This is a new entry so initialize it. if (fragment_count as usize) <= self.frags_unused_size { + new_expiry = Some(current_time + Crypto::SETTINGS.fragment_assembly_timeout as i64); let entry = &mut self.map[idx]; entry.key = key; entry.frags_idx = self.frags_first_unused as u32; @@ -132,7 +134,7 @@ impl UnassociatedFragCache { } } else { // If there are not enough free fragment slots by this point we just drop the fragment. - return; + return None; } } let entry = &mut self.map[idx]; @@ -160,8 +162,12 @@ impl UnassociatedFragCache { self.invalidate::(idx); } } + new_expiry } - pub(crate) fn check_for_expiry(&mut self, timeout: i64, current_time: i64) { + pub(crate) fn check_for_expiry(&mut self, current_time: i64) -> i64 { + self.check_for_expiry_inner(Crypto::SETTINGS.fragment_assembly_timeout as i64, current_time) + } + fn check_for_expiry_inner(&mut self, timeout: i64, current_time: i64) -> i64 { while self.frags_unused_size < self.frags.len() { // Check if we can drop the entry at the start of the ring buffer. let frag_idx = (self.frags_first_unused + self.frags_unused_size) % self.frags.len(); @@ -169,12 +175,14 @@ impl UnassociatedFragCache { debug_assert!(map_idx < self.map.len()); let entry = &mut self.map[map_idx]; - if entry.creation_time + timeout < current_time { + let expiry = entry.creation_time + timeout; + if expiry <= current_time { self.invalidate::(map_idx); } else { - break; + return expiry; } } + return i64::MAX; } fn invalidate(&mut self, idx: usize) { @@ -210,7 +218,7 @@ impl UnassociatedFragCache { } } } -impl Drop for UnassociatedFragCache { +impl Drop for UnassociatedFragCache { fn drop(&mut self) { for i in 0..self.map.len() { if self.map[i].key != 0 { @@ -233,11 +241,28 @@ fn test_cache() { drop(x); r.wrapping_mul(0x2545F4914F6CDD1Du64) } + use crate::crypto_impl::*; + struct Crypto {} + impl CryptoLayer for Crypto { + type Rng = rand_core::OsRng; + type PrpEnc = OpenSSLAes256Enc; + type PrpDec = OpenSSLAes256Dec; + type Aead = OpenSSLAesGcm; + type AeadPool = OpenSSLAesGcmPool; + type Hash = CrateSha512; + type Hmac = CrateHmacSha512; + type PublicKey = CrateP384PublicKey; + type KeyPair = CrateP384KeyPair; + type Kem = CrateKyber1024PrivateKey; - let mut cache = UnassociatedFragCache::new(); + type SessionData = (); + type IncomingPacketBuffer = Vec; + } + + let mut cache = UnassociatedFragCache::::new(); let mut assembled = Assembled::new(); - let mut time = 1; + let mut time = 0; let mut in_progress = Vec::new(); let mut in_progress_fragments = 0; // A basic fuzzer for testing the cache. @@ -267,11 +292,10 @@ fn test_cache() { fragment, j, fragment_count, - 1, time, &mut assembled, ); - time += 1; + time += 200; } } if drop >= fragment_count { @@ -310,11 +334,10 @@ fn test_cache() { fragment, no as usize, fragment_count as usize, - 1000, time, &mut assembled, ); - time += 1; + time += 200; in_progress_fragments -= 1; if packet.len() > 0 { diff --git a/performance/src/handshake_cache.rs b/performance/src/handshake_cache.rs index 1425a9b..b4b47aa 100644 --- a/performance/src/handshake_cache.rs +++ b/performance/src/handshake_cache.rs @@ -12,7 +12,7 @@ pub(crate) struct UnassociatedHandshakeCache { /// SoA format struct CacheInner { local_ids: [Option; MAX_UNASSOCIATED_HANDSHAKE_STATES], - timeouts: [i64; MAX_UNASSOCIATED_HANDSHAKE_STATES], + expiries: [i64; MAX_UNASSOCIATED_HANDSHAKE_STATES], handshakes: [Option>>; MAX_UNASSOCIATED_HANDSHAKE_STATES], } @@ -25,7 +25,7 @@ impl UnassociatedHandshakeCache { has_pending: AtomicBool::new(false), cache: RwLock::new(CacheInner { local_ids: std::array::from_fn(|_| None), - timeouts: std::array::from_fn(|_| 0), + expiries: std::array::from_fn(|_| 0), handshakes: std::array::from_fn(|_| None), }), } @@ -39,55 +39,64 @@ impl UnassociatedHandshakeCache { } None } - pub(crate) fn insert(&self, local_id: NonZeroU32, state: Arc>, current_time: i64) { + pub(crate) fn insert( + &self, + local_id: NonZeroU32, + state: Arc>, + current_time: i64, + ) -> Option { let mut cache = self.cache.write().unwrap(); let mut idx = 0; for i in 0..cache.local_ids.len() { - if cache.local_ids[i].is_none() || cache.timeouts[i] < current_time { + if cache.local_ids[i].is_none() || cache.expiries[i] <= current_time { idx = i; break; } else if cache.local_ids[i] == Some(local_id) { - return; + return None; } } + let expiry = current_time + Application::SETTINGS.fragment_assembly_timeout as i64; cache.local_ids[idx] = Some(local_id); - cache.timeouts[idx] = current_time + Application::SETTINGS.fragment_assembly_timeout as i64; + cache.expiries[idx] = expiry; cache.handshakes[idx] = Some(state); self.has_pending.store(true, Ordering::Release); + return Some(expiry); } pub(crate) fn remove(&self, local_id: NonZeroU32) -> bool { let mut cache = self.cache.write().unwrap(); for (i, id) in cache.local_ids.iter().enumerate() { if *id == Some(local_id) { cache.local_ids[i] = None; - cache.timeouts[i] = 0; + cache.expiries[i] = 0; cache.handshakes[i] = None; return true; } } false } - pub(crate) fn service(&self, current_time: i64) { + pub(crate) fn service(&self, current_time: i64) -> i64 { // Only check for expiration if we have a pending packet. // This check is allowed to have false positives for simplicity's sake. + let mut next_service_time = i64::MAX; if self.has_pending.swap(false, Ordering::Acquire) { // Check for packet expiration let mut cache = self.cache.write().unwrap(); - let mut has_pending = false; for i in 0..cache.local_ids.len() { if cache.local_ids[i].is_some() { - if cache.timeouts[i] < current_time { + let expiry = cache.expiries[i]; + if expiry <= current_time { cache.local_ids[i] = None; - cache.timeouts[i] = 0; + cache.expiries[i] = 0; cache.handshakes[i] = None; } else { - has_pending = true; + next_service_time = next_service_time.min(expiry); } } } - if has_pending { + if next_service_time < i64::MAX { self.has_pending.store(true, Ordering::Release); } } + next_service_time } } diff --git a/performance/src/zeta.rs b/performance/src/zeta.rs index cf038ce..c5cc66f 100644 --- a/performance/src/zeta.rs +++ b/performance/src/zeta.rs @@ -420,6 +420,9 @@ pub(crate) fn trans_to_a1), -) -> Result<(), ReceiveError> { +) -> Result { use FaultType::*; // <- s // ... @@ -566,7 +569,7 @@ pub(crate) fn received_x1_trans>( @@ -596,7 +603,7 @@ pub(crate) fn received_x2_trans), -) -> Result { +) -> Result<(bool, bool), ReceiveError> { use FaultType::*; // <- e, ee, ekem1, psk // -> s, se @@ -770,8 +777,9 @@ pub(crate) fn received_x2_trans send(packet, Some(&session.state.read().unwrap().hk_send)), + Ok((ref mut packet, _)) => send(packet, Some(&session.state.read().unwrap().hk_send)), _ => {} } - result.map(|_| should_warn_missing_ratchet) + result.map(|(_, reduced_service_time)| (should_warn_missing_ratchet, reduced_service_time)) } fn send_control( session: &Arc>, @@ -816,7 +824,7 @@ pub(crate) fn received_x3_trans), -) -> Result<(Arc>, bool), ReceiveError> { +) -> Result<(Arc>, bool, bool), ReceiveError> { use FaultType::*; // -> s, se if x3.len() < HANDSHAKE_COMPLETION_MIN_SIZE { @@ -916,7 +924,7 @@ pub(crate) fn received_x3_trans::new(); @@ -974,7 +982,7 @@ pub(crate) fn received_x3_trans Err(ReceiveError::RatchetStorageError), } @@ -994,7 +1002,7 @@ pub(crate) fn received_c1_trans), -) -> Result { +) -> Result<(bool, bool), ReceiveError> { use FaultType::*; if c1.len() != KEY_CONFIRMATION_SIZE { @@ -1024,6 +1032,7 @@ pub(crate) fn received_c1_trans Result<(), ReceiveError> { +) -> Result { use FaultType::*; if c2.len() != ACKNOWLEDGEMENT_SIZE { @@ -1120,7 +1130,8 @@ pub(crate) fn received_c2_trans), -) -> Result<(), ReceiveError> { +) -> Result { use FaultType::*; // -> s // <- s @@ -1478,13 +1489,14 @@ pub(crate) fn received_k1_trans), -) -> Result<(), ReceiveError> { +) -> Result { use FaultType::*; // <- e, ee, se if k2.len() != REKEY_SIZE { @@ -1601,6 +1613,7 @@ pub(crate) fn received_k2_trans::new(); @@ -1609,7 +1622,7 @@ pub(crate) fn received_k2_trans( payload: &[u8], mut send: impl FnMut(&mut [u8]) -> bool, mtu_sized_buffer: &mut [u8], -) -> Result<(), SendError> { +) -> Result { use SendError::*; let mtu = mtu_sized_buffer.len(); if mtu < MIN_TRANSPORT_MTU { @@ -1673,7 +1686,7 @@ pub(crate) fn send_payload( state.hk_send.encrypt_in_place(header_auth.try_into().unwrap()); if !send(&mut mtu_sized_buffer[..HEADER_SIZE + fragment_len]) { - return Ok(()); + return Ok(false); } i = j; } @@ -1692,7 +1705,7 @@ pub(crate) fn send_payload( state.hk_send.encrypt_in_place(header_auth.try_into().unwrap()); if !send(&mut mtu_sized_buffer[..HEADER_SIZE + fragment_len]) { - return Ok(()); + return Ok(false); } drop(state); @@ -1705,8 +1718,10 @@ pub(crate) fn send_payload( .lock() .unwrap() .change_priority(session.queue_idx, Reverse(i64::MIN)); + Ok(ctx.reduce_next_service_time(i64::MIN)) + } else { + Ok(false) } - Ok(()) } /// Corresponds to Algorithm 10 found in Section 4.3. pub(crate) fn receive_payload_in_place( diff --git a/performance/src/zssp.rs b/performance/src/zssp.rs index ad55015..236847f 100644 --- a/performance/src/zssp.rs +++ b/performance/src/zssp.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::hash::Hash; use std::io::Write; use std::num::NonZeroU32; +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Mutex, RwLock, Weak}; use arrayvec::ArrayVec; @@ -44,19 +45,27 @@ impl Clone for Context { } pub(crate) type SessionMap = RwLock>>>; + pub(crate) type SessionQueue = IndexedBinaryHeap>, Reverse>; + pub struct ContextInner { pub rng: Mutex, + pub next_service_time: AtomicI64, pub(crate) s_secret: Crypto::KeyPair, /// `session_queue -> state_machine_lock -> state -> session_map` pub(crate) session_queue: Mutex>, /// `session_queue -> state_machine_lock -> state -> session_map` pub(crate) session_map: SessionMap, - pub(crate) unassociated_defrag_cache: Mutex>, + pub(crate) unassociated_defrag_cache: Mutex>, pub(crate) unassociated_handshake_states: UnassociatedHandshakeCache, pub(crate) challenge: ChallengeContext, } +impl ContextInner { + pub(crate) fn reduce_next_service_time(&self, time: i64) -> bool { + self.next_service_time.fetch_min(time, Ordering::Relaxed) > time + } +} fn parse_fragment_header(incoming_fragment: &[u8]) -> Result<(usize, usize, [u8; AES_GCM_NONCE_SIZE]), ReceiveError> { let fragment_no = incoming_fragment[FRAGMENT_NO_IDX] as usize; @@ -114,6 +123,7 @@ impl Context { Self(Arc::new(ContextInner { rng: Mutex::new(rng), s_secret: static_secret_key, + next_service_time: AtomicI64::new(i64::MAX), session_map: RwLock::new(HashMap::new()), challenge, session_queue: Mutex::new(IndexedBinaryHeap::new()), @@ -144,7 +154,7 @@ impl Context { static_remote_key: Crypto::PublicKey, session_data: Crypto::SessionData, identity: &[u8], - ) -> Result>, OpenError> { + ) -> Result<(Arc>, Option), OpenError> { mtu = mtu.max(MIN_TRANSPORT_MTU); if identity.len() > IDENTITY_MAX_SIZE { return Err(OpenError::IdentityTooLarge); @@ -189,7 +199,7 @@ impl Context { remote_address: &impl Hash, mut incoming_fragment_buf: Crypto::IncomingPacketBuffer, output_buffer: impl Write, - ) -> Result, ReceiveError> { + ) -> Result<(ReceiveOk, Option), ReceiveError> { use crate::result::FaultType::*; let ctx = &self.0; send_unassociated_mtu = send_unassociated_mtu.max(MIN_TRANSPORT_MTU); @@ -315,7 +325,7 @@ impl Context { match packet_type { PACKET_TYPE_HANDSHAKE_RESPONSE => { log!(app, ReceivedRawX2); - let should_warn_missing_ratchet = received_x2_trans( + let (should_warn_missing_ratchet, reduced) = received_x2_trans( &mut app, ctx, &session, @@ -480,17 +490,19 @@ impl Context { let mut buffer = ArrayVec::::new(); let assembled_packet = if fragment_count > 1 { - self.0.unassociated_defrag_cache.lock().unwrap().assemble( + let next_service_time = self.0.unassociated_defrag_cache.lock().unwrap().assemble( &nonce, remote_address, incoming_fragment.len() - HEADER_SIZE, incoming_fragment_buf, fragment_no, fragment_count, - Crypto::SETTINGS.resend_time as i64, app.time(), &mut fragment_buffer, ); + if let Some(next_service_time) = next_service_time { + ctx.reduce_next_service_time(next_service_time); + } if fragment_buffer.is_empty() { return Ok(ReceiveOk::Unassociated); } else { @@ -616,13 +628,13 @@ impl Context { let ctx = &self.0; let mut session_queue = ctx.session_queue.lock().unwrap(); let current_time = app.time(); - let mut next_service_time = current_time + Crypto::SETTINGS.fragment_assembly_timeout as i64; + let mut queue_service_time = i64::MAX; // This update system takes heavy advantage of the fact that sessions only need to be updated // either roughly every second or roughly every hour. That big gap allows for minor optimizations. // If the gap changes (unlikely) this code may need to be rewritten. while let Some((session, Reverse(timer), queue_idx)) = session_queue.peek() { - if *timer >= current_time { - next_service_time = next_service_time.min(*timer); + if *timer > current_time { + queue_service_time = queue_service_time.min(*timer); break; } let session = match session.upgrade() { @@ -639,21 +651,31 @@ impl Context { } }); if let Some(next_timer) = result { - next_service_time = next_service_time.min(next_timer); + queue_service_time = queue_service_time.min(next_timer); session_queue.change_priority(queue_idx, Reverse(next_timer)); } else { session.expire_inner(Some(ctx), Some(&mut session_queue)); } } + // This is the only place where `ctx.next_service_time` can be increased. This only works + // correctly because we are holding the `session_queue` lock and we are guaranteed to run + // the service code for the other two systems which are not currently locked. + ctx.next_service_time.store(queue_service_time, Ordering::Relaxed); drop(session_queue); - self.0 + let defrag_service_time = self + .0 .unassociated_defrag_cache .lock() .unwrap() - .check_for_expiry(Crypto::SETTINGS.fragment_assembly_timeout as i64, current_time); - self.0.unassociated_handshake_states.service(current_time); + .check_for_expiry(current_time); + let handshake_service_time = self.0.unassociated_handshake_states.service(current_time); - next_service_time - current_time + ctx.next_service_time + .fetch_min(defrag_service_time.min(handshake_service_time), Ordering::Relaxed); + + queue_service_time = queue_service_time.min(current_time + Crypto::SETTINGS.fragment_assembly_timeout as i64); + + queue_service_time - current_time } }