From d0446a965e9edaee48a47d6f86b05ac4888a7220 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Tue, 14 Mar 2023 15:29:20 -0400 Subject: [PATCH 01/25] Fixes to crypto on macOS, a lot of cleanup, and work on full state replication for V2 VL2 (#1911) * Move some stuff around in prep for a VL2 rework and identity rework. * Mix ephemeral keys into "h" * More topology stuff for VL2. * Simplify key queue, fix macOS issues with bindings, and no need to cache PSK forever. * Some more merge fixes. * A bunch of ZSSP cleanup and optimization. Runs a bit faster now. * Remove some unused util code. * scatter gather stuff * The scatter/gather algorithm works. * Make OpenSSL init get called automatically at process launch, and some more scatter gather work. * added support for cloning on EcKey * Scatter/gather, move SG into VL2 since that is where it will be used, add an array chunker to utils::memory * Simplify some Rust generic madness. * docs * Some cleanup and reorg. * Bring back AES-GMAC-SIV tests. * Turns out a Mutex is not really any slower... --------- Co-authored-by: mamoniot --- {utils/src => attic}/thing.rs | 0 controller/src/controller.rs | 148 ++--- crypto/Cargo.toml | 1 + crypto/src/aes_fruity.rs | 47 +- crypto/src/aes_tests.rs | 559 +++++++++++++++++- crypto/src/bn.rs | 11 +- crypto/src/cipher_ctx.rs | 2 - crypto/src/ec.rs | 27 +- crypto/src/lib.rs | 13 +- crypto/src/p384.rs | 3 +- network-hypervisor/Cargo.toml | 2 + network-hypervisor/src/protocol.rs | 50 +- network-hypervisor/src/vl1/endpoint.rs | 12 +- network-hypervisor/src/vl1/identity.rs | 11 +- network-hypervisor/src/vl1/node.rs | 139 ++--- network-hypervisor/src/vl1/path.rs | 31 +- network-hypervisor/src/vl1/peer.rs | 126 ++-- network-hypervisor/src/vl2/mod.rs | 1 + .../src/vl2/multicastauthority.rs | 22 +- network-hypervisor/src/vl2/scattergather.rs | 234 ++++++++ network-hypervisor/src/vl2/switch.rs | 26 +- network-hypervisor/src/vl2/topology.rs | 5 + utils/src/base64.rs | 20 + utils/src/blob.rs | 3 +- utils/src/buffer.rs | 1 - utils/src/cast.rs | 36 ++ utils/src/gate.rs | 33 -- utils/src/gatherarray.rs | 102 ---- utils/src/lib.rs | 20 +- utils/src/memory.rs | 20 + utils/src/ringbuffermap.rs | 265 --------- vl1-service/src/vl1service.rs | 10 +- zssp/src/zssp.rs | 1 - 33 files changed, 1148 insertions(+), 833 deletions(-) rename {utils/src => attic}/thing.rs (100%) create mode 100644 network-hypervisor/src/vl2/scattergather.rs create mode 100644 utils/src/base64.rs create mode 100644 utils/src/cast.rs delete mode 100644 utils/src/gatherarray.rs delete mode 100644 utils/src/ringbuffermap.rs diff --git a/utils/src/thing.rs b/attic/thing.rs similarity index 100% rename from utils/src/thing.rs rename to attic/thing.rs diff --git a/controller/src/controller.rs b/controller/src/controller.rs index c6f032df5..ebe9fde5c 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -17,6 +17,7 @@ use zerotier_network_hypervisor::vl2::v1::Revocation; use zerotier_network_hypervisor::vl2::NetworkId; use zerotier_utils::blob::Blob; use zerotier_utils::buffer::OutOfBoundsError; +use zerotier_utils::cast::cast_ref; use zerotier_utils::error::InvalidParameterError; use zerotier_utils::reaper::Reaper; use zerotier_utils::tokio; @@ -139,62 +140,56 @@ impl Controller { } /// Compose and send network configuration packet (either V1 or V2) - fn send_network_config( + fn send_network_config( &self, - peer: &Peer, + app: &Application, + node: &Node, + peer: &Peer, config: &NetworkConfig, in_re_message_id: Option, // None for unsolicited push ) { - if let Some(host_system) = self.service.read().unwrap().upgrade() { - peer.send( - host_system.as_ref(), - host_system.node(), - None, - ms_monotonic(), - |packet| -> Result<(), OutOfBoundsError> { - let payload_start = packet.len(); + peer.send(app, node, None, ms_monotonic(), |packet| -> Result<(), OutOfBoundsError> { + let payload_start = packet.len(); - if let Some(in_re_message_id) = in_re_message_id { - let ok_header = packet.append_struct_get_mut::()?; - ok_header.verb = protocol::message_type::VL1_OK; - ok_header.in_re_verb = protocol::message_type::VL2_NETWORK_CONFIG_REQUEST; - ok_header.in_re_message_id = in_re_message_id.to_be_bytes(); - } else { - packet.append_u8(protocol::message_type::VL2_NETWORK_CONFIG)?; - } + if let Some(in_re_message_id) = in_re_message_id { + let ok_header = packet.append_struct_get_mut::()?; + ok_header.verb = protocol::message_type::VL1_OK; + ok_header.in_re_verb = protocol::message_type::VL2_NETWORK_CONFIG_REQUEST; + ok_header.in_re_message_id = in_re_message_id.to_be_bytes(); + } else { + packet.append_u8(protocol::message_type::VL2_NETWORK_CONFIG)?; + } - if peer.is_v2() { - todo!() - } else { - let config_data = if let Some(config_dict) = config.v1_proto_to_dictionary(&self.local_identity) { - config_dict.to_bytes() - } else { - eprintln!("WARNING: unexpected error serializing network config into V1 format dictionary"); - return Err(OutOfBoundsError); // abort - }; - if config_data.len() > (u16::MAX as usize) { - eprintln!("WARNING: network config is larger than 65536 bytes!"); - return Err(OutOfBoundsError); // abort - } + if peer.is_v2() { + todo!() + } else { + let config_data = if let Some(config_dict) = config.v1_proto_to_dictionary(&self.local_identity) { + config_dict.to_bytes() + } else { + eprintln!("WARNING: unexpected error serializing network config into V1 format dictionary"); + return Err(OutOfBoundsError); // abort + }; + if config_data.len() > (u16::MAX as usize) { + eprintln!("WARNING: network config is larger than 65536 bytes!"); + return Err(OutOfBoundsError); // abort + } - packet.append_u64(config.network_id.into())?; - packet.append_u16(config_data.len() as u16)?; - packet.append_bytes(config_data.as_slice())?; + packet.append_u64(config.network_id.into())?; + packet.append_u16(config_data.len() as u16)?; + packet.append_bytes(config_data.as_slice())?; - // TODO: for V1 we may need to introduce use of the chunking mechanism for large configs. - } + // TODO: for V1 we may need to introduce use of the chunking mechanism for large configs. + } - let new_payload_len = protocol::compress(&mut packet.as_bytes_mut()[payload_start..]); - packet.set_size(payload_start + new_payload_len); + let new_payload_len = protocol::compress(&mut packet.as_bytes_mut()[payload_start..]); + packet.set_size(payload_start + new_payload_len); - Ok(()) - }, - ); - } + Ok(()) + }); } /// Send one or more revocation object(s) to a peer. The provided vector is drained. - fn send_revocations(&self, peer: &Peer, revocations: &mut Vec) { + fn send_revocations(&self, peer: &Peer>, revocations: &mut Vec) { if let Some(host_system) = self.service.read().unwrap().upgrade() { let time_ticks = ms_monotonic(); while !revocations.is_empty() { @@ -496,28 +491,12 @@ impl Controller { } impl InnerProtocolLayer for Controller { - #[inline(always)] - fn should_respond_to(&self, _: &Valid) -> bool { - // Controllers always have to establish sessions to process requests. We don't really know if - // a member is relevant until we have looked up both the network and the member, since whether - // or not to "learn" unknown members is a network level option. - true - } - - fn has_trust_relationship(&self, id: &Valid) -> bool { - self.recently_authorized - .read() - .unwrap() - .get(&id.fingerprint) - .map_or(false, |by_network| by_network.values().any(|t| *t > ms_monotonic())) - } - - fn handle_packet( + fn handle_packet( &self, - host_system: &HostSystemImpl, - _: &Node, - source: &Arc, - source_path: &Arc, + app: &Application, + node: &Node, + source: &Arc>, + source_path: &Arc>, source_hops: u8, message_id: u64, verb: u8, @@ -526,10 +505,6 @@ impl InnerProtocolLayer for Controller { ) -> PacketHandlerResult { match verb { protocol::message_type::VL2_NETWORK_CONFIG_REQUEST => { - if !self.should_respond_to(&source.identity) { - return PacketHandlerResult::Ok; // handled and ignored - } - let network_id = payload.read_u64(&mut cursor); if network_id.is_err() { return PacketHandlerResult::Error; @@ -541,7 +516,7 @@ impl InnerProtocolLayer for Controller { let network_id = network_id.unwrap(); debug_event!( - host_system, + app, "[vl2] NETWORK_CONFIG_REQUEST from {}({}) for {:0>16x}", source.identity.address.to_string(), source_path.endpoint.to_string(), @@ -573,7 +548,8 @@ impl InnerProtocolLayer for Controller { let (result, config) = match self2.authorize(&source.identity, network_id, now).await { Result::Ok((result, Some(config))) => { //println!("{}", serde_yaml::to_string(&config).unwrap()); - self2.send_network_config(source.as_ref(), &config, Some(message_id)); + let app = self2.service.read().unwrap().upgrade().unwrap(); + self2.send_network_config(app.as_ref(), app.node(), cast_ref(source.as_ref()).unwrap(), &config, Some(message_id)); (result, Some(config)) } Result::Ok((result, None)) => (result, None), @@ -626,23 +602,21 @@ impl InnerProtocolLayer for Controller { } protocol::message_type::VL2_MULTICAST_GATHER => { - if let Some(service) = self.service.read().unwrap().upgrade() { - let auth = self.recently_authorized.read().unwrap(); - let time_ticks = ms_monotonic(); - self.multicast_authority.handle_vl2_multicast_gather( - |network_id, identity| { - auth.get(&identity.fingerprint) - .map_or(false, |t| t.get(&network_id).map_or(false, |t| *t > time_ticks)) - }, - time_ticks, - service.as_ref(), - service.node(), - source, - message_id, - payload, - cursor, - ); - } + let auth = self.recently_authorized.read().unwrap(); + let time_ticks = ms_monotonic(); + self.multicast_authority.handle_vl2_multicast_gather( + |network_id, identity| { + auth.get(&identity.fingerprint) + .map_or(false, |t| t.get(&network_id).map_or(false, |t| *t > time_ticks)) + }, + time_ticks, + app, + node, + source, + message_id, + payload, + cursor, + ); PacketHandlerResult::Ok } diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index dbe98e97b..9a7e18a40 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -19,6 +19,7 @@ foreign-types = "0.5.0" libc = "0.2" lazy_static = "^1" rand_core = "0.6.4" +ctor = "^0" #ed25519-dalek still uses rand_core 0.5.1, and that version is incompatible with 0.6.4, so we need to import and implement both. rand_core_051 = { package = "rand_core", version = "0.5.1" } diff --git a/crypto/src/aes_fruity.rs b/crypto/src/aes_fruity.rs index 5a0d5355c..b46c2d2c7 100644 --- a/crypto/src/aes_fruity.rs +++ b/crypto/src/aes_fruity.rs @@ -3,7 +3,7 @@ // MacOS implementation of AES primitives since CommonCrypto seems to be faster than OpenSSL, especially on ARM64. use std::os::raw::{c_int, c_void}; use std::ptr::{null, null_mut}; -use std::sync::atomic::AtomicPtr; +use std::sync::Mutex; use crate::secret::Secret; use crate::secure_eq; @@ -172,25 +172,18 @@ impl AesGcm { } } -pub struct Aes(AtomicPtr, AtomicPtr); +pub struct Aes(Mutex<(*mut c_void, *mut c_void)>); impl Drop for Aes { #[inline(always)] fn drop(&mut self) { unsafe { - loop { - let p = self.0.load(std::sync::atomic::Ordering::Acquire); - if !p.is_null() { - CCCryptorRelease(p); - break; - } + let p = self.0.lock().unwrap(); + if !p.0.is_null() { + CCCryptorRelease(p.0); } - loop { - let p = self.1.load(std::sync::atomic::Ordering::Acquire); - if !p.is_null() { - CCCryptorRelease(p); - break; - } + if !p.1.is_null() { + CCCryptorRelease(p.1); } } } @@ -238,7 +231,7 @@ impl Aes { ), 0 ); - Self(AtomicPtr::new(p0), AtomicPtr::new(p1)) + Self(Mutex::new((p0, p1))) } } @@ -247,16 +240,8 @@ impl Aes { assert_eq!(data.len(), 16); unsafe { let mut data_out_written = 0; - loop { - let p = self.0.load(std::sync::atomic::Ordering::Acquire); - if !p.is_null() { - CCCryptorUpdate(p, data.as_ptr().cast(), 16, data.as_mut_ptr().cast(), 16, &mut data_out_written); - self.0.store(p, std::sync::atomic::Ordering::Release); - break; - } else { - std::thread::yield_now(); - } - } + let p = self.0.lock().unwrap(); + CCCryptorUpdate(p.0, data.as_ptr().cast(), 16, data.as_mut_ptr().cast(), 16, &mut data_out_written); } } @@ -265,16 +250,8 @@ impl Aes { assert_eq!(data.len(), 16); unsafe { let mut data_out_written = 0; - loop { - let p = self.1.load(std::sync::atomic::Ordering::Acquire); - if !p.is_null() { - CCCryptorUpdate(p, data.as_ptr().cast(), 16, data.as_mut_ptr().cast(), 16, &mut data_out_written); - self.1.store(p, std::sync::atomic::Ordering::Release); - break; - } else { - std::thread::yield_now(); - } - } + let p = self.0.lock().unwrap(); + CCCryptorUpdate(p.1, data.as_ptr().cast(), 16, data.as_mut_ptr().cast(), 16, &mut data_out_written); } } } diff --git a/crypto/src/aes_tests.rs b/crypto/src/aes_tests.rs index c85b89420..883fff16b 100644 --- a/crypto/src/aes_tests.rs +++ b/crypto/src/aes_tests.rs @@ -1,14 +1,21 @@ #[cfg(test)] mod test { use crate::aes::AesGcm; - use crate::init; + use crate::aes_gmac_siv::AesGmacSiv; use crate::secret::Secret; use hex_literal::hex; use std::time::SystemTime; + fn to_hex(b: &[u8]) -> String { + let mut s = String::new(); + for c in b.iter() { + s = format!("{}{:0>2x}", s, *c); + } + s + } + #[test] fn aes_256_gcm() { - init(); let key = Secret::move_bytes([1u8; 32]); let mut enc = AesGcm::::new(&key); let mut dec = AesGcm::::new(&key); @@ -50,7 +57,7 @@ mod test { } #[test] - fn quick_benchmark() { + fn aes_256_gcm_quick_benchmark() { let mut buf = [0_u8; 12345]; for i in 1..12345 { buf[i] = i as u8; @@ -67,7 +74,7 @@ mod test { } let duration = SystemTime::now().duration_since(start).unwrap(); println!( - "AES-256-GCM encrypt benchmark: {} MiB/sec", + " AES-256-GCM encrypt benchmark: {} MiB/sec", (((benchmark_iterations * buf.len()) as f64) / 1048576.0) / duration.as_secs_f64() ); @@ -80,7 +87,7 @@ mod test { } let duration = SystemTime::now().duration_since(start).unwrap(); println!( - "AES-256-GCM decrypt benchmark: {} MiB/sec", + " AES-256-GCM decrypt benchmark: {} MiB/sec", (((benchmark_iterations * buf.len()) as f64) / 1048576.0) / duration.as_secs_f64() ); } @@ -114,6 +121,109 @@ mod test { } } + #[test] + fn aes_gmac_siv_test_vectors() { + let mut test_pt = [0_u8; 65536]; + let mut test_ct = [0_u8; 65536]; + let mut test_aad = [0_u8; 65536]; + for i in 0..65536 { + test_pt[i] = i as u8; + test_aad[i] = i as u8; + } + let mut c = AesGmacSiv::new(TV0_KEYS[0], TV0_KEYS[1]); + for (test_length, expected_ct_sha384, expected_tag) in TEST_VECTORS.iter() { + test_ct.fill(0); + c.reset(); + c.encrypt_init(&(*test_length as u64).to_le_bytes()); + c.encrypt_set_aad(&test_aad[0..*test_length]); + c.encrypt_first_pass(&test_pt[0..*test_length]); + c.encrypt_first_pass_finish(); + c.encrypt_second_pass(&test_pt[0..*test_length], &mut test_ct[0..*test_length]); + let tag = c.encrypt_second_pass_finish(); + let ct_hash = crate::hash::SHA384::hash(&test_ct[0..*test_length]).to_vec(); + //println!("{} {} {}", *test_length, to_hex(ct_hash.as_slice()), to_hex(tag)); + if !to_hex(ct_hash.as_slice()).eq(*expected_ct_sha384) { + panic!("test vector failed (ciphertest)"); + } + if !to_hex(tag).eq(*expected_tag) { + panic!("test vector failed (tag)"); + } + } + } + + #[test] + fn aes_gmac_siv_encrypt_decrypt() { + let aes_key_0: [u8; 32] = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]; + let aes_key_1: [u8; 32] = [ + 2, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ]; + let iv: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; + + let mut buf = [0_u8; 12345]; + for i in 1..12345 { + buf[i] = i as u8; + } + + let mut c = AesGmacSiv::new(&aes_key_0, &aes_key_1); + + for _ in 0..256 { + c.reset(); + c.encrypt_init(&iv); + c.encrypt_first_pass(&buf); + c.encrypt_first_pass_finish(); + c.encrypt_second_pass_in_place(&mut buf); + let tag = c.encrypt_second_pass_finish().clone(); + let sha = crate::hash::SHA384::hash(&buf).to_vec(); + let sha = to_hex(sha.as_slice()); + if sha != "4dc97c10abb6112a3907e5eb588ea5123719442b715da994d9756b003677719824326973960268823d924f66491a16e6" { + panic!("encrypt result hash check failed! {}", sha); + } + //println!("Encrypt OK, tag: {}, hash: {}", to_hex(&tag), sha); + + c.reset(); + c.decrypt_init(&tag); + c.decrypt_in_place(&mut buf); + let _ = c.decrypt_finish().expect("decrypt_finish() failed!"); + for i in 1..12345 { + if buf[i] != (i & 0xff) as u8 { + panic!("decrypt data check failed!"); + } + } + //println!("Decrypt OK"); + } + //println!("Encrypt/decrypt test OK"); + + let benchmark_iterations: usize = 80000; + let start = SystemTime::now(); + for _ in 0..benchmark_iterations { + c.reset(); + c.encrypt_init(&iv); + c.encrypt_first_pass(&buf); + c.encrypt_first_pass_finish(); + c.encrypt_second_pass_in_place(&mut buf); + let _ = c.encrypt_second_pass_finish(); + } + let duration = SystemTime::now().duration_since(start).unwrap(); + println!( + " AES-GMAC-SIV (legacy) encrypt benchmark: {} MiB/sec", + (((benchmark_iterations * buf.len()) as f64) / 1048576.0) / duration.as_secs_f64() + ); + let start = SystemTime::now(); + for _ in 0..benchmark_iterations { + c.reset(); + c.decrypt_init(&buf[0..16]); // we don't care if decryption is successful to benchmark, so anything will do + c.decrypt_in_place(&mut buf); + c.decrypt_finish(); + } + let duration = SystemTime::now().duration_since(start).unwrap(); + println!( + " AES-GMAC-SIV (legacy) decrypt benchmark: {} MiB/sec", + (((benchmark_iterations * buf.len()) as f64) / 1048576.0) / duration.as_secs_f64() + ); + } + struct GcmTV { pub key: &'static K, pub nonce: &'static [u8; 12], @@ -123,6 +233,445 @@ mod test { pub tag: &'static [u8; 16], } + /// AES-GMAC-SIV test keys. + const TV0_KEYS: [&'static [u8]; 2] = [ + "00000000000000000000000000000000".as_bytes(), + "11111111111111111111111111111111".as_bytes(), + ]; + + /// AES-GMAC-SIV test vectors. + /// Test vectors consist of a series of input sizes, a SHA384 hash of a resulting ciphertext, and an expected tag. + /// Input is a standard byte array consisting of bytes 0, 1, 2, 3, ..., 255 and then cycling back to 0 over and over + /// and is provided both as ciphertext and associated data (AAD). + #[allow(unused)] + const TEST_VECTORS: [(usize, &'static str, &'static str); 85] = [ + ( + 0, + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "43847e644239134deccf5538162c861e", + ), + ( + 777, + "aabf892f18a620b9c3bae91bb03a74c84193e4a7b64916c6bc88b885b9ebed4134495e5f22f12e3046fbb3f26fa111a7", + "b8c318b5dcc1d672114a6f7be54ef289", + ), + ( + 1554, + "648f551df29217f0e634b72ba6973c0eb95c7d4be8b135e550d8bcdf65b75980881bc0e03cf22589e04bedc7da1804cd", + "535b8ddd51ec82a1e850906fe321b21a", + ), + ( + 2331, + "bfbfdffea40062e23bbdf0835e1d38d1623bebca7407908bbc6d5b3f2bfd062a2d237f091affda7348094fafda0bd1a7", + "4f521876fbb2c563051196b33c20c822", + ), + ( + 3108, + "cc6035cab70f3a3298a5c4956ff07f179acf3771bb915c590a8a19fe5133d6d8a81c118148394dfb364af5c2fbdaadeb", + "d3adfa578c8bcd738c55ffc527358cef", + ), + ( + 3885, + "15ec2760a21c25f9870a84ee757f3da2c261a950c2f692d75ff9e99b2d50c826c21e27e49c4cd3450fedc7e60371589f", + "a4c22d6c3d773634c2dc057e1f7c6738", + ), + ( + 4662, + "c2afad6f034704300c34f143dcdcb86c9b954cec1ebf22e7071f288c58a2ae430d3e3748d214d1021472793d3f337dc6", + "c0601cb6cd4883102f70570c2cdc0ab6", + ), + ( + 5439, + "8fee067f5a7a475a630f9db8b2eb80c1edc40eb4246a0f1c078e535df7d06451c6a9bde1a23ba70285690dd7100a8626", + "7352239f2302b08844309d28b13fa867", + ), + ( + 6216, + "60095b4172438aee61e65f5379f4ef276c3632d4ac74eea7723a2201823432614aba7b4670d9bf7a5b9126ca38f3b88a", + "c0f0b0aa651965f8514b473c5406285e", + ), + ( + 6993, + "10e754dd08b4d2a6c109fb01fce2b57d54743947e14a7e67d7efd0608baf91f7fc42a53328fe8c18d234abad8ebcdff0", + "58444988a62a99060728a7637c8499eb", + ), + ( + 7770, + "1abc4a5dcd2696336bd0e8af20fe7fc261aa424b52cfb5ad80ee7c7c793ac44f11db3506cdbbbaed0f80000925d08d52", + "e8065c563bc6018cdcbf9aaafef767e6", + ), + ( + 8547, + "26aaf74ae8bfc6aaf45ceee0476ea0a484304f5c36050d3e2265cb194a2f7c308213314232270608b6d3f1c11b834e33", + "ec50e4b3f6e4b3de24b3476623d08157", + ), + ( + 9324, + "863206305d466aa9c0d0ec674572069f61fe5009767f99ec8832912725c28c49d6a106ad3f55372c922e4e169fc382ce", + "0cfac64f49e0f128d0a18d293878f222", + ), + ( + 10101, + "bd0c0950b947a6c34f1fa6e877433b42c039a8ea7b37634c40fb47efae4958ba74ef0991cfedf3c82a0b87ef59635071", + "e0220a02b74259eeebbebede847d50f9", + ), + ( + 10878, + "d7b9901af1dacf6a8c369b993ba1c607f9b7f073d02311c72d8449d3494d477ffc8344a1d8b488020ccfc7c80fbd27e1", + "ebe3933146734a6ade2b434f2bcd78ae", + ), + ( + 11655, + "0ba265e3ef0bebf01a4f3490da462c7730aad6aa6c70bb9ce64a36d26d24fe213660e60e4d3301329170471f11ff8ca2", + "ec3dd4bf4cb7d527a86dd559c773a87b", + ), + ( + 12432, + "c3b6755a1be922ec71c1e187ead36c4e6fc307c72969c64ca1e9b7339d61e1a93a74a315fd73bed8fa5797b78b19dbe5", + "5b58dcf392749bcef91056ba9475d0ef", + ), + ( + 13209, + "2fb1a67151183daa2f0d7f0064534497357f173161349dd008499a8c1a123cc942662ecc426e2ad7743fe0ab9f5d7be1", + "c011260d328d310e2ab606aa1ef8afd4", + ), + ( + 13986, + "6afae2a07ce9bfe30fbbfb7dcf32d755bcf357334dc5c309e58cab38ebe559f25b313a0b3ca32ff1dc41f7b99718f653", + "011bf43cfbbb7ae5986f8e0fc87771a9", + ), + ( + 14763, + "cc6215c115eb6411f4712c2289f5bf0ccb5151635f9f9ceac7c1b62d8d2f4d26498079d0289f83aeb26e97b5b924ffc4", + "a015034a8d5bc83cc76c6983a5ba19ab", + ), + ( + 15540, + "3cebce794e947341c4ceec444ca43c6ac57c6f58de462bfec7566cbd59a1b6f2eae774120e29521e76120a604d1a12d9", + "d373cd2bd9000655141ac632880eca40", + ), + ( + 16317, + "899147b98d78bb5d137dc7c4f03be7eca82bcca19cc3a701261332923707aed2e6719d35d2f2bf067cd1d193a53529cf", + "ed223b64529299c787f49d631ce181c1", + ), + ( + 17094, + "aecd1830958b994b2c331b90e7d8ff79f27c83a71f5797a65ade3a30b4fa5928e79140bcd03f375591d53df96fea1a4d", + "948a7c253d54bb6b65d78530c0eb7aab", + ), + ( + 17871, + "e677ffd4ecaba5899659fefe5fe8e643004392be3be6dc5a801409870ac1e3398f47cc1d83f7a4c41925b6337e01f7fd", + "156a600c336f3ac034ca90034aa22635", + ), + ( + 18648, + "4ee50f4a98d0bbd160add6acf76765ccdac0c1cd0bb2adbbcb22dd012a1121620b739a120df7dc4091e684ddf28eb726", + "75873467b416a7b025f9f1b015bf653a", + ), + ( + 19425, + "aa025f32c0575af7209828fc7fc4591b41fa7cfb485e26c5401e63ca1fa05776f8b8af1769a15e81f2c663bca9b02ab3", + "5679efa7a4404e1e5c9b372782a41bf2", + ), + ( + 20202, + "6e77ab62d2affeb27f4ef326191b3df3863c338a629f64a785505f4a5968ff59bc011c7a27951cb00e2e7d9b9bd32fec", + "36a9c4515d34f9bb962d8876ab3b5c86", + ), + ( + 20979, + "1625b4f0e65fc66f11ba3ee6b3e20c732535654c447df6b517ced113107a1057a64477faa2af4a5ede4034bf3cff98ea", + "9058044e0f71c28d4f8d3281a3aec024", + ), + ( + 21756, + "94efe6aa55bd77bfa58c185dec313a41003f9bef02568e72c337be4de1b46c6e5bb9a9329b4f108686489b8bc9d5f4f0", + "8d6d2c90590268a26f5e7d76351f48c1", + ), + ( + 22533, + "7327a05fdb0ac92433dfc2c85c5e96e6ddcbdb01e079f8dafbee79c14cb4d5fd46047acd6bb0e09a98f6dd03dced2a0a", + "4e0f0a394f85bca35c68ef667aa9c244", + ), + ( + 23310, + "93da9e356efbc8b5ae366256f4c6fc11c11fc347aaa879d591b7c1262d90adf98925f571914696054f1d09c74783561e", + "8c83c157be439280afc790ee3fd667eb", + ), + ( + 24087, + "99b91be5ffca51b1cbc7410798b1540b5b1a3356f801ed4dc54812919c08ca5a9adc218bc51e594d97b46445a1515506", + "9436ff05729a77f673e815e464aeaa75", + ), + ( + 24864, + "074253ad5d5a5d2b072e7aeaffa04a06119ec812a88ca43481fe5e2dce02cf6736952095cd342ec70b833c12fc1777f4", + "69d8951b96866a08efbb65f2bc31cfbc", + ), + ( + 25641, + "c0a301f90597c05cf19e60c35378676764086b7156e455f4800347f8a6e733d644e4cc709fb9d95a9211f3e1e10c762a", + "3561c9802143c306ecc5e07e3b976d9e", + ), + ( + 26418, + "3c839e59d945b841acb604e1b9ae3df36a291444ce0bcae336ee875beaf208bf10af7342b375429ecb92ec54d11a5907", + "3032ffdb8daee11b2e739132c6175615", + ), + ( + 27195, + "3dc59b16603950dfc26a90bf036712eb088412e8de4d1b27c3fa6be6502ac12d89d194764fb53c3dc7d90fa696ba5a16", + "49436717edff7cd67c9a1be16d524f07", + ), + ( + 27972, + "4fbc0d40ff13376b8ed5382890cdea337b4a0c9c31b477c4008d2ef8299bd5ab771ba70b1b4b743f8f7caa1f0164d1a1", + "64a9856a3bb81dc81ff1bc1025192dc9", + ), + ( + 28749, + "6ab191aa6327f229cc94e8c7b1b7ee30bc723e6aeaf3050eb7d14cb491c3513254e9b19894c2b4f071d298401fd31945", + "101f2ffea60f246a3b57c4a530d67cf1", + ), + ( + 29526, + "d06dece58e6c7345986aae4b7f15b3317653f5387d6262f389b5cbbe804568124a876eabb89204e96b3c0f7b552df3c4", + "5c0e873adba65a9f4cb24cce4f194b18", + ), + ( + 30303, + "7a33c1268eafdc1f89ad460fa4ded8d3df9a3cabe4339706877878c64a2c8080cf3fa5ea7f2f24744e3341476b1eb5a5", + "b7dc708fc46ce5cde24a31ad549fec83", + ), + ( + 31080, + "37bf1f9fca6d705b989b2d63259ca924dc860fc6027e07d9aad79b94841227739774f5d324590df45d8f41249ef742ea", + "8ead50308c281e699b79b69dad7ecb91", + ), + ( + 31857, + "91b120c73be86f9d53326fa707cfa1411e5ac76ab998a2d7ebd73a75e3b1a04c9f0855d102184b8a3fd5d99818b0b134", + "6056d09595bd16bfa317c6f87ce64bb7", + ), + ( + 32634, + "42cc255c06184ead57b27efd0cefb0f2c788c8962a6fd15db3f25533a7f49700bca85af916f9e985f1941a6e66943b38", + "3b15e332d2f53bb97e1a9d03e6113b97", + ), + ( + 33411, + "737f8bb8f3fd03a9d13e50abba3a42f4491c36eda3eb215085abda733227ec490cb863ffbd68f915c8fb2926a899fbc3", + "b2c647d25c46aab4d4a5ede4a3b4576d", + ), + ( + 34188, + "e9caa36505e19628175d1ce8b933267380099753a41e503fa2f894cea17b7692f0b27079ed33cdd1293db9a35722d561", + "a2882adfd00f22823250215b12b3a1fd", + ), + ( + 34965, + "81ddc348ebbdfb963daa5d0c1b51bbb73cacd883d4fc4316db6bd3388779beff7be0655bbac73951f89dc53832199c11", + "f33106eb8104f3780350c6d4f82333ad", + ), + ( + 35742, + "308ce31daf40dab707e2cb4c4a5307bc403e24c971ae1e30e998449f804a167fe5f2cf617d585851b6fe9f2b4209f09c", + "44070ac90cbf350ab92289cc063e978c", + ), + ( + 36519, + "71f51b4bddbe8a52f18be75f9bdb3fca0773901b794de845450fb308c34775ede1a6da9a82b61e9682a29a3ef71274e2", + "0e387704298c444bf3afba0edc0c1c1c", + ), + ( + 37296, + "478ac94eee8c5f96210003fcb478392b91f2ef6fc3a729774e5fe82a2d8d0abc54ae1d25b3eaefb061e2bd43b70ca4ea", + "fb65ebeda52cd5848d303c0677cecb7f", + ), + ( + 38073, + "bc3a9390618da7d644be932627353e2c92024df939d2d8497fba61fae3dd822cdd3e130c1707f4a9d5d4a0cbb4b3e0b3", + "d790d529a837ec79f7cc3f66ed9a399f", + ), + ( + 38850, + "ef0e63a53a10e56477c47e13320b8a7d330aee3a4363c850edc56c0707a2686478e5a5193f54ceb33467ab7e8a22aa21", + "6f2c18742f106f16fc290767342fb62b", + ), + ( + 39627, + "c16f63533c099d872d9a01c326db7756e7eb488c756b9a6ebf575993d8ea2eb45c572b2e162f061e145710e0e21e8e18", + "a57afde7938b223ae5e109a03db4ee4c", + ), + ( + 40404, + "ade484ae8c13465a73589ef14789bb6891c933453e198df84edd34b4ac5c83aa90f2cf61fa072fa4d8f5b5c4cd68fa9e", + "a01d13009db86ac442f7afd39d83309f", + ), + ( + 41181, + "6c5c7eed0e043a0bd60bcac9b5b546e150028d70c1efefc9ff69037ef4dc1a36878b171b9f2a639df822d11054a0e405", + "6321c8622ca5866c875d340206d06a28", + ), + ( + 41958, + "dd311c54222fb0d92858719cf5b1c51bb5e3ca2539ffd68f1dd6c7e38969495be935804855ccdcc4b4cf221fcdbda886", + "cf401eb819b5dc5cd8c909aae9b3b34b", + ), + ( + 42735, + "31cda9d663199b32eff042dd16c0b909ba999641e77ba751c91752bfc4d595e17ec6467119e74a600b72da72ba287d0a", + "12fd6298ab5d744eb6ade3106565afad", + ), + ( + 43512, + "11b014057d51a8384d549d5d083c4406b575df6a9295853dd8f2f84f078cc241bb90495a119126b10b9510efcb68c0d3", + "a48a49eea5dc90359ef21f32132f8604", + ), + ( + 44289, + "b44f5dbeecd76ee7efe3fb4dfe10ba8135d7a5e4d104149f4a91c5c6ee9446d9be19fb4c9ba668b074466d3892e22228", + "07e1cbb7a19174d9b1e4d5a2c741cc14", + ), + ( + 45066, + "d87bbba3a3c739cab622386c89aeb685a70009fab1a606bd34622adfa3a75a05b58d56ee6b9874d414db38a6a32927b3", + "a27cd252712cd2a1a2d95dea39f888d4", + ), + ( + 45843, + "abb90e60ea13c6cb3b401b8e271637416b87fbede165dde7be1d34abe4427dae4b39b499352cacac909bc43fb94028c8", + "df3ae762b9257936feda435a61a9c3a1", + ), + ( + 46620, + "56d1132ee6e0f85543950d2d9667244b66b0ce6414eacd1859b128ed0b9026b31a25bfdcce3d1a0ce7c39d99f609c89c", + "cfe7c3c3f1cb615e2d210cc8136443e6", + ), + ( + 47397, + "ecb023ec4c23cf95d1848a38b359f1f590f172dee9d8fb1be6bc9c4fb2ce96f612d60d7b111de539ab8313a87b821176", + "501d24752bf55cb12239863981898a07", + ), + ( + 48174, + "34236ab60f05bb510aa0880fec358fb2002903efa14c912cab8a399e09418f97223ca2f7b8d6798c11d39e79032eaaa8", + "4ecaba4eae886aa429927188abab9623", + ), + ( + 48951, + "55e8b40fad90a3d8c85a0f4d5bcf5975b8a6e2fb78377109f5b607a5e367187fbbc9a1e978aab3228fbf43ad23d0ad13", + "84c43bc30eb4a67230b6c634fe3c7782", + ), + ( + 49728, + "14b1f896d0d01ecff4e456c3c392b1ca2bad9f1ef07713f84cdd89e663aa27ca77d80213ed57a89431eb992b11d98749", + "7f58c2f9a249f70fe1c6f9b4f65e5a1d", + ), + ( + 50505, + "1335b1fb56196e0b371fa53ab7445845fdefcea3eb2833478deb3526e2ec888945e95ee8239b52caae5b9920ba4f43bb", + "5fd729126b236ce3e0686fc706dce20f", + ), + ( + 51282, + "0d1983a6cab870c5e78f89a11dd30e7d2c71a3882f8bba3e71dc1b96a2d9fc6cc6d91d683b74456b886de34df792cfda", + "7731ae6e6c54dfde12f6116357e812ea", + ), + ( + 52059, + "9d619fb4aa8441baaefed7b778693c291f2c1441b206ec135930fac3529d26587ac36f4472949e0b198b51c0c5a9d0f8", + "39db2c996aea28996e03d576c118630f", + ), + ( + 52836, + "31dca4fa285878ba3efc3b66a248a078b69a11c3c73f81077377c4ffcb7002627aad5faa955e3141c1d8508aad68c8f6", + "32ac1e5a09e7e629ff95f30aa9b69c00", + ), + ( + 53613, + "931a9969cf2bb02302c32b1eecd4933805e2da403d85aaf98c82c68129fb95f089eb85c65a6fcbc7d81bedb39de0cabb", + "1a6f54b87c12868da530eac94d99eb31", + ), + ( + 54390, + "2f0742565801a37810ecb3f50a6f782e73a369a790d1a6a85135e7ffa12fc063db8909ab9eca7cf7308832887a6149d1", + "1b18ed6a8f901b7947626216839f0643", + ), + ( + 55167, + "901defbd308b54deef89acd0d94e4387b370f9d2e6f870d72da2e447ed3ebe69c5f9f144488bd6207a732102160bff47", + "1e0e6a05fcc0794121f617e28cfac1a0", + ), + ( + 55944, + "df984a5f7475250155dc4733a746e98446dc93a56a3f3bff691ddfef7deefb32b1da1b0e7e15cce443831ebfb3e30ada", + "876121af882d0ebeae38f111f3d4b6e8", + ), + ( + 56721, + "acb693ed837b33561408cb1eed636e0082ac404f3fd72d277fa146ae5cd81a1fde3645f4cdc7babd8ba044b78075cb67", + "5b90ed6c7943fc6da623c536e2ff1352", + ), + ( + 57498, + "dffb54bf5938e812076cfbf15cd524d72a189566c7980363a49dd89fb49e230d9742ef0b0e1ac543dca14366d735d152", + "22aee072457306e32747fbbbc3ae127c", + ), + ( + 58275, + "92dbc245a980fc78974f7a27e62c22b12a00be9d3ef8d3718ff85f6d5fbcbf1d9d1e0f0a3daeb8c2628d090550a0ff6b", + "5fa348117faba4ac8c9d9317ff44cd2d", + ), + ( + 59052, + "57721475cb719691850696d9a8ad4c28ca8ef9a7d45874ca21df4df250cb87ea60c464f4e3252e2d6161ed36c4b56d75", + "24d92ae7cac56d9c0276b06f7428d5df", + ), + ( + 59829, + "d0936026440b5276747cb9fb7dc96de5d4e7846c233ca5f6f9354b2b39f760333483cbe99ffa905facb347242f58a7ef", + "05c57068e183f9d835e7f461202f923c", + ), + ( + 60606, + "7b3bb3527b73a8692f076f6a503b2e09b427119543c7812db73c7c7fb2d43af9ecbd2a8a1452ac8ada96ad0bad7bb185", + "f958635a193fec0bfb958e97961381df", + ), + ( + 61383, + "ff0d00255a36747eced86acfccd0cf9ef09faa9f44c8cf382efec462e7ead66e562a971060c3f32798ba142d9e1640a2", + "838159b222e56aadde8229ed56a14095", + ), + ( + 62160, + "15806e088ed1428cd73ede3fecf5b60e2a616f1925004dadd2cab8e847059f795659659e82a4554f270baf88bf60af63", + "fed2aa0c9c0a73d499cc970aef21c52f", + ), + ( + 62937, + "cfad71b23b6da51256bd1ddbd1ac77977fe10b2ad0a830a23a794cef914bf71a9519d78a5f83fc411e8d8db996a45d4e", + "e1ea412fd3e1bd91c24b6b6445e8ff43", + ), + ( + 63714, + "7d03a3698a79b1af1663e3e485c2efdc306ecd87b2644f2e01d83a35999d6cdf12241b6114d60d107c10c0d0c9cc0d23", + "e6a3c3f3fd2d9cfcdc06cca2f59e9a83", + ), + ( + 64491, + "e12b168cce0e82ed1db88df549f39b3ff40b5884a09fceae69c4c3db13c1c37ea79531c47b2700d1c27774a1ab7e8b35", + "4cbb14d789f5cd8eca49ce9e1d442ea1", + ), + ( + 65268, + "056c9d1172cfa76ce7f19c605e5969c284b82dca155dc9c1ed58062ab4d5a7704e27fe69f3aa745b73f45f1cd0ee57df", + "8195187f092d52c2a8695b680568b934", + ), + ]; + /// const NIST_AES_GCM_TEST_VECTORS: &[GcmTV<[u8; 32]>] = &[ GcmTV { diff --git a/crypto/src/bn.rs b/crypto/src/bn.rs index 01ef5c5d7..7d79cc14f 100644 --- a/crypto/src/bn.rs +++ b/crypto/src/bn.rs @@ -845,14 +845,10 @@ impl Neg for BigNum { #[cfg(test)] mod tests { - use crate::{ - bn::{BigNum, BigNumContext}, - init, - }; + use crate::bn::{BigNum, BigNumContext}; #[test] fn test_to_from_slice() { - init(); let v0 = BigNum::from_u32(10_203_004).unwrap(); let vec = v0.to_vec(); let v1 = BigNum::from_slice(&vec).unwrap(); @@ -862,7 +858,6 @@ mod tests { #[test] fn test_negation() { - init(); let a = BigNum::from_u32(909_829_283).unwrap(); assert!(!a.is_negative()); @@ -871,7 +866,6 @@ mod tests { #[test] fn test_shift() { - init(); let a = BigNum::from_u32(909_829_283).unwrap(); assert!(a == &(&a << 1) >> 1); @@ -880,7 +874,6 @@ mod tests { #[cfg(not(osslconf = "OPENSSL_NO_DEPRECATED_3_0"))] #[test] fn test_prime_numbers() { - init(); let a = BigNum::from_u32(19_029_017).unwrap(); let mut p = BigNum::new().unwrap(); p.generate_prime(128, true, None, Some(&a)).unwrap(); @@ -893,7 +886,6 @@ mod tests { #[cfg(ossl110)] #[test] fn test_secure_bn() { - init(); let a = BigNum::new().unwrap(); assert!(!a.is_secure()); @@ -904,7 +896,6 @@ mod tests { #[cfg(ossl110)] #[test] fn test_const_time_bn() { - init(); let a = BigNum::new().unwrap(); assert!(!a.is_const_time()); diff --git a/crypto/src/cipher_ctx.rs b/crypto/src/cipher_ctx.rs index f5cba9441..91ab22bd3 100644 --- a/crypto/src/cipher_ctx.rs +++ b/crypto/src/cipher_ctx.rs @@ -127,11 +127,9 @@ impl CipherCtxRef { #[cfg(test)] mod test { use super::*; - use crate::init; #[test] fn aes_128_ecb() { - init(); let key = [1u8; 16]; let ctx = CipherCtx::new().unwrap(); unsafe { diff --git a/crypto/src/ec.rs b/crypto/src/ec.rs index a1b0ef15b..c0f367442 100644 --- a/crypto/src/ec.rs +++ b/crypto/src/ec.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::ptr; use crate::bn::{BigNumContext, BigNumContextRef, BigNumRef}; @@ -5,13 +6,11 @@ use crate::error::{cvt, cvt_n, cvt_p, ErrorStack}; use foreign_types::{foreign_type, ForeignType, ForeignTypeRef}; foreign_type! { - #[derive(Clone)] pub unsafe type EcGroup: Send + Sync { type CType = ffi::EC_GROUP; fn drop = ffi::EC_GROUP_free; } /// Public and optional private key on the given curve. - #[derive(Clone)] pub unsafe type EcKey { type CType = ffi::EC_KEY; fn drop = ffi::EC_KEY_free; @@ -125,3 +124,27 @@ impl EcPointRef { } } } + +impl ToOwned for EcKeyRef { + type Owned = EcKey; + + fn to_owned(&self) -> EcKey { + unsafe { + let r = ffi::EC_KEY_up_ref(self.as_ptr()); + assert!(r == 1); + EcKey::from_ptr(self.as_ptr()) + } + } +} + +impl Clone for EcKey { + fn clone(&self) -> EcKey { + (**self).to_owned() + } +} + +impl fmt::Debug for EcKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "EcKey") + } +} diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 56d47b4be..f13d75eb1 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -26,15 +26,21 @@ pub use aes_openssl as aes; mod aes_tests; +#[cfg(target_os = "macos")] pub mod aes_gmac_siv_fruity; -pub mod aes_gmac_siv_openssl; #[cfg(target_os = "macos")] pub use aes_gmac_siv_fruity as aes_gmac_siv; + +#[cfg(not(target_os = "macos"))] +pub mod aes_gmac_siv_openssl; #[cfg(not(target_os = "macos"))] pub use aes_gmac_siv_openssl as aes_gmac_siv; -/// This must be called before using any function from this library. -pub fn init() { +use ctor::ctor; + +#[ctor] +fn openssl_init() { + println!("OpenSSL init()"); ffi::init(); } @@ -52,4 +58,5 @@ pub fn secure_eq + ?Sized, B: AsRef<[u8]> + ?Sized>(a: &A, b: &B) false } } + pub const ZEROES: [u8; 64] = [0_u8; 64]; diff --git a/crypto/src/p384.rs b/crypto/src/p384.rs index c20a671b6..fa092f094 100644 --- a/crypto/src/p384.rs +++ b/crypto/src/p384.rs @@ -1322,11 +1322,10 @@ pub use openssl_based::*; #[cfg(test)] mod tests { - use crate::{init, p384::P384KeyPair, secure_eq}; + use crate::{p384::P384KeyPair, secure_eq}; #[test] fn generate_sign_verify_agree() { - init(); let kp = P384KeyPair::generate(); let kp2 = P384KeyPair::generate(); diff --git a/network-hypervisor/Cargo.toml b/network-hypervisor/Cargo.toml index fa6951cfe..7e768e488 100644 --- a/network-hypervisor/Cargo.toml +++ b/network-hypervisor/Cargo.toml @@ -16,6 +16,8 @@ lz4_flex = { version = "^0", features = ["safe-encode", "safe-decode", "checked- serde = { version = "^1", features = ["derive"], default-features = false } phf = { version = "^0", features = ["macros", "std"], default-features = false } num-traits = "^0" +rmp-serde = "^1" +fastcdc = "^3" [dev-dependencies] rand = "*" diff --git a/network-hypervisor/src/protocol.rs b/network-hypervisor/src/protocol.rs index 92bc84ce6..b7b1fb1dd 100644 --- a/network-hypervisor/src/protocol.rs +++ b/network-hypervisor/src/protocol.rs @@ -12,8 +12,6 @@ use zerotier_crypto::aes_gmac_siv::AesGmacSiv; use zerotier_crypto::hash::hmac_sha384; use zerotier_crypto::secret::Secret; -use self::v1::VERB_FLAG_COMPRESSED; - /* * Legacy V1 protocol versions: * @@ -75,6 +73,8 @@ pub type MessageId = u64; /// ZeroTier VL1 and VL2 wire protocol message types. pub mod message_type { + // VL1: Virtual Layer 1, the peer to peer network + pub const VL1_NOP: u8 = 0x00; pub const VL1_HELLO: u8 = 0x01; pub const VL1_ERROR: u8 = 0x02; @@ -85,6 +85,8 @@ pub mod message_type { pub const VL1_PUSH_DIRECT_PATHS: u8 = 0x10; pub const VL1_USER_MESSAGE: u8 = 0x14; + // VL2: Virtual Layer 2, the virtual Ethernet network + pub const VL2_MULTICAST_LIKE: u8 = 0x09; pub const VL2_NETWORK_CREDENTIALS: u8 = 0x0a; pub const VL2_NETWORK_CONFIG_REQUEST: u8 = 0x0b; @@ -111,6 +113,12 @@ pub mod message_type { } } +/// Verb (inner) flag indicating that the packet's payload (after the verb) is LZ4 compressed. +pub const MESSAGE_FLAG_COMPRESSED: u8 = 0x80; + +/// Mask to get only the verb from the verb + verb flags byte. +pub const MESSAGE_TYPE_MASK: u8 = 0x1f; + /// Default maximum payload size for UDP transport. /// /// This is small enough to traverse numerous weird networks including PPPoE and Google Cloud's @@ -201,18 +209,6 @@ pub mod v1 { /// Byte found at FRAGMENT_INDICATOR_INDEX to indicate a fragment. pub const FRAGMENT_INDICATOR: u8 = 0xff; - /// Verb (inner) flag indicating that the packet's payload (after the verb) is LZ4 compressed. - pub const VERB_FLAG_COMPRESSED: u8 = 0x80; - - /// Verb (inner) flag indicating that payload is authenticated with HMAC-SHA384. - pub const VERB_FLAG_EXTENDED_AUTHENTICATION: u8 = 0x40; - - /// Mask to get only the verb from the verb + verb flags byte. - pub const VERB_MASK: u8 = 0x1f; - - /// Maximum number of verbs that the protocol can support. - pub const VERB_MAX_COUNT: usize = 32; - /// Header (outer) flag indicating that this packet has additional fragments. pub const HEADER_FLAG_FRAGMENTED: u8 = 0x40; @@ -245,30 +241,6 @@ pub mod v1 { /// Maximum number of hops to allow. pub const FORWARD_MAX_HOPS: u8 = 3; - /// Attempt to compress a packet's payload with LZ4 - /// - /// If this returns true the destination buffer will contain a compressed packet. If false is - /// returned the contents of 'dest' are entirely undefined. This indicates that the data was not - /// compressable or some other error occurred. - pub fn compress_packet(src: &[u8], dest: &mut Buffer) -> bool { - if src.len() > (VERB_INDEX + 16) { - let compressed_data_size = { - let d = unsafe { dest.entire_buffer_mut() }; - d[..VERB_INDEX].copy_from_slice(&src[..VERB_INDEX]); - d[VERB_INDEX] = src[VERB_INDEX] | VERB_FLAG_COMPRESSED; - lz4_flex::block::compress_into(&src[VERB_INDEX + 1..], &mut d[VERB_INDEX + 1..]) - }; - if compressed_data_size.is_ok() { - let compressed_data_size = compressed_data_size.unwrap(); - if compressed_data_size > 0 && compressed_data_size < (src.len() - VERB_INDEX) { - unsafe { dest.set_size_unchecked(VERB_INDEX + 1 + compressed_data_size) }; - return true; - } - } - } - return false; - } - /// Set header flag indicating that a packet is fragmented. /// /// This will panic if the buffer provided doesn't contain a proper header. @@ -534,7 +506,7 @@ pub fn compress(payload: &mut [u8]) -> usize { let mut tmp = [0u8; 65536]; if let Ok(mut compressed_size) = lz4_flex::block::compress_into(&payload[1..], &mut tmp) { if compressed_size < (payload.len() - 1) { - payload[0] |= VERB_FLAG_COMPRESSED; + payload[0] |= MESSAGE_FLAG_COMPRESSED; payload[1..(1 + compressed_size)].copy_from_slice(&tmp[..compressed_size]); return 1 + compressed_size; } diff --git a/network-hypervisor/src/vl1/endpoint.rs b/network-hypervisor/src/vl1/endpoint.rs index 21a612f65..0bf307dae 100644 --- a/network-hypervisor/src/vl1/endpoint.rs +++ b/network-hypervisor/src/vl1/endpoint.rs @@ -10,10 +10,10 @@ use crate::vl1::identity::IDENTITY_FINGERPRINT_SIZE; use crate::vl1::inetaddress::InetAddress; use crate::vl1::{Address, MAC}; +use zerotier_utils::base64; use zerotier_utils::buffer::Buffer; use zerotier_utils::error::InvalidFormatError; use zerotier_utils::marshalable::{Marshalable, UnmarshalError}; -use zerotier_utils::{base64_decode_url_nopad, base64_encode_url_nopad}; pub const TYPE_NIL: u8 = 0; pub const TYPE_ZEROTIER: u8 = 1; @@ -319,7 +319,7 @@ impl ToString for Endpoint { fn to_string(&self) -> String { match self { Endpoint::Nil => format!("nil"), - Endpoint::ZeroTier(a, ah) => format!("zt:{}-{}", a.to_string(), base64_encode_url_nopad(ah)), + Endpoint::ZeroTier(a, ah) => format!("zt:{}-{}", a.to_string(), base64::encode_url_nopad(ah)), Endpoint::Ethernet(m) => format!("eth:{}", m.to_string()), Endpoint::WifiDirect(m) => format!("wifip2p:{}", m.to_string()), Endpoint::Bluetooth(m) => format!("bt:{}", m.to_string()), @@ -327,8 +327,8 @@ impl ToString for Endpoint { Endpoint::IpUdp(ip) => format!("udp:{}", ip.to_string()), Endpoint::IpTcp(ip) => format!("tcp:{}", ip.to_string()), Endpoint::Http(url) => format!("url:{}", url.clone()), // http or https - Endpoint::WebRTC(offer) => format!("webrtc:{}", base64_encode_url_nopad(offer.as_slice())), - Endpoint::ZeroTierEncap(a, ah) => format!("zte:{}-{}", a.to_string(), base64_encode_url_nopad(ah)), + Endpoint::WebRTC(offer) => format!("webrtc:{}", base64::encode_url_nopad(offer.as_slice())), + Endpoint::ZeroTierEncap(a, ah) => format!("zte:{}-{}", a.to_string(), base64::encode_url_nopad(ah)), } } } @@ -351,7 +351,7 @@ impl FromStr for Endpoint { let address_and_hash = endpoint_data.split_once("-"); if address_and_hash.is_some() { let (address, hash) = address_and_hash.unwrap(); - if let Some(hash) = base64_decode_url_nopad(hash) { + if let Some(hash) = base64::decode_url_nopad(hash) { if hash.len() == IDENTITY_FINGERPRINT_SIZE { if endpoint_type == "zt" { return Ok(Endpoint::ZeroTier(Address::from_str(address)?, hash.as_slice().try_into().unwrap())); @@ -370,7 +370,7 @@ impl FromStr for Endpoint { "tcp" => return Ok(Endpoint::IpTcp(InetAddress::from_str(endpoint_data)?)), "url" => return Ok(Endpoint::Http(endpoint_data.into())), "webrtc" => { - if let Some(offer) = base64_decode_url_nopad(endpoint_data) { + if let Some(offer) = base64::decode_url_nopad(endpoint_data) { return Ok(Endpoint::WebRTC(offer)); } } diff --git a/network-hypervisor/src/vl1/identity.rs b/network-hypervisor/src/vl1/identity.rs index 9b1a33da2..a50e10f68 100644 --- a/network-hypervisor/src/vl1/identity.rs +++ b/network-hypervisor/src/vl1/identity.rs @@ -16,10 +16,11 @@ use zerotier_crypto::x25519::*; use zerotier_crypto::{hash::*, secure_eq}; use zerotier_utils::arrayvec::ArrayVec; +use zerotier_utils::base64; use zerotier_utils::buffer::Buffer; use zerotier_utils::error::{InvalidFormatError, InvalidParameterError}; +use zerotier_utils::hex; use zerotier_utils::marshalable::{Marshalable, UnmarshalError}; -use zerotier_utils::{base64_decode_url_nopad, base64_encode_url_nopad, hex}; use crate::protocol::{ADDRESS_SIZE, ADDRESS_SIZE_STRING, IDENTITY_POW_THRESHOLD}; use crate::vl1::Address; @@ -492,7 +493,7 @@ impl Identity { &p384.ecdsa_self_signature, &p384.ed25519_self_signature, ); - s.push_str(base64_encode_url_nopad(&p384_joined).as_str()); + s.push_str(base64::encode_url_nopad(&p384_joined).as_str()); if self.secret.is_some() && include_private { let secret = self.secret.as_ref().unwrap(); if secret.p384.is_some() { @@ -502,7 +503,7 @@ impl Identity { p384_secret.ecdsa.secret_key_bytes().as_bytes(), ); s.push(':'); - s.push_str(base64_encode_url_nopad(&p384_secret_joined).as_str()); + s.push_str(base64::encode_url_nopad(&p384_secret_joined).as_str()); } } } @@ -586,8 +587,8 @@ impl FromStr for Identity { let keys = [ hex::from_string(keys[0].unwrap_or("")), hex::from_string(keys[1].unwrap_or("")), - base64_decode_url_nopad(keys[2].unwrap_or("")).unwrap_or_else(|| Vec::new()), - base64_decode_url_nopad(keys[3].unwrap_or("")).unwrap_or_else(|| Vec::new()), + base64::decode_url_nopad(keys[2].unwrap_or("")).unwrap_or_else(|| Vec::new()), + base64::decode_url_nopad(keys[3].unwrap_or("")).unwrap_or_else(|| Vec::new()), ]; if keys[0].len() != C25519_PUBLIC_KEY_SIZE + ED25519_PUBLIC_KEY_SIZE { return Err(InvalidFormatError); diff --git a/network-hypervisor/src/vl1/node.rs b/network-hypervisor/src/vl1/node.rs index 4db0ec531..e02c65dc0 100644 --- a/network-hypervisor/src/vl1/node.rs +++ b/network-hypervisor/src/vl1/node.rs @@ -25,13 +25,12 @@ use zerotier_utils::gate::IntervalGate; use zerotier_utils::hex; use zerotier_utils::marshalable::Marshalable; use zerotier_utils::ringbuffer::RingBuffer; -use zerotier_utils::thing::Thing; /// Interface trait to be implemented by code that's using the ZeroTier network hypervisor. /// /// This is analogous to a C struct full of function pointers to callbacks along with some /// associated type definitions. -pub trait ApplicationLayer: Sync + Send { +pub trait ApplicationLayer: Sync + Send + 'static { /// Type for local system sockets. type LocalSocket: Sync + Send + Hash + PartialEq + Eq + Clone + ToString + Sized + 'static; @@ -56,6 +55,9 @@ pub trait ApplicationLayer: Sync + Send { /// unbound, etc. fn local_socket_is_valid(&self, socket: &Self::LocalSocket) -> bool; + /// Check if this node should respond to messages from a given peer at all. + fn should_respond_to(&self, id: &Valid) -> bool; + /// Called to send a packet over the physical network (virtual -> physical). /// /// This sends with UDP-like semantics. It should do whatever best effort it can and return. @@ -130,24 +132,6 @@ pub enum PacketHandlerResult { /// it could also be implemented for testing or "off label" use of VL1 to carry different protocols. #[allow(unused)] pub trait InnerProtocolLayer: Sync + Send { - /// Check if this node should respond to messages from a given peer at all. - /// - /// The default implementation always returns true. - fn should_respond_to(&self, id: &Valid) -> bool { - true - } - - /// Check if this node has any trust relationship with the provided identity. - /// - /// This should return true if there is any special trust relationship. It controls things - /// like sharing of detailed P2P connectivity data, which should be limited to peers with - /// some privileged relationship like mutual membership in a network. - /// - /// The default implementation always returns true. - fn has_trust_relationship(&self, id: &Valid) -> bool { - true - } - /// Handle a packet, returning true if it was handled by the next layer. /// /// Do not attempt to handle OK or ERROR. Instead implement handle_ok() and handle_error(). @@ -155,9 +139,9 @@ pub trait InnerProtocolLayer: Sync + Send { fn handle_packet( &self, app: &Application, - node: &Node, - source: &Arc, - source_path: &Arc, + node: &Node, + source: &Arc>, + source_path: &Arc>, source_hops: u8, message_id: u64, verb: u8, @@ -172,9 +156,9 @@ pub trait InnerProtocolLayer: Sync + Send { fn handle_error( &self, app: &Application, - node: &Node, - source: &Arc, - source_path: &Arc, + node: &Node, + source: &Arc>, + source_path: &Arc>, source_hops: u8, message_id: u64, in_re_verb: u8, @@ -191,9 +175,9 @@ pub trait InnerProtocolLayer: Sync + Send { fn handle_ok( &self, app: &Application, - node: &Node, - source: &Arc, - source_path: &Arc, + node: &Node, + source: &Arc>, + source_path: &Arc>, source_hops: u8, message_id: u64, in_re_verb: u8, @@ -205,12 +189,12 @@ pub trait InnerProtocolLayer: Sync + Send { } } -struct RootInfo { +struct RootInfo { /// Root sets to which we are a member. sets: HashMap>, /// Root peers and their statically defined endpoints (from root sets). - roots: HashMap, Vec>, + roots: HashMap>, Vec>, /// If this node is a root, these are the root sets to which it's a member in binary serialized form. /// Set to None if this node is not a root, meaning it doesn't appear in any of its root sets. @@ -236,17 +220,15 @@ struct BackgroundTaskIntervals { whois_queue_retry: IntervalGate<{ WHOIS_RETRY_INTERVAL }>, } -struct WhoisQueueItem { - v1_proto_waiting_packets: RingBuffer<(Weak, PooledPacketBuffer), WHOIS_MAX_WAITING_PACKETS>, +struct WhoisQueueItem { + v1_proto_waiting_packets: + RingBuffer<(Weak>, PooledPacketBuffer), WHOIS_MAX_WAITING_PACKETS>, last_retry_time: i64, retry_count: u16, } -const PATH_MAP_SIZE: usize = std::mem::size_of::() + 128], Arc>>(); -type PathMap = HashMap, Arc>; - /// A ZeroTier VL1 node that can communicate securely with the ZeroTier peer-to-peer network. -pub struct Node { +pub struct Node { /// A random ID generated to identify this particular running instance. pub instance_id: [u8; 16], @@ -257,27 +239,23 @@ pub struct Node { intervals: Mutex, /// Canonicalized network paths, held as Weak<> to be automatically cleaned when no longer in use. - paths: RwLock>, // holds a PathMap<> but as a Thing<> to hide ApplicationLayer template parameter + paths: RwLock, Arc>>>, /// Peers with which we are currently communicating. - peers: RwLock>>, + peers: RwLock>>>, /// This node's trusted roots, sorted in ascending order of quality/preference, and cluster definitions. - roots: RwLock, + roots: RwLock>, /// Current best root. - best_root: RwLock>>, + best_root: RwLock>>>, /// Queue of identities being looked up. - whois_queue: Mutex>, + whois_queue: Mutex>>, } -impl Node { - pub fn new( - app: &Application, - auto_generate_identity: bool, - auto_upgrade_identity: bool, - ) -> Result { +impl Node { + pub fn new(app: &Application, auto_generate_identity: bool, auto_upgrade_identity: bool) -> Result { let mut id = { let id = app.load_node_identity(); if id.is_none() { @@ -308,7 +286,7 @@ impl Node { instance_id: random::get_bytes_secure(), identity: id, intervals: Mutex::new(BackgroundTaskIntervals::default()), - paths: RwLock::new(Thing::new(PathMap::::new())), + paths: RwLock::new(HashMap::new()), peers: RwLock::new(HashMap::new()), roots: RwLock::new(RootInfo { sets: HashMap::new(), @@ -323,7 +301,7 @@ impl Node { } #[inline] - pub fn peer(&self, a: Address) -> Option> { + pub fn peer(&self, a: Address) -> Option>> { self.peers.read().unwrap().get(&a).cloned() } @@ -334,13 +312,13 @@ impl Node { /// Get the current "best" root from among this node's trusted roots. #[inline] - pub fn best_root(&self) -> Option> { + pub fn best_root(&self) -> Option>> { self.best_root.read().unwrap().clone() } /// Check whether a peer is a root according to any root set trusted by this node. #[inline] - pub fn is_peer_root(&self, peer: &Peer) -> bool { + pub fn is_peer_root(&self, peer: &Peer) -> bool { self.roots.read().unwrap().roots.keys().any(|p| p.identity.eq(&peer.identity)) } @@ -391,7 +369,7 @@ impl Node { self.roots.read().unwrap().sets.values().cloned().map(|s| s.remove_typestate()).collect() } - pub fn do_background_tasks(&self, app: &Application) -> Duration { + pub fn do_background_tasks(&self, app: &Application) -> Duration { const INTERVAL_MS: i64 = 1000; const INTERVAL: Duration = Duration::from_millis(INTERVAL_MS as u64); let time_ticks = app.time_ticks(); @@ -622,7 +600,7 @@ impl Node { let mut need_keepalive = Vec::new(); // First check all paths in read mode to avoid blocking the entire node. - for (k, path) in self.paths.read().unwrap().get::>().iter() { + for (k, path) in self.paths.read().unwrap().iter() { if app.local_socket_is_valid(k.local_socket()) { match path.service(time_ticks) { PathServiceResult::Ok => {} @@ -636,19 +614,13 @@ impl Node { // Lock in write mode and remove dead paths, doing so piecemeal to again avoid blocking. for dp in dead_paths.iter() { - self.paths.write().unwrap().get_mut::>().remove(dp); + self.paths.write().unwrap().remove(dp); } // Finally run keepalive sends as a batch. let keepalive_buf = [time_ticks as u8]; // just an arbitrary byte, no significance for p in need_keepalive.iter() { - app.wire_send( - &p.endpoint, - Some(p.local_socket::()), - Some(p.local_interface::()), - &keepalive_buf, - 0, - ); + app.wire_send(&p.endpoint, Some(&p.local_socket), Some(&p.local_interface), &keepalive_buf, 0); } } @@ -674,7 +646,7 @@ impl Node { INTERVAL } - pub fn handle_incoming_physical_packet( + pub fn handle_incoming_physical_packet( &self, app: &Application, inner: &Inner, @@ -696,14 +668,7 @@ impl Node { source_local_interface.to_string() ); - // An 0xff value at byte [8] means this is a ZSSP packet. This is accomplished via the - // backward compatibility hack of always having 0xff at byte [4] of 6-byte session IDs - // and by having 0xffffffffffff be the "nil" session ID for session init packets. ZSSP - // is the new V2 Noise-based forward-secure transport protocol. What follows below this - // is legacy handling of the old v1 protocol. - if packet.u8_at(8).map_or(false, |x| x == 0xff) { - todo!(); - } + // TODO: detect inbound ZSSP sessions, handle ZSSP mode. // Legacy ZeroTier V1 packet handling if let Ok(fragment_header) = packet.struct_mut_at::(0) { @@ -712,7 +677,7 @@ impl Node { if dest == self.identity.address { let fragment_header = &*fragment_header; // discard mut - let path = self.canonical_path::(source_endpoint, source_local_socket, source_local_interface, time_ticks); + let path = self.canonical_path(source_endpoint, source_local_socket, source_local_interface, time_ticks); path.log_receive_anything(time_ticks); if fragment_header.is_fragment() { @@ -827,8 +792,8 @@ impl Node { if let Some(forward_path) = peer.direct_path() { app.wire_send( &forward_path.endpoint, - Some(forward_path.local_socket::()), - Some(forward_path.local_interface::()), + Some(&forward_path.local_socket), + Some(&forward_path.local_interface), packet.as_bytes(), 0, ); @@ -845,11 +810,11 @@ impl Node { } /// Enqueue and send a WHOIS query for a given address, adding the supplied packet (if any) to the list to be processed on reply. - fn whois( + fn whois( &self, app: &Application, address: Address, - waiting_packet: Option<(Weak, PooledPacketBuffer)>, + waiting_packet: Option<(Weak>, PooledPacketBuffer)>, time_ticks: i64, ) { { @@ -873,7 +838,7 @@ impl Node { } /// Send a WHOIS query to the current best root. - fn send_whois(&self, app: &Application, mut addresses: &[Address], time_ticks: i64) { + fn send_whois(&self, app: &Application, mut addresses: &[Address], time_ticks: i64) { debug_assert!(!addresses.is_empty()); debug_event!(app, "[vl1] [v1] sending WHOIS for {}", { let mut tmp = String::new(); @@ -905,7 +870,7 @@ impl Node { } /// Called by Peer when an identity is received from another node, e.g. via OK(WHOIS). - pub(crate) fn handle_incoming_identity( + pub(crate) fn handle_incoming_identity( &self, app: &Application, inner: &Inner, @@ -918,7 +883,7 @@ impl Node { let mut whois_queue = self.whois_queue.lock().unwrap(); if let Some(qi) = whois_queue.get_mut(&received_identity.address) { let address = received_identity.address; - if inner.should_respond_to(&received_identity) { + if app.should_respond_to(&received_identity) { let mut peers = self.peers.write().unwrap(); if let Some(peer) = peers.get(&address).cloned().or_else(|| { Peer::new(&self.identity, received_identity, time_ticks) @@ -957,31 +922,23 @@ impl Node { } /// Get the canonical Path object corresponding to an endpoint. - pub(crate) fn canonical_path( + pub(crate) fn canonical_path( &self, ep: &Endpoint, local_socket: &Application::LocalSocket, local_interface: &Application::LocalInterface, time_ticks: i64, - ) -> Arc { + ) -> Arc> { let paths = self.paths.read().unwrap(); - if let Some(path) = paths.get::>().get(&PathKey::Ref(ep, local_socket)) { + if let Some(path) = paths.get(&PathKey::Ref(ep, local_socket)) { path.clone() } else { drop(paths); self.paths .write() .unwrap() - .get_mut::>() .entry(PathKey::Copied(ep.clone(), local_socket.clone())) - .or_insert_with(|| { - Arc::new(Path::new::( - ep.clone(), - local_socket.clone(), - local_interface.clone(), - time_ticks, - )) - }) + .or_insert_with(|| Arc::new(Path::new(ep.clone(), local_socket.clone(), local_interface.clone(), time_ticks))) .clone() } } diff --git a/network-hypervisor/src/vl1/path.rs b/network-hypervisor/src/vl1/path.rs index 5a59364ec..da32ae857 100644 --- a/network-hypervisor/src/vl1/path.rs +++ b/network-hypervisor/src/vl1/path.rs @@ -7,10 +7,8 @@ use std::sync::Mutex; use crate::protocol; use crate::vl1::endpoint::Endpoint; -use crate::vl1::node::*; use zerotier_crypto::random; -use zerotier_utils::thing::Thing; use zerotier_utils::NEVER_HAPPENED_TICKS; pub(crate) const SERVICE_INTERVAL_MS: i64 = protocol::PATH_KEEPALIVE_INTERVAL; @@ -26,27 +24,22 @@ pub(crate) enum PathServiceResult { /// These are maintained in Node and canonicalized so that all unique paths have /// one and only one unique path object. That enables statistics to be tracked /// for them and uniform application of things like keepalives. -pub struct Path { +pub struct Path { pub endpoint: Endpoint, - local_socket: Thing<64>, - local_interface: Thing<64>, + pub local_socket: LocalSocket, + pub local_interface: LocalInterface, last_send_time_ticks: AtomicI64, last_receive_time_ticks: AtomicI64, create_time_ticks: i64, fragmented_packets: Mutex>, } -impl Path { - pub(crate) fn new( - endpoint: Endpoint, - local_socket: Application::LocalSocket, - local_interface: Application::LocalInterface, - time_ticks: i64, - ) -> Self { +impl Path { + pub(crate) fn new(endpoint: Endpoint, local_socket: LocalSocket, local_interface: LocalInterface, time_ticks: i64) -> Self { Self { endpoint, - local_socket: Thing::new(local_socket), // enlarge Thing<> if this panics - local_interface: Thing::new(local_interface), // enlarge Thing<> if this panics + local_socket, + local_interface, last_send_time_ticks: AtomicI64::new(NEVER_HAPPENED_TICKS), last_receive_time_ticks: AtomicI64::new(NEVER_HAPPENED_TICKS), create_time_ticks: time_ticks, @@ -54,16 +47,6 @@ impl Path { } } - #[inline(always)] - pub(crate) fn local_socket(&self) -> &Application::LocalSocket { - self.local_socket.get() - } - - #[inline(always)] - pub(crate) fn local_interface(&self) -> &Application::LocalInterface { - self.local_interface.get() - } - /// Receive a fragment and return a FragmentedPacket if the entire packet was assembled. /// This returns None if more fragments are needed to assemble the packet. pub(crate) fn v1_proto_receive_fragment( diff --git a/network-hypervisor/src/vl1/peer.rs b/network-hypervisor/src/vl1/peer.rs index 1b2bc3e86..bb1fb42d0 100644 --- a/network-hypervisor/src/vl1/peer.rs +++ b/network-hypervisor/src/vl1/peer.rs @@ -24,11 +24,11 @@ use crate::{VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION}; pub(crate) const SERVICE_INTERVAL_MS: i64 = 10000; -pub struct Peer { +pub struct Peer { pub identity: Valid, v1_proto_static_secret: v1::SymmetricSecret, - paths: Mutex>, + paths: Mutex>>, pub(crate) last_send_time_ticks: AtomicI64, pub(crate) last_receive_time_ticks: AtomicI64, @@ -41,8 +41,8 @@ pub struct Peer { remote_node_info: RwLock, } -struct PeerPath { - path: Weak, +struct PeerPath { + path: Weak>, last_receive_time_ticks: i64, } @@ -53,11 +53,11 @@ struct RemoteNodeInfo { } /// Sort a list of paths by quality or priority, with best paths first. -fn prioritize_paths(paths: &mut Vec) { +fn prioritize_paths(paths: &mut Vec>) { paths.sort_unstable_by(|a, b| a.last_receive_time_ticks.cmp(&b.last_receive_time_ticks).reverse()); } -impl Peer { +impl Peer { /// Create a new peer. /// /// This only returns None if this_node_identity does not have its secrets or if some @@ -113,7 +113,7 @@ impl Peer { /// Get current best path or None if there are no direct paths to this peer. #[inline] - pub fn direct_path(&self) -> Option> { + pub fn direct_path(&self) -> Option>> { for p in self.paths.lock().unwrap().iter() { let pp = p.path.upgrade(); if pp.is_some() { @@ -125,7 +125,7 @@ impl Peer { /// Get either the current best direct path or an indirect path via e.g. a root. #[inline] - pub fn path(&self, node: &Node) -> Option> { + pub fn path(&self, node: &Node) -> Option>> { let direct_path = self.direct_path(); if direct_path.is_some() { return direct_path; @@ -136,7 +136,7 @@ impl Peer { return None; } - fn learn_path(&self, app: &Application, new_path: &Arc, time_ticks: i64) { + fn learn_path(&self, app: &Application, new_path: &Arc>, time_ticks: i64) { let mut paths = self.paths.lock().unwrap(); // TODO: check path filter @@ -202,7 +202,7 @@ impl Peer { } /// Called every SERVICE_INTERVAL_MS by the background service loop in Node. - pub(crate) fn service(&self, _: &Application, _: &Node, time_ticks: i64) -> bool { + pub(crate) fn service(&self, _: &Application, _: &Node, time_ticks: i64) -> bool { // Prune dead paths and sort in descending order of quality. { let mut paths = self.paths.lock().unwrap(); @@ -223,7 +223,7 @@ impl Peer { } /// Send a prepared and encrypted packet using the V1 protocol with fragmentation if needed. - fn v1_proto_internal_send( + fn v1_proto_internal_send( &self, app: &Application, endpoint: &Endpoint, @@ -281,11 +281,11 @@ impl Peer { /// The builder function must append the verb (with any verb flags) and packet payload. If it returns /// an error, the error is returned immediately and the send is aborted. None is returned if the send /// function itself fails for some reason such as no paths being available. - pub fn send Result>( + pub fn send Result>( &self, app: &Application, - node: &Node, - path: Option<&Arc>, + node: &Node, + path: Option<&Arc>>, time_ticks: i64, builder_function: BuilderFunction, ) -> Option> { @@ -369,8 +369,8 @@ impl Peer { self.v1_proto_internal_send( app, &path.endpoint, - Some(path.local_socket::()), - Some(path.local_interface::()), + Some(&path.local_socket), + Some(&path.local_interface), max_fragment_size, packet, ); @@ -385,16 +385,7 @@ impl Peer { /// /// If explicit_endpoint is not None the packet will be sent directly to this endpoint. /// Otherwise it will be sent via the best direct or indirect path known. - /// - /// Unlike other messages HELLO is sent partially in the clear and always with the long-lived - /// static identity key. Authentication in old versions is via Poly1305 and in new versions - /// via HMAC-SHA512. - pub(crate) fn send_hello( - &self, - app: &Application, - node: &Node, - explicit_endpoint: Option<&Endpoint>, - ) -> bool { + pub(crate) fn send_hello(&self, app: &Application, node: &Node, explicit_endpoint: Option<&Endpoint>) -> bool { let mut path = None; let destination = if let Some(explicit_endpoint) = explicit_endpoint { explicit_endpoint @@ -420,7 +411,7 @@ impl Peer { f.0.dest = self.identity.address.to_bytes(); f.0.src = node.identity.address.to_bytes(); f.0.flags_cipher_hops = v1::CIPHER_NOCRYPT_POLY1305; - f.1.verb = message_type::VL1_HELLO | v1::VERB_FLAG_EXTENDED_AUTHENTICATION; + f.1.verb = message_type::VL1_HELLO; f.1.version_proto = PROTOCOL_VERSION; f.1.version_major = VERSION_MAJOR; f.1.version_minor = VERSION_MINOR; @@ -454,8 +445,8 @@ impl Peer { self.v1_proto_internal_send( app, destination, - Some(p.local_socket::()), - Some(p.local_interface::()), + Some(&p.local_socket), + Some(&p.local_interface), max_fragment_size, packet, ); @@ -473,13 +464,13 @@ impl Peer { /// those fragments after the main packet header and first chunk. /// /// This returns true if the packet decrypted and passed authentication. - pub(crate) fn v1_proto_receive( + pub(crate) fn v1_proto_receive( self: &Arc, - node: &Node, + node: &Node, app: &Application, inner: &Inner, time_ticks: i64, - source_path: &Arc, + source_path: &Arc>, packet_header: &v1::PacketHeader, frag0: &PacketBuffer, fragments: &[Option], @@ -503,7 +494,7 @@ impl Peer { }; if let Ok(mut verb) = payload.u8_at(0) { - if (verb & v1::VERB_FLAG_COMPRESSED) != 0 { + if (verb & MESSAGE_FLAG_COMPRESSED) != 0 { let mut decompressed_payload = [0u8; v1::SIZE_MAX]; decompressed_payload[0] = verb; if let Ok(dlen) = lz4_flex::block::decompress_into(&payload.as_bytes()[1..], &mut decompressed_payload[1..]) { @@ -528,7 +519,7 @@ impl Peer { } } - verb &= v1::VERB_MASK; // mask off flags + verb &= MESSAGE_TYPE_MASK; // mask off flags debug_event!( app, "[vl1] #{:0>16x} decrypted and authenticated, verb: {} ({:0>2x})", @@ -539,7 +530,7 @@ impl Peer { return match verb { message_type::VL1_NOP => PacketHandlerResult::Ok, - message_type::VL1_HELLO => self.handle_incoming_hello(app, inner, node, time_ticks, message_id, source_path, &payload), + message_type::VL1_HELLO => self.handle_incoming_hello(app, node, time_ticks, message_id, source_path, &payload), message_type::VL1_ERROR => { self.handle_incoming_error(app, inner, node, time_ticks, source_path, packet_header.hops(), message_id, &payload) } @@ -554,9 +545,9 @@ impl Peer { path_is_known, &payload, ), - message_type::VL1_WHOIS => self.handle_incoming_whois(app, inner, node, time_ticks, message_id, &payload), + message_type::VL1_WHOIS => self.handle_incoming_whois(app, node, time_ticks, message_id, &payload), message_type::VL1_RENDEZVOUS => self.handle_incoming_rendezvous(app, node, time_ticks, message_id, source_path, &payload), - message_type::VL1_ECHO => self.handle_incoming_echo(app, inner, node, time_ticks, message_id, &payload), + message_type::VL1_ECHO => self.handle_incoming_echo(app, node, time_ticks, message_id, &payload), message_type::VL1_PUSH_DIRECT_PATHS => self.handle_incoming_push_direct_paths(app, node, time_ticks, source_path, &payload), message_type::VL1_USER_MESSAGE => self.handle_incoming_user_message(app, node, time_ticks, source_path, &payload), _ => inner.handle_packet(app, node, self, &source_path, packet_header.hops(), message_id, verb, &payload, 1), @@ -567,17 +558,16 @@ impl Peer { return PacketHandlerResult::Error; } - fn handle_incoming_hello( + fn handle_incoming_hello( &self, app: &Application, - inner: &Inner, - node: &Node, + node: &Node, time_ticks: i64, message_id: MessageId, - source_path: &Arc, + source_path: &Arc>, payload: &PacketBuffer, ) -> PacketHandlerResult { - if !(inner.should_respond_to(&self.identity) || node.this_node_is_root() || node.is_peer_root(self)) { + if !(app.should_respond_to(&self.identity) || node.this_node_is_root() || node.is_peer_root(self)) { debug_event!( app, "[vl1] dropping HELLO from {} due to lack of trust relationship", @@ -621,13 +611,13 @@ impl Peer { return PacketHandlerResult::Error; } - fn handle_incoming_error( + fn handle_incoming_error( self: &Arc, app: &Application, inner: &Inner, - node: &Node, + node: &Node, _time_ticks: i64, - source_path: &Arc, + source_path: &Arc>, source_hops: u8, message_id: u64, payload: &PacketBuffer, @@ -659,13 +649,13 @@ impl Peer { return PacketHandlerResult::Error; } - fn handle_incoming_ok( + fn handle_incoming_ok( self: &Arc, app: &Application, inner: &Inner, - node: &Node, + node: &Node, time_ticks: i64, - source_path: &Arc, + source_path: &Arc>, source_hops: u8, message_id: u64, path_is_known: bool, @@ -761,16 +751,15 @@ impl Peer { return PacketHandlerResult::Error; } - fn handle_incoming_whois( + fn handle_incoming_whois( self: &Arc, app: &Application, - inner: &Inner, - node: &Node, + node: &Node, time_ticks: i64, _message_id: MessageId, payload: &PacketBuffer, ) -> PacketHandlerResult { - if node.this_node_is_root() || inner.should_respond_to(&self.identity) { + if node.this_node_is_root() || app.should_respond_to(&self.identity) { let mut addresses = payload.as_bytes(); while addresses.len() >= ADDRESS_SIZE { if !self @@ -794,29 +783,28 @@ impl Peer { return PacketHandlerResult::Ok; } - fn handle_incoming_rendezvous( + fn handle_incoming_rendezvous( self: &Arc, _app: &Application, - node: &Node, + node: &Node, _time_ticks: i64, _message_id: MessageId, - _source_path: &Arc, + _source_path: &Arc>, _payload: &PacketBuffer, ) -> PacketHandlerResult { if node.is_peer_root(self) {} return PacketHandlerResult::Ok; } - fn handle_incoming_echo( + fn handle_incoming_echo( &self, app: &Application, - inner: &Inner, - node: &Node, + node: &Node, time_ticks: i64, message_id: MessageId, payload: &PacketBuffer, ) -> PacketHandlerResult { - if inner.should_respond_to(&self.identity) || node.is_peer_root(self) { + if app.should_respond_to(&self.identity) || node.is_peer_root(self) { self.send(app, node, None, time_ticks, |packet| { let mut f: &mut OkHeader = packet.append_struct_get_mut().unwrap(); f.verb = message_type::VL1_OK; @@ -834,44 +822,44 @@ impl Peer { return PacketHandlerResult::Ok; } - fn handle_incoming_push_direct_paths( + fn handle_incoming_push_direct_paths( self: &Arc, _app: &Application, - _node: &Node, + _node: &Node, _time_ticks: i64, - _source_path: &Arc, + _source_path: &Arc>, _payload: &PacketBuffer, ) -> PacketHandlerResult { PacketHandlerResult::Ok } - fn handle_incoming_user_message( + fn handle_incoming_user_message( self: &Arc, _app: &Application, - _node: &Node, + _node: &Node, _time_ticks: i64, - _source_path: &Arc, + _source_path: &Arc>, _payload: &PacketBuffer, ) -> PacketHandlerResult { PacketHandlerResult::Ok } } -impl Hash for Peer { +impl Hash for Peer { #[inline(always)] fn hash(&self, state: &mut H) { state.write_u64(self.identity.address.into()); } } -impl PartialEq for Peer { +impl PartialEq for Peer { #[inline(always)] fn eq(&self, other: &Self) -> bool { self.identity.fingerprint.eq(&other.identity.fingerprint) } } -impl Eq for Peer {} +impl Eq for Peer {} fn v1_proto_try_aead_decrypt( secret: &v1::SymmetricSecret, @@ -899,7 +887,7 @@ fn v1_proto_try_aead_decrypt( if cipher == v1::CIPHER_SALSA2012_POLY1305 { salsa.crypt_in_place(payload.as_bytes_mut()); Some(message_id) - } else if (payload.u8_at(0).unwrap_or(0) & v1::VERB_MASK) == message_type::VL1_HELLO { + } else if (payload.u8_at(0).unwrap_or(0) & MESSAGE_TYPE_MASK) == message_type::VL1_HELLO { Some(message_id) } else { // SECURITY: fail if there is no encryption and the message is not HELLO. No other types are allowed diff --git a/network-hypervisor/src/vl2/mod.rs b/network-hypervisor/src/vl2/mod.rs index 672ae41b6..7fa53b5bc 100644 --- a/network-hypervisor/src/vl2/mod.rs +++ b/network-hypervisor/src/vl2/mod.rs @@ -3,6 +3,7 @@ mod iproute; mod multicastgroup; mod networkid; +mod scattergather; mod switch; mod topology; diff --git a/network-hypervisor/src/vl2/multicastauthority.rs b/network-hypervisor/src/vl2/multicastauthority.rs index c253ca4cc..d81f40090 100644 --- a/network-hypervisor/src/vl2/multicastauthority.rs +++ b/network-hypervisor/src/vl2/multicastauthority.rs @@ -3,19 +3,13 @@ use std::sync::{Arc, Mutex, RwLock}; use crate::protocol; use crate::protocol::PacketBuffer; -use crate::vl1::{Address, ApplicationLayer, Identity, Node, PacketHandlerResult, Peer, MAC}; +use crate::vl1::*; use crate::vl2::{MulticastGroup, NetworkId}; use zerotier_utils::buffer::OutOfBoundsError; use zerotier_utils::sync::RMaybeWLockGuard; /// Handler implementations for VL2_MULTICAST_LIKE and VL2_MULTICAST_GATHER. -/// -/// Both controllers and roots will want to handle these, with the latter supporting them for legacy -/// reasons only. Regular nodes may also want to handle them in the future. So, break this out to allow -/// easy code reuse. To integrate call the appropriate method when the appropriate message type is -/// received and pass in a function to check whether specific network/identity combinations should be -/// processed. The GATHER implementation will send reply packets to the source peer. pub struct MulticastAuthority { subscriptions: RwLock>>>, } @@ -47,11 +41,11 @@ impl MulticastAuthority { } /// Call for VL2_MULTICAST_LIKE packets. - pub fn handle_vl2_multicast_like bool>( + pub fn handle_vl2_multicast_like bool>( &self, auth: Authenticator, time_ticks: i64, - source: &Arc, + source: &Arc>, payload: &PacketBuffer, mut cursor: usize, ) -> PacketHandlerResult { @@ -84,13 +78,13 @@ impl MulticastAuthority { } /// Call for VL2_MULTICAST_GATHER packets. - pub fn handle_vl2_multicast_gather bool>( + pub fn handle_vl2_multicast_gather bool>( &self, auth: Authenticator, time_ticks: i64, - host_system: &HostSystemImpl, - node: &Node, - source: &Arc, + app: &Application, + node: &Node, + source: &Arc>, message_id: u64, payload: &PacketBuffer, mut cursor: usize, @@ -114,7 +108,7 @@ impl MulticastAuthority { } while !gathered.is_empty() { - source.send(host_system, node, None, time_ticks, |packet| -> Result<(), OutOfBoundsError> { + source.send(app, node, None, time_ticks, |packet| -> Result<(), OutOfBoundsError> { let ok_header = packet.append_struct_get_mut::()?; ok_header.verb = protocol::message_type::VL1_OK; ok_header.in_re_verb = protocol::message_type::VL2_MULTICAST_GATHER; diff --git a/network-hypervisor/src/vl2/scattergather.rs b/network-hypervisor/src/vl2/scattergather.rs new file mode 100644 index 000000000..a4a46637f --- /dev/null +++ b/network-hypervisor/src/vl2/scattergather.rs @@ -0,0 +1,234 @@ +use std::io::Write; + +use fastcdc::v2020; + +use zerotier_crypto::hash::{SHA384, SHA384_HASH_SIZE}; +use zerotier_utils::error::{InvalidFormatError, InvalidParameterError}; +use zerotier_utils::memory::byte_array_chunks_exact; + +const MAX_RECURSION_DEPTH: u8 = 64; // sanity limit, object would have to be quite huge to hit this + +/// Re-assembler for scattered objects. +pub struct ScatteredObject { + data_chunks: Vec>, + need: Vec, +} + +impl ScatteredObject { + /// Create a new assembler to gather an object given its root hash list. + pub fn init(hash_list: Vec) -> Self { + Self { data_chunks: Vec::new(), need: hash_list } + } + + fn gather_recursive Option>>( + hl: &[u8], + new_hl: &mut Vec, + get_chunk: &mut GetChunk, + have_all_data_chunk_hashes: &mut bool, + depth: u8, + ) -> Result<(), InvalidFormatError> { + if (hl.len() % SHA384_HASH_SIZE) != 0 || hl.is_empty() { + return Err(InvalidFormatError); + } + for h in byte_array_chunks_exact::(hl) { + if (h[SHA384_HASH_SIZE - 1] & 0x01) != 0 { + if let Some(chunk) = get_chunk(h) { + if depth < MAX_RECURSION_DEPTH { + Self::gather_recursive(chunk.as_slice(), new_hl, get_chunk, have_all_data_chunk_hashes, depth + 1)?; + continue; + } else { + return Err(InvalidFormatError); + } + } + *have_all_data_chunk_hashes = false; + } + let _ = new_hl.write_all(h); + } + return Ok(()); + } + + /// Try to assemble this object using the supplied function to request chunks we don't have. + /// + /// Once all chunks are retrieved this will return Ok(Some(object)). A return of Ok(None) means there are + /// still missing chunks that couldn't be resolved with the supplied getter. In that case this should be + /// called again once more chunks are fetched. Use need() to get an iterator over chunks that are still + /// outstanding. + /// + /// Once a result is returned this assembler object should be discarded. An error return indicates an + /// invalid object due to either invalid chunk data or too many recursions. + pub fn gather Option>>( + &mut self, + mut get_chunk: GetChunk, + ) -> Result>, InvalidFormatError> { + // First attempt to resolve all chunks of hashes until we have a complete in-order list of all data chunks. + let mut new_need = Vec::with_capacity(self.need.len()); + let mut have_all_data_chunk_hashes = true; // set to false if we still need to get chunks of hashes + Self::gather_recursive(self.need.as_slice(), &mut new_need, &mut get_chunk, &mut have_all_data_chunk_hashes, 0)?; + std::mem::swap(&mut self.need, &mut new_need); + + // Once we have all data chunks, resolve those until the entire object is re-assembled. + if have_all_data_chunk_hashes { + self.data_chunks.resize(self.need.len() / SHA384_HASH_SIZE, Vec::new()); + + let mut chunk_no = 0; + let mut missing_chunks = false; + for h in byte_array_chunks_exact::(self.need.as_slice()) { + let dc = self.data_chunks.get_mut(chunk_no).unwrap(); + if dc.is_empty() { + debug_assert_eq!(h.len(), SHA384_HASH_SIZE); + if let Some(chunk) = get_chunk(h) { + if !chunk.is_empty() { + *dc = chunk; + } else { + return Err(InvalidFormatError); + } + } else { + missing_chunks = true; + } + } + chunk_no += 1; + } + + if !missing_chunks { + let mut obj_size = 0; + for dc in self.data_chunks.iter() { + obj_size += dc.len(); + } + let mut obj = Vec::with_capacity(obj_size); + for dc in self.data_chunks.iter() { + let _ = obj.write_all(dc.as_slice()); + } + return Ok(Some(obj)); + } + } + + return Ok(None); + } + + /// Get an iterator of hashes currently known to be needed to reassemble this object. + /// + /// This list can get longer through the course of object retrival since incoming chunks can + /// be chunks of hashes instead of chunks of data. + pub fn need(&self) -> impl Iterator { + byte_array_chunks_exact::(self.need.as_slice()) + } +} + +/// Decompose an object into a series of chunks identified by SHA384 hashes. +/// +/// This splits the supplied binary object into chunks using the FastCDC2020 content defined chunking +/// algorithm. For each chunk a SHA384 hash is computed and added to a hash list. If the resulting +/// hash list is larger than max_chunk_size it is recurisvely chunked into chunks of hashes. Chunking +/// of the hash list is done deterministically rather than using FastCDC since the record size in these +/// chunks is always a multiple of the hash size. +/// +/// The supplied function is called to output each chunk except for the root hash list, which is +/// returned. It's technically possible for the same chunk to be output more than once if there are +/// long runs of identical data in the supplied object. In this case it need only be stored once. +/// +/// * `obj` - Blob to decompose +/// * `max_chunk_size` - Maximum size of any chunk including root hash list (minimum allowed: 256) +/// * `store_chunk` - Function that is called to store each chunk other than the root hash list +pub fn scatter( + obj: &[u8], + max_chunk_size: u32, + mut store_chunk: F, +) -> Result, InvalidParameterError> { + if max_chunk_size < 512 { + return Err(InvalidParameterError("max chunk size must be >= 512")); + } + let mut root_hash_list = Vec::with_capacity(max_chunk_size as usize); + + for chunk in v2020::FastCDC::new(obj, (max_chunk_size / 4).max(v2020::MINIMUM_MIN), max_chunk_size / 2, max_chunk_size) { + let chunk = &obj[chunk.offset..chunk.offset + chunk.length]; + let mut chunk_hash = SHA384::hash(chunk); + chunk_hash[SHA384_HASH_SIZE - 1] &= 0xfe; // chunk of data + let _ = root_hash_list.write_all(&chunk_hash); + store_chunk(chunk_hash, chunk); + } + + if root_hash_list.len() > (max_chunk_size as usize) { + let max_hashes_per_chunk = ((max_chunk_size / (SHA384_HASH_SIZE as u32)) * (SHA384_HASH_SIZE as u32)) as usize; + let mut new_root_hash_list = Vec::with_capacity(max_chunk_size as usize); + let mut recursion_depth = 0; + loop { + let mut r = root_hash_list.as_slice(); + while !r.is_empty() { + debug_assert_eq!(new_root_hash_list.len() % SHA384_HASH_SIZE, 0); + debug_assert_eq!(r.len() % SHA384_HASH_SIZE, 0); + if (new_root_hash_list.len() + r.len()) <= (max_chunk_size as usize) { + let _ = new_root_hash_list.write_all(r); + break; + } else { + let clen = r.len().min(max_hashes_per_chunk); + let chunk = &r[..clen]; + r = &r[clen..]; + + let mut chunk_hash = SHA384::hash(chunk); + chunk_hash[SHA384_HASH_SIZE - 1] |= 0x01; // chunk of hashes + let _ = new_root_hash_list.write_all(&chunk_hash); + store_chunk(chunk_hash, chunk); + } + } + std::mem::swap(&mut root_hash_list, &mut new_root_hash_list); + + if root_hash_list.len() <= (max_chunk_size as usize) { + break; + } else { + new_root_hash_list.clear(); + if recursion_depth >= MAX_RECURSION_DEPTH { + return Err(InvalidParameterError("max recursion depth exceeded")); + } + recursion_depth += 1; + } + } + } + + return Ok(root_hash_list); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn scatter_gather_random_blobs() { + let mut random_data = Vec::new(); + random_data.resize(1024 * 1024 * 8, 0); + zerotier_crypto::random::fill_bytes_secure(random_data.as_mut()); + + let mut chunks = HashMap::new(); + for _ in 0..4 { + chunks.clear(); + let test_blob = ((zerotier_crypto::random::xorshift64_random() as usize) % (random_data.len() - 1)) + 1; + let test_blob = &random_data.as_slice()[..test_blob]; + + let root_hash_list = scatter(test_blob, 1024, |k, v| { + //println!("{}", hex::to_string(&k)); + chunks.insert(k, v.to_vec()); + }) + .unwrap(); + + let mut assembler = ScatteredObject::init(root_hash_list); + let mut gathered_blob; + loop { + gathered_blob = assembler + .gather(|c| { + if zerotier_crypto::random::xorshift64_random() < (u64::MAX / 8) { + None + } else { + chunks.get(c).cloned() + } + }) + .unwrap(); + if gathered_blob.is_some() { + break; + } + } + let gathered_blob = gathered_blob.unwrap(); + + assert!(gathered_blob.eq(test_blob)); + } + } +} diff --git a/network-hypervisor/src/vl2/switch.rs b/network-hypervisor/src/vl2/switch.rs index 8b66ee4c2..1dd807421 100644 --- a/network-hypervisor/src/vl2/switch.rs +++ b/network-hypervisor/src/vl2/switch.rs @@ -11,20 +11,12 @@ pub struct Switch {} #[allow(unused_variables)] impl InnerProtocolLayer for Switch { - fn should_respond_to(&self, id: &zerotier_crypto::typestate::Valid) -> bool { - true - } - - fn has_trust_relationship(&self, id: &zerotier_crypto::typestate::Valid) -> bool { - true - } - fn handle_packet( &self, app: &Application, - node: &Node, - source: &Arc, - source_path: &Arc, + node: &Node, + source: &Arc>, + source_path: &Arc>, source_hops: u8, message_id: u64, verb: u8, @@ -37,9 +29,9 @@ impl InnerProtocolLayer for Switch { fn handle_error( &self, app: &Application, - node: &Node, - source: &Arc, - source_path: &Arc, + node: &Node, + source: &Arc>, + source_path: &Arc>, source_hops: u8, message_id: u64, in_re_verb: u8, @@ -54,9 +46,9 @@ impl InnerProtocolLayer for Switch { fn handle_ok( &self, app: &Application, - node: &Node, - source: &Arc, - source_path: &Arc, + node: &Node, + source: &Arc>, + source_path: &Arc>, source_hops: u8, message_id: u64, in_re_verb: u8, diff --git a/network-hypervisor/src/vl2/topology.rs b/network-hypervisor/src/vl2/topology.rs index 3defd1233..4b34782e7 100644 --- a/network-hypervisor/src/vl2/topology.rs +++ b/network-hypervisor/src/vl2/topology.rs @@ -20,6 +20,11 @@ pub struct Member<'a> { pub name: Cow<'a, str>, } +#[allow(unused)] +pub mod member_flag { + pub const BRIDGING_ALLOWED: u64 = 0x0001; +} + #[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] pub struct Topology<'a> { pub timestamp: i64, diff --git a/utils/src/base64.rs b/utils/src/base64.rs new file mode 100644 index 000000000..84cf8d310 --- /dev/null +++ b/utils/src/base64.rs @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +const BASE64_URL_SAFE_NO_PAD_ENGINE: base64::engine::fast_portable::FastPortable = + base64::engine::fast_portable::FastPortable::from(&base64::alphabet::URL_SAFE, base64::engine::fast_portable::NO_PAD); + +/// Encode base64 using URL-safe alphabet and no padding. +pub fn encode_url_nopad(bytes: &[u8]) -> String { + base64::encode_engine(bytes, &BASE64_URL_SAFE_NO_PAD_ENGINE) +} + +/// Decode base64 using URL-safe alphabet and no padding, or None on error. +pub fn decode_url_nopad(b64: &str) -> Option> { + base64::decode_engine(b64, &BASE64_URL_SAFE_NO_PAD_ENGINE).ok() +} diff --git a/utils/src/blob.rs b/utils/src/blob.rs index 660b35947..85ce65e21 100644 --- a/utils/src/blob.rs +++ b/utils/src/blob.rs @@ -14,7 +14,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::hex; -/// Fixed size byte array with Serde serializer/deserializer for sizes over 32 elements and hex to_string(). +/// Fixed size Serde serializable byte array. +/// This makes it easier to deal with blobs larger than 32 bytes (due to serde array limitations) #[repr(transparent)] #[derive(Clone, Eq, PartialEq)] pub struct Blob([u8; L]); diff --git a/utils/src/buffer.rs b/utils/src/buffer.rs index cfffef323..4c148bcfa 100644 --- a/utils/src/buffer.rs +++ b/utils/src/buffer.rs @@ -27,7 +27,6 @@ impl Display for OutOfBoundsError { } impl Debug for OutOfBoundsError { - #[inline(always)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } diff --git a/utils/src/cast.rs b/utils/src/cast.rs new file mode 100644 index 000000000..38406aafd --- /dev/null +++ b/utils/src/cast.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +use std::any::TypeId; +use std::mem::size_of; + +/// Returns true if two types are in fact the same type. +#[inline(always)] +pub fn same_type() -> bool { + TypeId::of::() == TypeId::of::() && size_of::() == size_of::() +} + +/// Cast a reference if the types are equal, such as from a specific type to a generic that it implements. +#[inline(always)] +pub fn cast_ref(u: &U) -> Option<&V> { + if same_type::() { + Some(unsafe { std::mem::transmute::<&U, &V>(u) }) + } else { + None + } +} + +/// Cast a reference if the types are equal, such as from a specific type to a generic that it implements. +#[inline(always)] +pub fn cast_mut(u: &mut U) -> Option<&mut V> { + if same_type::() { + Some(unsafe { std::mem::transmute::<&mut U, &mut V>(u) }) + } else { + None + } +} diff --git a/utils/src/gate.rs b/utils/src/gate.rs index 037edcc26..e3a8aa6bb 100644 --- a/utils/src/gate.rs +++ b/utils/src/gate.rs @@ -33,36 +33,3 @@ impl IntervalGate { } } } - -/* -/// Boolean rate limiter with atomic semantics. -#[repr(transparent)] -pub struct AtomicIntervalGate(AtomicI64); - -impl Default for AtomicIntervalGate { - #[inline(always)] - fn default() -> Self { - Self(AtomicI64::new(crate::util::NEVER_HAPPENED_TICKS)) - } -} - -impl AtomicIntervalGate { - #[inline(always)] - #[allow(unused)] - pub fn new(initial_ts: i64) -> Self { - Self(AtomicI64::new(initial_ts)) - } - - #[inline(always)] - #[allow(unused)] - pub fn gate(&self, mut time: i64) -> bool { - let prev_time = self.0.load(Ordering::Acquire); - if (time - prev_time) < FREQ { - false - } else { - self.0.store(time, Ordering::Release); - true - } - } -} -*/ diff --git a/utils/src/gatherarray.rs b/utils/src/gatherarray.rs deleted file mode 100644 index b0b0a3539..000000000 --- a/utils/src/gatherarray.rs +++ /dev/null @@ -1,102 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - -use std::mem::{size_of, MaybeUninit}; -use std::ptr::copy_nonoverlapping; - -use crate::arrayvec::ArrayVec; - -/// A fixed sized array of items to be gathered with fast check logic to return when complete. -/// -/// This supports a maximum capacity of 64 and will panic if created with a larger value for C. -pub struct GatherArray { - a: [MaybeUninit; C], - have_bits: u64, - have_count: u8, - goal: u8, -} - -impl GatherArray { - /// Create a new gather array, which must be initialized prior to use. - #[inline(always)] - pub fn new(goal: u8) -> Self { - assert!(C <= 64); - assert!(goal <= (C as u8)); - assert_eq!(size_of::<[T; C]>(), size_of::<[MaybeUninit; C]>()); - Self { - a: unsafe { MaybeUninit::uninit().assume_init() }, - have_bits: 0, - have_count: 0, - goal, - } - } - - /// Add an item to the array if we don't have this index anymore, returning complete array if all parts are here. - #[inline(always)] - pub fn add_return_when_satisfied(&mut self, index: u8, value: T) -> Option> { - if index < self.goal { - let mut have = self.have_bits; - let got = 1u64.wrapping_shl(index as u32); - if (have & got) == 0 { - have |= got; - self.have_bits = have; - let count = self.have_count + 1; - self.have_count = count; - let goal = self.goal as usize; - unsafe { - self.a.get_unchecked_mut(index as usize).write(value); - if (self.have_count as usize) == goal { - debug_assert_eq!(0xffffffffffffffffu64.wrapping_shr(64 - goal as u32), have); - let mut tmp = ArrayVec::new(); - copy_nonoverlapping( - self.a.as_ptr().cast::(), - tmp.a.as_mut_ptr().cast::(), - size_of::>() * goal, - ); - tmp.s = goal; - self.goal = 0; - return Some(tmp); - } - } - } - } - return None; - } -} - -impl Drop for GatherArray { - #[inline] - fn drop(&mut self) { - let have = self.have_bits; - for i in 0..self.goal { - if (have & 1u64.wrapping_shl(i as u32)) != 0 { - unsafe { self.a.get_unchecked_mut(i as usize).assume_init_drop() }; - } - } - self.goal = 0; - } -} - -#[cfg(test)] -mod tests { - use super::GatherArray; - - #[test] - fn gather_array() { - for goal in 2u8..64u8 { - let mut m = GatherArray::::new(goal); - for x in 0..(goal - 1) { - assert!(m.add_return_when_satisfied(x, x).is_none()); - } - let r = m.add_return_when_satisfied(goal - 1, goal - 1).unwrap(); - for x in 0..goal { - assert_eq!(r.as_ref()[x as usize], x); - } - } - } -} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index cb1e278e9..194f7af4e 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -7,8 +7,10 @@ */ pub mod arrayvec; +pub mod base64; pub mod blob; pub mod buffer; +pub mod cast; pub mod defer; pub mod dictionary; pub mod error; @@ -16,7 +18,6 @@ pub mod error; pub mod exitcode; pub mod flatsortedmap; pub mod gate; -pub mod gatherarray; pub mod hex; pub mod io; pub mod json; @@ -26,9 +27,7 @@ pub mod pool; #[cfg(feature = "tokio")] pub mod reaper; pub mod ringbuffer; -pub mod ringbuffermap; pub mod sync; -pub mod thing; pub mod varint; #[cfg(feature = "tokio")] @@ -38,20 +37,7 @@ pub use tokio; pub use futures_util; /// Initial value that should be used for monotonic tick time variables. -pub const NEVER_HAPPENED_TICKS: i64 = 0; - -const BASE64_URL_SAFE_NO_PAD_ENGINE: base64::engine::fast_portable::FastPortable = - base64::engine::fast_portable::FastPortable::from(&base64::alphabet::URL_SAFE, base64::engine::fast_portable::NO_PAD); - -/// Encode base64 using URL-safe alphabet and no padding. -pub fn base64_encode_url_nopad(bytes: &[u8]) -> String { - base64::encode_engine(bytes, &BASE64_URL_SAFE_NO_PAD_ENGINE) -} - -/// Decode base64 using URL-safe alphabet and no padding, or None on error. -pub fn base64_decode_url_nopad(b64: &str) -> Option> { - base64::decode_engine(b64, &BASE64_URL_SAFE_NO_PAD_ENGINE).ok() -} +pub const NEVER_HAPPENED_TICKS: i64 = i64::MIN; /// Get milliseconds since unix epoch. #[inline] diff --git a/utils/src/memory.rs b/utils/src/memory.rs index 9710a9eb8..c66e7a6ae 100644 --- a/utils/src/memory.rs +++ b/utils/src/memory.rs @@ -6,6 +6,9 @@ * https://www.zerotier.com/ */ +// This is a collection of functions that use "unsafe" to do things with memory that should in fact +// be safe. Some of these may eventually get stable standard library replacements. + #[allow(unused_imports)] use std::mem::{needs_drop, size_of, MaybeUninit}; @@ -56,6 +59,23 @@ pub fn load_raw(src: &[u8]) -> T { } } +/// Our version of the not-yet-stable array_chunks method in slice, but only for byte arrays. +#[inline(always)] +pub fn byte_array_chunks_exact(a: &[u8]) -> impl Iterator { + let mut i = 0; + let l = a.len(); + std::iter::from_fn(move || { + let j = i + S; + if j <= l { + let next = unsafe { &*a.as_ptr().add(i).cast() }; + i = j; + Some(next) + } else { + None + } + }) +} + /// Obtain a view into an array cast as another array. /// This will panic if the template parameters would result in out of bounds access. #[inline(always)] diff --git a/utils/src/ringbuffermap.rs b/utils/src/ringbuffermap.rs deleted file mode 100644 index c438bb642..000000000 --- a/utils/src/ringbuffermap.rs +++ /dev/null @@ -1,265 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * (c) ZeroTier, Inc. - * https://www.zerotier.com/ - */ - -use std::hash::{Hash, Hasher}; -use std::mem::MaybeUninit; - -const EMPTY: u16 = 0xffff; - -#[inline(always)] -fn xorshift64(mut x: u64) -> u64 { - x ^= x.wrapping_shl(13); - x ^= x.wrapping_shr(7); - x ^= x.wrapping_shl(17); - x -} - -struct XorShiftHasher(u64); - -impl XorShiftHasher { - #[inline(always)] - fn new(salt: u32) -> Self { - Self(salt as u64) - } -} - -impl Hasher for XorShiftHasher { - #[inline(always)] - fn finish(&self) -> u64 { - self.0 - } - - #[inline(always)] - fn write(&mut self, mut bytes: &[u8]) { - let mut x = self.0; - while bytes.len() >= 8 { - x = xorshift64(x.wrapping_add(u64::from_ne_bytes(unsafe { *bytes.as_ptr().cast::<[u8; 8]>() }))); - bytes = &bytes[8..]; - } - while bytes.len() >= 4 { - x = xorshift64(x.wrapping_add(u32::from_ne_bytes(unsafe { *bytes.as_ptr().cast::<[u8; 4]>() }) as u64)); - bytes = &bytes[4..]; - } - for b in bytes.iter() { - x = xorshift64(x.wrapping_add(*b as u64)); - } - self.0 = x; - } - - #[inline(always)] - fn write_isize(&mut self, i: isize) { - self.0 = xorshift64(self.0.wrapping_add(i as u64)); - } - - #[inline(always)] - fn write_usize(&mut self, i: usize) { - self.0 = xorshift64(self.0.wrapping_add(i as u64)); - } - - #[inline(always)] - fn write_i32(&mut self, i: i32) { - self.0 = xorshift64(self.0.wrapping_add(i as u64)); - } - - #[inline(always)] - fn write_u32(&mut self, i: u32) { - self.0 = xorshift64(self.0.wrapping_add(i as u64)); - } - - #[inline(always)] - fn write_i64(&mut self, i: i64) { - self.0 = xorshift64(self.0.wrapping_add(i as u64)); - } - - #[inline(always)] - fn write_u64(&mut self, i: u64) { - self.0 = xorshift64(self.0.wrapping_add(i)); - } -} - -struct Entry { - key: MaybeUninit, - value: MaybeUninit, - bucket: u16, // which bucket is this in? EMPTY for none - next: u16, // next item in bucket's linked list, EMPTY for none - prev: u16, // previous entry to permit deletion of old entries from bucket lists -} - -/// A hybrid between a circular buffer and a map. -/// -/// The map has a finite capacity. If a new entry is added and there's no more room the oldest -/// entry is removed and overwritten. The same could be achieved by pairing a circular buffer -/// with a HashMap but that would be less efficient. This requires no memory allocations unless -/// the K or V types allocate memory and occupies a fixed amount of memory. -/// -/// There is no explicit remove since that would require more complex logic to maintain FIFO -/// ordering for replacement of entries. Old entries just roll off the end. -/// -/// This is used for things like defragmenting incoming packets to support multiple fragmented -/// packets in flight. Having no allocations is good to reduce the potential for memory -/// exhaustion attacks. -/// -/// The C template parameter is the total capacity while the B parameter is the number of -/// buckets in the hash table. The maximum for both these parameters is 65535. This could be -/// increased by making the index variables larger (e.g. u32 instead of u16). -pub struct RingBufferMap { - entries: [Entry; C], - salt: u32, - buckets: [u16; B], - entry_ptr: u16, -} - -impl RingBufferMap { - /// Create a new map with the supplied random salt to perturb the hashing function. - #[inline] - pub fn new(salt: u32) -> Self { - debug_assert!(C <= EMPTY as usize); - debug_assert!(B <= EMPTY as usize); - #[allow(invalid_value)] - let mut tmp: Self = unsafe { MaybeUninit::uninit().assume_init() }; - // EMPTY is the maximum value of the indices, which is all 0xff, so this sets all indices to EMPTY. - unsafe { std::ptr::write_bytes(&mut tmp, 0xff, 1) }; - tmp.salt = salt; - tmp.entry_ptr = 0; - tmp - } - - #[inline] - pub fn get(&self, key: &K) -> Option<&V> { - let mut h = XorShiftHasher::new(self.salt); - key.hash(&mut h); - let mut e = self.buckets[(h.finish() as usize) % B]; - while e != EMPTY { - let ee = &self.entries[e as usize]; - debug_assert!(ee.bucket != EMPTY); - if unsafe { ee.key.assume_init_ref().eq(key) } { - return Some(unsafe { &ee.value.assume_init_ref() }); - } - e = ee.next; - } - return None; - } - - /// Get an entry, creating if not present, and return a mutable reference to it. - #[inline] - pub fn get_or_create_mut V>(&mut self, key: &K, create: CF) -> &mut V { - let mut h = XorShiftHasher::new(self.salt); - key.hash(&mut h); - let bucket = (h.finish() as usize) % B; - - let mut e = self.buckets[bucket]; - while e != EMPTY { - unsafe { - let e_ptr = &mut *self.entries.as_mut_ptr().add(e as usize); - debug_assert!(e_ptr.bucket != EMPTY); - if e_ptr.key.assume_init_ref().eq(key) { - return e_ptr.value.assume_init_mut(); - } - e = e_ptr.next; - } - } - - return self.internal_add(bucket, key.clone(), create()); - } - - /// Set a value or create a new entry if not found. - #[inline] - pub fn set(&mut self, key: K, value: V) { - let mut h = XorShiftHasher::new(self.salt); - key.hash(&mut h); - let bucket = (h.finish() as usize) % B; - - let mut e = self.buckets[bucket]; - while e != EMPTY { - let e_ptr = &mut self.entries[e as usize]; - debug_assert!(e_ptr.bucket != EMPTY); - if unsafe { e_ptr.key.assume_init_ref().eq(&key) } { - unsafe { *e_ptr.value.assume_init_mut() = value }; - return; - } - e = e_ptr.next; - } - - let _ = self.internal_add(bucket, key, value); - } - - #[inline] - fn internal_add(&mut self, bucket: usize, key: K, value: V) -> &mut V { - let e = (self.entry_ptr as usize) % C; - self.entry_ptr = self.entry_ptr.wrapping_add(1); - let e_ptr = unsafe { &mut *self.entries.as_mut_ptr().add(e) }; - - if e_ptr.bucket != EMPTY { - if e_ptr.prev != EMPTY { - self.entries[e_ptr.prev as usize].next = e_ptr.next; - } else { - self.buckets[e_ptr.bucket as usize] = e_ptr.next; - } - unsafe { - e_ptr.key.assume_init_drop(); - e_ptr.value.assume_init_drop(); - } - } - - e_ptr.key.write(key); - e_ptr.value.write(value); - e_ptr.bucket = bucket as u16; - e_ptr.next = self.buckets[bucket]; - if e_ptr.next != EMPTY { - self.entries[e_ptr.next as usize].prev = e as u16; - } - self.buckets[bucket] = e as u16; - e_ptr.prev = EMPTY; - unsafe { e_ptr.value.assume_init_mut() } - } -} - -impl Drop for RingBufferMap { - #[inline] - fn drop(&mut self) { - for e in self.entries.iter_mut() { - if e.bucket != EMPTY { - unsafe { - e.key.assume_init_drop(); - e.value.assume_init_drop(); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::RingBufferMap; - - #[test] - fn finite_map() { - let mut m = RingBufferMap::::new(1); - for i in 0..64 { - m.set(i, i); - } - for i in 0..64 { - assert_eq!(*m.get(&i).unwrap(), i); - } - - for i in 0..256 { - m.set(i, i); - } - for i in 0..128 { - assert!(m.get(&i).is_none()); - } - for i in 128..256 { - assert_eq!(*m.get(&i).unwrap(), i); - } - - m.set(1000, 1000); - assert!(m.get(&128).is_none()); - assert_eq!(*m.get(&129).unwrap(), 129); - assert_eq!(*m.get(&1000).unwrap(), 1000); - } -} diff --git a/vl1-service/src/vl1service.rs b/vl1-service/src/vl1service.rs index f7f8c6669..32450c8a9 100644 --- a/vl1-service/src/vl1service.rs +++ b/vl1-service/src/vl1service.rs @@ -37,7 +37,7 @@ pub struct VL1Service { vl1_data_storage: Arc, inner: Arc, buffer_pool: Arc, - node_container: Option, // never None, set in new() + node_container: Option>, // never None, set in new() } struct VL1ServiceMutableState { @@ -79,7 +79,7 @@ impl VL1Service { } #[inline(always)] - pub fn node(&self) -> &Node { + pub fn node(&self) -> &Node { debug_assert!(self.node_container.is_some()); unsafe { self.node_container.as_ref().unwrap_unchecked() } } @@ -216,6 +216,12 @@ impl ApplicationLayer for VL1Servi socket.is_valid() } + #[inline] + fn should_respond_to(&self, _: &Valid) -> bool { + // TODO: provide a way for the user of VL1Service to control this + true + } + #[inline] fn load_node_identity(&self) -> Option> { self.vl1_data_storage.load_node_identity() diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index dd85ac1d1..4db210db4 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -155,7 +155,6 @@ impl Context { /// /// * `max_incomplete_session_queue_size` - Maximum number of incomplete sessions in negotiation phase pub fn new(max_incomplete_session_queue_size: usize, default_physical_mtu: usize) -> Self { - zerotier_crypto::init(); Self { max_incomplete_session_queue_size, default_physical_mtu: AtomicUsize::new(default_physical_mtu), From 7a7703a268476b4fa46bf2185c4f2de6dbf374bd Mon Sep 17 00:00:00 2001 From: mamoniot Date: Tue, 14 Mar 2023 17:45:48 -0400 Subject: [PATCH 02/25] switched to sha512 everywhere --- crypto/src/hash.rs | 6 +- zssp/src/proto.rs | 13 ++--- zssp/src/zssp.rs | 139 ++++++++++++++++++++++++++------------------- 3 files changed, 88 insertions(+), 70 deletions(-) diff --git a/crypto/src/hash.rs b/crypto/src/hash.rs index e71472b9c..808567fcf 100644 --- a/crypto/src/hash.rs +++ b/crypto/src/hash.rs @@ -276,12 +276,10 @@ pub fn hmac_sha512_into(key: &[u8], msg: &[u8], md: &mut [u8]) { hm.finish_into(md); } -pub fn hmac_sha512_secret(key: &[u8], msg: &[u8]) -> Secret { - debug_assert!(C <= HMAC_SHA512_SIZE); +pub fn hmac_sha512_secret(key: &[u8], msg: &[u8]) -> Secret { let mut hm = HMACSHA512::new(key); hm.update(msg); - let buff = hm.finish(); - unsafe { Secret::from_bytes(&buff[..C]) } + Secret::move_bytes(hm.finish()) } #[inline(always)] diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index 7952c9d30..b3d6a93dd 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -9,7 +9,7 @@ use std::mem::size_of; use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_PUBLICKEYBYTES}; -use zerotier_crypto::hash::SHA384_HASH_SIZE; +use zerotier_crypto::hash::SHA512_HASH_SIZE; use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE; use crate::error::Error; @@ -26,9 +26,8 @@ pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_ /// Initial value of 'h' /// echo -n 'Noise_XKpsk3_P384_AESGCM_SHA384_hybridKyber1024' | shasum -a 384 -pub(crate) const INITIAL_H: [u8; SHA384_HASH_SIZE] = [ - 0x35, 0x27, 0x16, 0x62, 0x58, 0x04, 0x0c, 0x7a, 0x99, 0xa8, 0x0b, 0x49, 0xb2, 0x6b, 0x25, 0xfb, 0xf5, 0x26, 0x2a, 0x26, 0xe7, 0xb3, 0x70, 0xcb, - 0x2c, 0x3c, 0xcb, 0x7f, 0xca, 0x20, 0x06, 0x91, 0x20, 0x55, 0x52, 0x8e, 0xd4, 0x3c, 0x97, 0xc3, 0xd5, 0x6c, 0xb4, 0x13, 0x02, 0x54, 0x83, 0x12, +pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = [ + 0xd3, 0x18, 0x1c, 0xee, 0x05, 0x8d, 0x35, 0x06, 0x22, 0xcd, 0xfc, 0x46, 0x82, 0x2a, 0x60, 0x81, 0xf5, 0xe0, 0x47, 0x65, 0x57, 0x66, 0x53, 0x17, 0x95, 0xae, 0x33, 0x3c, 0x90, 0x83, 0x3b, 0xbf, 0x7f, 0x5e, 0xd7, 0x3d, 0x03, 0x39, 0x10, 0x03, 0x29, 0xfd, 0x2d, 0x59, 0xa0, 0x99, 0x56, 0x63, 0x18, 0x2d, 0x63, 0xb7, 0x8d, 0xd1, 0x7a, 0x2c, 0xf7, 0x92, 0x16, 0xdd, 0xfa, 0xf1, 0x05, 0x37, ]; /// Version 0: Noise_XK with NIST P-384 plus Kyber1024 hybrid exchange on session init. @@ -65,7 +64,7 @@ pub(crate) const MAX_NOISE_HANDSHAKE_FRAGMENTS: usize = 16; // enough room for p pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS * MIN_TRANSPORT_MTU; /// Size of keys used during derivation, mixing, etc. process. -pub(crate) const BASE_KEY_SIZE: usize = 64; +pub(crate) const NOISE_HASHLEN: usize = SHA512_HASH_SIZE; pub(crate) const AES_256_KEY_SIZE: usize = 32; pub(crate) const AES_HEADER_PROTECTION_KEY_SIZE: usize = 16; @@ -160,14 +159,14 @@ pub(crate) struct RekeyAck { pub session_protocol_version: u8, // -- start AES-GCM encrypted portion (using current key) pub bob_e: [u8; P384_PUBLIC_KEY_SIZE], - pub next_key_fingerprint: [u8; SHA384_HASH_SIZE], // SHA384(next secret) + pub next_key_fingerprint: [u8; SHA512_HASH_SIZE], // SHA384(next secret) // -- end AES-GCM encrypted portion pub gcm_tag: [u8; AES_GCM_TAG_SIZE], } impl RekeyAck { pub const ENC_START: usize = HEADER_SIZE + 1; - pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE + SHA384_HASH_SIZE; + pub const AUTH_START: usize = Self::ENC_START + P384_PUBLIC_KEY_SIZE + SHA512_HASH_SIZE; pub const SIZE: usize = Self::AUTH_START + AES_GCM_TAG_SIZE; } diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index dd85ac1d1..c33d45f6e 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -15,8 +15,8 @@ use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesGcm}; -use zerotier_crypto::hash::{hmac_sha512_secret, SHA384, SHA384_HASH_SIZE}; -use zerotier_crypto::p384::{P384KeyPair, P384PublicKey, P384_ECDH_SHARED_SECRET_SIZE}; +use zerotier_crypto::hash::{SHA512, hmac_sha512_secret}; +use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; @@ -106,8 +106,8 @@ struct IncomingIncompleteSession { timestamp: i64, alice_session_id: SessionId, bob_session_id: SessionId, - noise_h: [u8; SHA384_HASH_SIZE], - noise_es_ee: Secret, + noise_h: [u8; NOISE_HASHLEN], + noise_ck_es_ee: Secret, hk: Secret, header_protection_key: Secret, bob_noise_e_secret: P384KeyPair, @@ -116,9 +116,9 @@ struct IncomingIncompleteSession { struct OutgoingSessionOffer { last_retry_time: AtomicI64, - psk: Secret, - noise_h: [u8; SHA384_HASH_SIZE], - noise_es: Secret, + psk: Secret, + noise_h: [u8; NOISE_HASHLEN], + noise_ck_es: Secret, alice_noise_e_secret: P384KeyPair, alice_hk_secret: Secret, metadata: Option>, @@ -139,7 +139,7 @@ enum Offer { } struct SessionKey { - ratchet_key: Secret, // Key used in derivation of the next session key + ratchet_key: Secret, // Key used in derivation of the next session key receive_cipher_pool: [Mutex>; GCM_CIPHER_POOL_SIZE], // Pool of reusable sending ciphers send_cipher_pool: [Mutex>; GCM_CIPHER_POOL_SIZE], // Pool of reusable receiving ciphers rekey_at_time: i64, // Rekey at or after this time (ticks) @@ -290,7 +290,7 @@ impl Context { mtu: usize, remote_s_public_blob: &[u8], remote_s_public_p384: P384PublicKey, - psk: Secret, + psk: Secret, metadata: Option>, application_data: Application::Data, current_time: i64, @@ -302,9 +302,14 @@ impl Context { let alice_noise_e_secret = P384KeyPair::generate(); let alice_noise_e = alice_noise_e_secret.public_key_bytes().clone(); let noise_es = alice_noise_e_secret.agree(&remote_s_public_p384).ok_or(Error::InvalidParameter)?; + let noise_h = mix_hash(&mix_hash(&INITIAL_H, remote_s_public_blob), &alice_noise_e); + let noise_ck_es = hmac_sha512_secret(&INITIAL_H, noise_es.as_bytes()); let alice_hk_secret = pqc_kyber::keypair(&mut random::SecureRandom::default()); let header_protection_key: Secret = Secret(random::get_bytes_secure()); + // Init aesgcm before we move noise_ck_es + let mut gcm = AesGcm::new(&kbkdf256::(&noise_ck_es)); + let (local_session_id, session) = { let mut sessions = self.sessions.write().unwrap(); @@ -331,8 +336,8 @@ impl Context { outgoing_offer: Offer::NoiseXKInit(Box::new(OutgoingSessionOffer { last_retry_time: AtomicI64::new(current_time), psk, - noise_h: mix_hash(&mix_hash(&INITIAL_H, remote_s_public_blob), &alice_noise_e), - noise_es: noise_es.clone(), + noise_h, + noise_ck_es, alice_noise_e_secret, alice_hk_secret: Secret(alice_hk_secret.secret), metadata, @@ -367,7 +372,6 @@ impl Context { } // Encrypt and add authentication tag. - let mut gcm = AesGcm::new(&kbkdf::(noise_es.as_bytes())); gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_INIT, 1)); gcm.aad(&offer.noise_h); gcm.crypt_in_place(&mut init_packet[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]); @@ -736,9 +740,11 @@ impl Context { let noise_h = mix_hash(&mix_hash(&INITIAL_H, app.get_local_s_public_blob()), alice_noise_e.as_bytes()); let noise_h_next = mix_hash(&noise_h, &pkt_assembled[HEADER_SIZE..]); + let noise_ck_es = hmac_sha512_secret(&INITIAL_H, noise_es.as_bytes()); + drop(noise_es); // Decrypt and authenticate init packet, also proving that caller knows our static identity. - let mut gcm = AesGcm::new(&kbkdf::(noise_es.as_bytes())); + let mut gcm = AesGcm::new(&kbkdf256::(&noise_ck_es)); gcm.reset_init_gcm(&incoming_message_nonce); gcm.aad(&noise_h); gcm.crypt_in_place(&mut pkt_assembled[AliceNoiseXKInit::ENC_START..AliceNoiseXKInit::AUTH_START]); @@ -755,12 +761,12 @@ impl Context { let alice_session_id = SessionId::new_from_array(&pkt.alice_session_id).ok_or(Error::InvalidPacket)?; let header_protection_key = Secret(pkt.header_protection_key); - // Create Bob's ephemeral keys and derive noise_es_ee by agreeing with Alice's. Also create + // Create Bob's ephemeral keys and derive noise_ck by agreeing with Alice's. Also create // a Kyber ciphertext to send back to Alice. let bob_noise_e_secret = P384KeyPair::generate(); let bob_noise_e = bob_noise_e_secret.public_key_bytes().clone(); - let noise_es_ee = hmac_sha512_secret( - noise_es.as_bytes(), + let noise_ck_es_ee = hmac_sha512_secret( + noise_ck_es.as_bytes(), bob_noise_e_secret.agree(&alice_noise_e).ok_or(Error::FailedAuthentication)?.as_bytes(), ); let (bob_hk_ciphertext, hk) = pqc_kyber::encapsulate(&pkt.alice_hk_public, &mut random::SecureRandom::default()) @@ -787,7 +793,7 @@ impl Context { ack.bob_hk_ciphertext = bob_hk_ciphertext; // Encrypt main section of reply and attach tag. - let mut gcm = AesGcm::new(&kbkdf::(noise_es_ee.as_bytes())); + let mut gcm = AesGcm::new(&kbkdf256::(&noise_ck_es_ee)); gcm.reset_init_gcm(&create_message_nonce(PACKET_TYPE_BOB_NOISE_XK_ACK, 1)); gcm.aad(&noise_h_next); gcm.crypt_in_place(&mut ack_packet[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START]); @@ -820,7 +826,7 @@ impl Context { alice_session_id, bob_session_id, noise_h: mix_hash(&mix_hash(&noise_h_next, &bob_noise_e), &ack_packet[HEADER_SIZE..]), - noise_es_ee: noise_es_ee.clone(), + noise_ck_es_ee, hk, bob_noise_e_secret, header_protection_key: Secret(pkt.header_protection_key), @@ -875,8 +881,8 @@ impl Context { // Derive noise_es_ee from Bob's ephemeral public key. let bob_noise_e = P384PublicKey::from_bytes(&pkt.bob_noise_e).ok_or(Error::FailedAuthentication)?; - let noise_es_ee = hmac_sha512_secret::( - outgoing_offer.noise_es.as_bytes(), + let noise_ck_es_ee = hmac_sha512_secret( + outgoing_offer.noise_ck_es.as_bytes(), outgoing_offer .alice_noise_e_secret .agree(&bob_noise_e) @@ -888,7 +894,7 @@ impl Context { let noise_h_next = mix_hash(&mix_hash(&outgoing_offer.noise_h, bob_noise_e.as_bytes()), &pkt_assembled[HEADER_SIZE..]); // Decrypt and authenticate Bob's reply. - let mut gcm = AesGcm::new(&kbkdf::(noise_es_ee.as_bytes())); + let mut gcm = AesGcm::new(&kbkdf256::(&noise_ck_es_ee)); gcm.reset_init_gcm(&incoming_message_nonce); gcm.aad(&outgoing_offer.noise_h); gcm.crypt_in_place(&mut pkt_assembled[BobNoiseXKAck::ENC_START..BobNoiseXKAck::AUTH_START]); @@ -910,9 +916,9 @@ impl Context { // Packet fully authenticated if session.update_receive_window(incoming_counter) { - let noise_es_ee_se_hk_psk = hmac_sha512_secret::( - hmac_sha512_secret::(noise_es_ee.as_bytes(), noise_se.as_bytes()).as_bytes(), - hmac_sha512_secret::(outgoing_offer.psk.as_bytes(), hk.as_bytes()).as_bytes(), + let noise_ck_es_ee_se_hk_psk = hmac_sha512_secret( + hmac_sha512_secret(noise_ck_es_ee.as_bytes(), noise_se.as_bytes()).as_bytes(), + hmac_sha512_secret(outgoing_offer.psk.as_bytes(), hk.as_bytes()).as_bytes(), ); let reply_message_nonce = create_message_nonce(PACKET_TYPE_ALICE_NOISE_XK_ACK, 2); @@ -929,8 +935,8 @@ impl Context { let mut enc_start = ack_len; ack_len = append_to_slice(&mut ack, ack_len, alice_s_public_blob)?; - let mut gcm = AesGcm::new(&kbkdf::( - hmac_sha512_secret::(noise_es_ee.as_bytes(), hk.as_bytes()).as_bytes(), + let mut gcm = AesGcm::new(&kbkdf256::( + &hmac_sha512_secret(noise_ck_es_ee.as_bytes(), hk.as_bytes()), )); gcm.reset_init_gcm(&reply_message_nonce); gcm.aad(&noise_h_next); @@ -947,8 +953,8 @@ impl Context { enc_start = ack_len; ack_len = append_to_slice(&mut ack, ack_len, metadata)?; - let mut gcm = AesGcm::new(&kbkdf::( - noise_es_ee_se_hk_psk.as_bytes(), + let mut gcm = AesGcm::new(&kbkdf256::( + &noise_ck_es_ee_se_hk_psk, )); gcm.reset_init_gcm(&reply_message_nonce); gcm.aad(&noise_h_next); @@ -962,7 +968,7 @@ impl Context { let mut state = session.state.write().unwrap(); let _ = state.remote_session_id.insert(bob_session_id); let _ = state.keys[0].insert(SessionKey::new::( - noise_es_ee_se_hk_psk, + noise_ck_es_ee_se_hk_psk, 1, current_time, 2, @@ -1035,8 +1041,8 @@ impl Context { let alice_static_public_blob = r.read_decrypt_auth( alice_static_public_blob_size, - kbkdf::( - hmac_sha512_secret::(incoming.noise_es_ee.as_bytes(), incoming.hk.as_bytes()).as_bytes(), + kbkdf256::( + &hmac_sha512_secret(incoming.noise_ck_es_ee.as_bytes(), incoming.hk.as_bytes()), ), &incoming.noise_h, &incoming_message_nonce, @@ -1053,9 +1059,9 @@ impl Context { let noise_h_next = mix_hash(&noise_h_next, psk.as_bytes()); // Complete Noise_XKpsk3 on Bob's side. - let noise_es_ee_se_hk_psk = hmac_sha512_secret::( - hmac_sha512_secret::( - incoming.noise_es_ee.as_bytes(), + let noise_ck_es_ee_se_hk_psk = hmac_sha512_secret( + hmac_sha512_secret( + incoming.noise_ck_es_ee.as_bytes(), incoming .bob_noise_e_secret .agree(&alice_noise_s) @@ -1063,7 +1069,7 @@ impl Context { .as_bytes(), ) .as_bytes(), - hmac_sha512_secret::(psk.as_bytes(), incoming.hk.as_bytes()).as_bytes(), + hmac_sha512_secret(psk.as_bytes(), incoming.hk.as_bytes()).as_bytes(), ); // Decrypt meta-data and verify the final key in the process. Copy meta-data @@ -1071,7 +1077,7 @@ impl Context { let alice_meta_data_size = r.read_u16()? as usize; let alice_meta_data = r.read_decrypt_auth( alice_meta_data_size, - kbkdf::(noise_es_ee_se_hk_psk.as_bytes()), + kbkdf256::(&noise_ck_es_ee_se_hk_psk), &noise_h_next, &incoming_message_nonce, )?; @@ -1091,7 +1097,7 @@ impl Context { physical_mtu: self.default_physical_mtu.load(Ordering::Relaxed), remote_session_id: Some(incoming.alice_session_id), keys: [ - Some(SessionKey::new::(noise_es_ee_se_hk_psk, 1, current_time, 2, true, true)), + Some(SessionKey::new::(noise_ck_es_ee_se_hk_psk, 1, current_time, 2, true, true)), None, ], current_key: 0, @@ -1151,9 +1157,9 @@ impl Context { let noise_es = app.get_local_s_keypair().agree(&alice_e).ok_or(Error::FailedAuthentication)?; let noise_ee = bob_e_secret.agree(&alice_e).ok_or(Error::FailedAuthentication)?; let noise_se = bob_e_secret.agree(&session.static_public_key).ok_or(Error::FailedAuthentication)?; - let noise_psk_se_ee_es = hmac_sha512_secret::( - hmac_sha512_secret::( - hmac_sha512_secret::(key.ratchet_key.as_bytes(), noise_es.as_bytes()).as_bytes(), + let noise_psk_se_ee_es = hmac_sha512_secret( + hmac_sha512_secret( + hmac_sha512_secret(key.ratchet_key.as_bytes(), noise_es.as_bytes()).as_bytes(), noise_ee.as_bytes(), ) .as_bytes(), @@ -1166,7 +1172,7 @@ impl Context { let reply: &mut RekeyAck = byte_array_as_proto_buffer_mut(&mut reply_buf).unwrap(); reply.session_protocol_version = SESSION_PROTOCOL_VERSION; reply.bob_e = *bob_e_secret.public_key_bytes(); - reply.next_key_fingerprint = SHA384::hash(noise_psk_se_ee_es.as_bytes()); + reply.next_key_fingerprint = SHA512::hash(noise_psk_se_ee_es.as_bytes()); let counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?.get(); set_packet_header( @@ -1249,9 +1255,9 @@ impl Context { let noise_es = alice_e_secret.agree(&session.static_public_key).ok_or(Error::FailedAuthentication)?; let noise_ee = alice_e_secret.agree(&bob_e).ok_or(Error::FailedAuthentication)?; let noise_se = app.get_local_s_keypair().agree(&bob_e).ok_or(Error::FailedAuthentication)?; - let noise_psk_se_ee_es = hmac_sha512_secret::( - hmac_sha512_secret::( - hmac_sha512_secret::(key.ratchet_key.as_bytes(), noise_es.as_bytes()).as_bytes(), + let noise_psk_se_ee_es = hmac_sha512_secret( + hmac_sha512_secret( + hmac_sha512_secret(key.ratchet_key.as_bytes(), noise_es.as_bytes()).as_bytes(), noise_ee.as_bytes(), ) .as_bytes(), @@ -1260,7 +1266,7 @@ impl Context { // We need to check that the key Bob is acknowledging matches the latest sent offer. // Because of OOO, it might not, in which case this rekey must be cancelled and retried. - if secure_eq(&pkt.next_key_fingerprint, &SHA384::hash(noise_psk_se_ee_es.as_bytes())) { + if secure_eq(&pkt.next_key_fingerprint, &SHA512::hash(noise_psk_se_ee_es.as_bytes())) { if session.update_receive_window(incoming_counter) { // The new "Alice" knows Bob has the key since this is an ACK, so she can go // ahead and set current_key to the new key. Then when she sends something @@ -1391,10 +1397,10 @@ impl Session { } /// Get the ratchet count and a hash fingerprint of the current active key. - pub fn key_info(&self) -> Option<(u64, [u8; 48])> { + pub fn key_info(&self) -> Option<(u64, [u8; NOISE_HASHLEN])> { let state = self.state.read().unwrap(); if let Some(key) = state.keys[state.current_key].as_ref() { - Some((key.ratchet_count, SHA384::hash(key.ratchet_key.as_bytes()))) + Some((key.ratchet_count, SHA512::hash(key.ratchet_key.as_bytes()))) } else { None } @@ -1583,15 +1589,15 @@ fn assemble_fragments_into(fragments: &[A::IncomingPacketBu impl SessionKey { fn new( - key: Secret, + key: Secret, ratchet_count: u64, current_time: i64, current_counter: u64, bob: bool, confirmed: bool, ) -> Self { - let a2b = kbkdf::(key.as_bytes()); - let b2a = kbkdf::(key.as_bytes()); + let a2b = kbkdf256::(&key); + let b2a = kbkdf256::(&key); let (receive_key, send_key) = if bob { (a2b, b2a) } else { @@ -1600,7 +1606,7 @@ impl SessionKey { let receive_cipher_pool = std::array::from_fn(|_| Mutex::new(AesGcm::new(&receive_key))); let send_cipher_pool = std::array::from_fn(|_| Mutex::new(AesGcm::new(&send_key))); Self { - ratchet_key: kbkdf::(key.as_bytes()), + ratchet_key: kbkdf512::(&key), receive_cipher_pool, send_cipher_pool, rekey_at_time: current_time @@ -1680,8 +1686,8 @@ fn append_to_slice(s: &mut [u8], p: usize, d: &[u8]) -> Result { } /// MixHash to update 'h' during negotiation. -fn mix_hash(h: &[u8; SHA384_HASH_SIZE], m: &[u8]) -> [u8; SHA384_HASH_SIZE] { - let mut hasher = SHA384::new(); +fn mix_hash(h: &[u8; NOISE_HASHLEN], m: &[u8]) -> [u8; NOISE_HASHLEN] { + let mut hasher = SHA512::new(); hasher.update(h); hasher.update(m); hasher.finish() @@ -1689,11 +1695,11 @@ fn mix_hash(h: &[u8; SHA384_HASH_SIZE], m: &[u8]) -> [u8; SHA384_HASH_SIZE] { /// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 7) /// Cryptographically this isn't meaningfully different from HMAC(key, [label]) but this is how NIST rolls. -fn kbkdf(key: &[u8]) -> Secret { - //These are the values we have assigned to the 5 variables involved in https://csrc.nist.gov/publications/detail/sp/800-108/final: - // K_in = key, i = 0x01, Label = 'Z'||'T'||LABEL, Context = 0x00, L = (OUTPUT_BYTES * 8) +/// These are the values we have assigned to the 5 variables involved in https://csrc.nist.gov/publications/detail/sp/800-108/final: +/// K_in = key, i = 0x01, Label = 'Z'||'T'||LABEL, Context = 0x00, L = 512 or 256 as a u16 +fn kbkdf512(key: &Secret) -> Secret { hmac_sha512_secret( - key, + key.as_bytes(), &[ 1, b'Z', @@ -1701,11 +1707,26 @@ fn kbkdf(key: &[u8]) -> Secret> 8) & 0xff) as u8, - ((OUTPUT_BYTES * 8) & 0xff) as u8, + 2u8, + 0u8, ], ) } +fn kbkdf256(key: &Secret) -> Secret<32> { + hmac_sha512_secret( + key.as_bytes(), + &[ + 1, + b'Z', + b'T', + LABEL, + 0x00, + 0, + 1u8, + 0u8, + ], + ).first_n_clone() +} fn prng32(mut x: u32) -> u32 { // based on lowbias32 from https://nullprogram.com/blog/2018/07/31/ From 2c607f72d82236eac98f43493efb9421d4ff05c3 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Tue, 14 Mar 2023 18:14:01 -0400 Subject: [PATCH 03/25] made rekeying noise compliant --- crypto/src/hash.rs | 17 ++++++----------- zssp/Cargo.toml | 1 + zssp/src/proto.rs | 10 ++++++---- zssp/src/zssp.rs | 32 ++++++++++++++++++++------------ 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/crypto/src/hash.rs b/crypto/src/hash.rs index 808567fcf..c9a7a2327 100644 --- a/crypto/src/hash.rs +++ b/crypto/src/hash.rs @@ -269,13 +269,15 @@ pub fn hmac_sha512(key: &[u8], msg: &[u8]) -> [u8; HMAC_SHA512_SIZE] { hm.finish() } -#[inline(always)] -pub fn hmac_sha512_into(key: &[u8], msg: &[u8], md: &mut [u8]) { +pub fn hmac_sha512_secret256(key: &[u8], msg: &[u8]) -> Secret<32> { let mut hm = HMACSHA512::new(key); hm.update(msg); - hm.finish_into(md); + let mut md = [0u8; HMAC_SHA512_SIZE]; + hm.finish_into(&mut md); + // With such a simple procedure hopefully the compiler implements the following line as a move from md and not a copy + // If not we ought to change this code so we don't leak a secret value on the stack + unsafe { Secret::from_bytes(&md[0..32]) } } - pub fn hmac_sha512_secret(key: &[u8], msg: &[u8]) -> Secret { let mut hm = HMACSHA512::new(key); hm.update(msg); @@ -288,10 +290,3 @@ pub fn hmac_sha384(key: &[u8], msg: &[u8]) -> [u8; HMAC_SHA384_SIZE] { hm.update(msg); hm.finish() } - -#[inline(always)] -pub fn hmac_sha384_into(key: &[u8], msg: &[u8], md: &mut [u8]) { - let mut hm = HMACSHA384::new(key); - hm.update(msg); - hm.finish_into(md); -} diff --git a/zssp/Cargo.toml b/zssp/Cargo.toml index d4888b865..1d381576a 100644 --- a/zssp/Cargo.toml +++ b/zssp/Cargo.toml @@ -25,3 +25,4 @@ doc = false zerotier-utils = { path = "../utils" } zerotier-crypto = { path = "../crypto" } pqc_kyber = { version = "0.4.0", default-features = false, features = ["kyber1024", "std"] } +hex-literal = "0.3.4" diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index b3d6a93dd..95d22d92d 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -8,6 +8,7 @@ use std::mem::size_of; +use hex_literal::hex; use pqc_kyber::{KYBER_CIPHERTEXTBYTES, KYBER_PUBLICKEYBYTES}; use zerotier_crypto::hash::SHA512_HASH_SIZE; use zerotier_crypto::p384::P384_PUBLIC_KEY_SIZE; @@ -25,10 +26,11 @@ pub const MIN_TRANSPORT_MTU: usize = 128; pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_XK_ACK_MIN_SIZE; /// Initial value of 'h' -/// echo -n 'Noise_XKpsk3_P384_AESGCM_SHA384_hybridKyber1024' | shasum -a 384 -pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = [ - 0xd3, 0x18, 0x1c, 0xee, 0x05, 0x8d, 0x35, 0x06, 0x22, 0xcd, 0xfc, 0x46, 0x82, 0x2a, 0x60, 0x81, 0xf5, 0xe0, 0x47, 0x65, 0x57, 0x66, 0x53, 0x17, 0x95, 0xae, 0x33, 0x3c, 0x90, 0x83, 0x3b, 0xbf, 0x7f, 0x5e, 0xd7, 0x3d, 0x03, 0x39, 0x10, 0x03, 0x29, 0xfd, 0x2d, 0x59, 0xa0, 0x99, 0x56, 0x63, 0x18, 0x2d, 0x63, 0xb7, 0x8d, 0xd1, 0x7a, 0x2c, 0xf7, 0x92, 0x16, 0xdd, 0xfa, 0xf1, 0x05, 0x37, -]; +/// echo -n 'Noise_XKpsk3_P384_AESGCM_SHA512_hybridKyber1024' | shasum -a 512 +pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = hex!("12ae70954e8d93bf7f73d0fe48d487155666f541e532f9461af5ef52ab90c8fd9259ef9e48f5adcf9af63f869805a570004ae095655dcaddbc226a50623b2b25"); +/// Initial value of 'h' +/// echo -n 'Noise_KKpsk0_P384_AESGCM_SHA512' | shasum -a 512 +pub(crate) const INITIAL_H_REKEY: [u8; SHA512_HASH_SIZE] = hex!("daeedd651ac9c5173f2eaaff996beebac6f3f1bfe9a70bb1cc54fa1fb2bf46260d71a3c4fb4d4ee36f654c31773a8a15e5d5be974a0668dc7db70f4e13ed172e"); /// Version 0: Noise_XK with NIST P-384 plus Kyber1024 hybrid exchange on session init. pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index c33d45f6e..e397565e1 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -15,7 +15,7 @@ use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesGcm}; -use zerotier_crypto::hash::{SHA512, hmac_sha512_secret}; +use zerotier_crypto::hash::{SHA512, hmac_sha512_secret, hmac_sha512_secret256}; use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; @@ -1157,9 +1157,13 @@ impl Context { let noise_es = app.get_local_s_keypair().agree(&alice_e).ok_or(Error::FailedAuthentication)?; let noise_ee = bob_e_secret.agree(&alice_e).ok_or(Error::FailedAuthentication)?; let noise_se = bob_e_secret.agree(&session.static_public_key).ok_or(Error::FailedAuthentication)?; - let noise_psk_se_ee_es = hmac_sha512_secret( + let noise_ck_psk_es_ee_se = hmac_sha512_secret( hmac_sha512_secret( - hmac_sha512_secret(key.ratchet_key.as_bytes(), noise_es.as_bytes()).as_bytes(), + hmac_sha512_secret( + hmac_sha512_secret(&INITIAL_H_REKEY, key.ratchet_key.as_bytes()).as_bytes(), + noise_es.as_bytes(), + ) + .as_bytes(), noise_ee.as_bytes(), ) .as_bytes(), @@ -1172,7 +1176,7 @@ impl Context { let reply: &mut RekeyAck = byte_array_as_proto_buffer_mut(&mut reply_buf).unwrap(); reply.session_protocol_version = SESSION_PROTOCOL_VERSION; reply.bob_e = *bob_e_secret.public_key_bytes(); - reply.next_key_fingerprint = SHA512::hash(noise_psk_se_ee_es.as_bytes()); + reply.next_key_fingerprint = SHA512::hash(noise_ck_psk_es_ee_se.as_bytes()); let counter = session.get_next_outgoing_counter().ok_or(Error::MaxKeyLifetimeExceeded)?.get(); set_packet_header( @@ -1204,7 +1208,7 @@ impl Context { drop(state); let mut state = session.state.write().unwrap(); let _ = state.keys[key_index ^ 1].replace(SessionKey::new::( - noise_psk_se_ee_es, + noise_ck_psk_es_ee_se, next_ratchet_count, current_time, counter, @@ -1255,9 +1259,13 @@ impl Context { let noise_es = alice_e_secret.agree(&session.static_public_key).ok_or(Error::FailedAuthentication)?; let noise_ee = alice_e_secret.agree(&bob_e).ok_or(Error::FailedAuthentication)?; let noise_se = app.get_local_s_keypair().agree(&bob_e).ok_or(Error::FailedAuthentication)?; - let noise_psk_se_ee_es = hmac_sha512_secret( + let noise_ck_psk_es_ee_se = hmac_sha512_secret( hmac_sha512_secret( - hmac_sha512_secret(key.ratchet_key.as_bytes(), noise_es.as_bytes()).as_bytes(), + hmac_sha512_secret( + hmac_sha512_secret(&INITIAL_H_REKEY, key.ratchet_key.as_bytes()).as_bytes(), + noise_es.as_bytes(), + ) + .as_bytes(), noise_ee.as_bytes(), ) .as_bytes(), @@ -1266,7 +1274,7 @@ impl Context { // We need to check that the key Bob is acknowledging matches the latest sent offer. // Because of OOO, it might not, in which case this rekey must be cancelled and retried. - if secure_eq(&pkt.next_key_fingerprint, &SHA512::hash(noise_psk_se_ee_es.as_bytes())) { + if secure_eq(&pkt.next_key_fingerprint, &SHA512::hash(noise_ck_psk_es_ee_se.as_bytes())) { if session.update_receive_window(incoming_counter) { // The new "Alice" knows Bob has the key since this is an ACK, so she can go // ahead and set current_key to the new key. Then when she sends something @@ -1276,7 +1284,7 @@ impl Context { let next_key_index = key_index ^ 1; let mut state = session.state.write().unwrap(); let _ = state.keys[next_key_index].replace(SessionKey::new::( - noise_psk_se_ee_es, + noise_ck_psk_es_ee_se, next_ratchet_count, current_time, session.send_counter.load(Ordering::Relaxed), @@ -1696,7 +1704,7 @@ fn mix_hash(h: &[u8; NOISE_HASHLEN], m: &[u8]) -> [u8; NOISE_HASHLEN] { /// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 7) /// Cryptographically this isn't meaningfully different from HMAC(key, [label]) but this is how NIST rolls. /// These are the values we have assigned to the 5 variables involved in https://csrc.nist.gov/publications/detail/sp/800-108/final: -/// K_in = key, i = 0x01, Label = 'Z'||'T'||LABEL, Context = 0x00, L = 512 or 256 as a u16 +/// K_in = key, i = 1u8, Label = b'Z'||b'T'||LABEL, Context = 0u8, L = 512u16 or 256u16 fn kbkdf512(key: &Secret) -> Secret { hmac_sha512_secret( key.as_bytes(), @@ -1713,7 +1721,7 @@ fn kbkdf512(key: &Secret) -> Secret(key: &Secret) -> Secret<32> { - hmac_sha512_secret( + hmac_sha512_secret256( key.as_bytes(), &[ 1, @@ -1725,7 +1733,7 @@ fn kbkdf256(key: &Secret) -> Secret<32> { 1u8, 0u8, ], - ).first_n_clone() + ) } fn prng32(mut x: u32) -> u32 { From 3b3ed9765d69f75684aa6109d24ec0eff59025c0 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Tue, 14 Mar 2023 18:15:24 -0400 Subject: [PATCH 04/25] fixed formatting --- zssp/src/proto.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index 95d22d92d..2913928a5 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -27,10 +27,14 @@ pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_ /// Initial value of 'h' /// echo -n 'Noise_XKpsk3_P384_AESGCM_SHA512_hybridKyber1024' | shasum -a 512 -pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = hex!("12ae70954e8d93bf7f73d0fe48d487155666f541e532f9461af5ef52ab90c8fd9259ef9e48f5adcf9af63f869805a570004ae095655dcaddbc226a50623b2b25"); +pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = hex!( + "12ae70954e8d93bf7f73d0fe48d487155666f541e532f9461af5ef52ab90c8fd9259ef9e48f5adcf9af63f869805a570004ae095655dcaddbc226a50623b2b25" +); /// Initial value of 'h' /// echo -n 'Noise_KKpsk0_P384_AESGCM_SHA512' | shasum -a 512 -pub(crate) const INITIAL_H_REKEY: [u8; SHA512_HASH_SIZE] = hex!("daeedd651ac9c5173f2eaaff996beebac6f3f1bfe9a70bb1cc54fa1fb2bf46260d71a3c4fb4d4ee36f654c31773a8a15e5d5be974a0668dc7db70f4e13ed172e"); +pub(crate) const INITIAL_H_REKEY: [u8; SHA512_HASH_SIZE] = hex!( + "daeedd651ac9c5173f2eaaff996beebac6f3f1bfe9a70bb1cc54fa1fb2bf46260d71a3c4fb4d4ee36f654c31773a8a15e5d5be974a0668dc7db70f4e13ed172e" +); /// Version 0: Noise_XK with NIST P-384 plus Kyber1024 hybrid exchange on session init. pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; From 87eb124551aa45d3b9275b9ec54671ec5c4ca769 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Tue, 14 Mar 2023 18:16:21 -0400 Subject: [PATCH 05/25] ran cargo fmt --- zssp/src/proto.rs | 10 ++++------ zssp/src/zssp.rs | 48 ++++++++++++----------------------------------- 2 files changed, 16 insertions(+), 42 deletions(-) diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index 2913928a5..826371983 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -27,14 +27,12 @@ pub const MAX_INIT_PAYLOAD_SIZE: usize = MAX_NOISE_HANDSHAKE_SIZE - ALICE_NOISE_ /// Initial value of 'h' /// echo -n 'Noise_XKpsk3_P384_AESGCM_SHA512_hybridKyber1024' | shasum -a 512 -pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = hex!( - "12ae70954e8d93bf7f73d0fe48d487155666f541e532f9461af5ef52ab90c8fd9259ef9e48f5adcf9af63f869805a570004ae095655dcaddbc226a50623b2b25" -); +pub(crate) const INITIAL_H: [u8; SHA512_HASH_SIZE] = + hex!("12ae70954e8d93bf7f73d0fe48d487155666f541e532f9461af5ef52ab90c8fd9259ef9e48f5adcf9af63f869805a570004ae095655dcaddbc226a50623b2b25"); /// Initial value of 'h' /// echo -n 'Noise_KKpsk0_P384_AESGCM_SHA512' | shasum -a 512 -pub(crate) const INITIAL_H_REKEY: [u8; SHA512_HASH_SIZE] = hex!( - "daeedd651ac9c5173f2eaaff996beebac6f3f1bfe9a70bb1cc54fa1fb2bf46260d71a3c4fb4d4ee36f654c31773a8a15e5d5be974a0668dc7db70f4e13ed172e" -); +pub(crate) const INITIAL_H_REKEY: [u8; SHA512_HASH_SIZE] = + hex!("daeedd651ac9c5173f2eaaff996beebac6f3f1bfe9a70bb1cc54fa1fb2bf46260d71a3c4fb4d4ee36f654c31773a8a15e5d5be974a0668dc7db70f4e13ed172e"); /// Version 0: Noise_XK with NIST P-384 plus Kyber1024 hybrid exchange on session init. pub(crate) const SESSION_PROTOCOL_VERSION: u8 = 0x00; diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index e397565e1..4af147e81 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -15,7 +15,7 @@ use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesGcm}; -use zerotier_crypto::hash::{SHA512, hmac_sha512_secret, hmac_sha512_secret256}; +use zerotier_crypto::hash::{hmac_sha512_secret, hmac_sha512_secret256, SHA512}; use zerotier_crypto::p384::{P384KeyPair, P384PublicKey}; use zerotier_crypto::secret::Secret; use zerotier_crypto::{random, secure_eq}; @@ -935,9 +935,10 @@ impl Context { let mut enc_start = ack_len; ack_len = append_to_slice(&mut ack, ack_len, alice_s_public_blob)?; - let mut gcm = AesGcm::new(&kbkdf256::( - &hmac_sha512_secret(noise_ck_es_ee.as_bytes(), hk.as_bytes()), - )); + let mut gcm = AesGcm::new(&kbkdf256::(&hmac_sha512_secret( + noise_ck_es_ee.as_bytes(), + hk.as_bytes(), + ))); gcm.reset_init_gcm(&reply_message_nonce); gcm.aad(&noise_h_next); gcm.crypt_in_place(&mut ack[enc_start..ack_len]); @@ -953,9 +954,7 @@ impl Context { enc_start = ack_len; ack_len = append_to_slice(&mut ack, ack_len, metadata)?; - let mut gcm = AesGcm::new(&kbkdf256::( - &noise_ck_es_ee_se_hk_psk, - )); + let mut gcm = AesGcm::new(&kbkdf256::(&noise_ck_es_ee_se_hk_psk)); gcm.reset_init_gcm(&reply_message_nonce); gcm.aad(&noise_h_next); gcm.crypt_in_place(&mut ack[enc_start..ack_len]); @@ -1041,9 +1040,10 @@ impl Context { let alice_static_public_blob = r.read_decrypt_auth( alice_static_public_blob_size, - kbkdf256::( - &hmac_sha512_secret(incoming.noise_ck_es_ee.as_bytes(), incoming.hk.as_bytes()), - ), + kbkdf256::(&hmac_sha512_secret( + incoming.noise_ck_es_ee.as_bytes(), + incoming.hk.as_bytes(), + )), &incoming.noise_h, &incoming_message_nonce, )?; @@ -1706,34 +1706,10 @@ fn mix_hash(h: &[u8; NOISE_HASHLEN], m: &[u8]) -> [u8; NOISE_HASHLEN] { /// These are the values we have assigned to the 5 variables involved in https://csrc.nist.gov/publications/detail/sp/800-108/final: /// K_in = key, i = 1u8, Label = b'Z'||b'T'||LABEL, Context = 0u8, L = 512u16 or 256u16 fn kbkdf512(key: &Secret) -> Secret { - hmac_sha512_secret( - key.as_bytes(), - &[ - 1, - b'Z', - b'T', - LABEL, - 0x00, - 0, - 2u8, - 0u8, - ], - ) + hmac_sha512_secret(key.as_bytes(), &[1, b'Z', b'T', LABEL, 0x00, 0, 2u8, 0u8]) } fn kbkdf256(key: &Secret) -> Secret<32> { - hmac_sha512_secret256( - key.as_bytes(), - &[ - 1, - b'Z', - b'T', - LABEL, - 0x00, - 0, - 1u8, - 0u8, - ], - ) + hmac_sha512_secret256(key.as_bytes(), &[1, b'Z', b'T', LABEL, 0x00, 0, 1u8, 0u8]) } fn prng32(mut x: u32) -> u32 { From e3268fa524e6502e6706780a5770e5103352f1ed Mon Sep 17 00:00:00 2001 From: mamoniot Date: Tue, 14 Mar 2023 18:19:21 -0400 Subject: [PATCH 06/25] fixed comment --- zssp/src/zssp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 4af147e81..84da62f3e 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -1703,7 +1703,7 @@ fn mix_hash(h: &[u8; NOISE_HASHLEN], m: &[u8]) -> [u8; NOISE_HASHLEN] { /// HMAC-SHA512 key derivation based on: https://csrc.nist.gov/publications/detail/sp/800-108/final (page 7) /// Cryptographically this isn't meaningfully different from HMAC(key, [label]) but this is how NIST rolls. -/// These are the values we have assigned to the 5 variables involved in https://csrc.nist.gov/publications/detail/sp/800-108/final: +/// These are the values we have assigned to the 5 variables involved in their KDF: /// K_in = key, i = 1u8, Label = b'Z'||b'T'||LABEL, Context = 0u8, L = 512u16 or 256u16 fn kbkdf512(key: &Secret) -> Secret { hmac_sha512_secret(key.as_bytes(), &[1, b'Z', b'T', LABEL, 0x00, 0, 2u8, 0u8]) From f6540e129a88cf003189de42eb4b1926f1cbca49 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Mon, 20 Mar 2023 15:26:15 -0400 Subject: [PATCH 07/25] added arc_pool to tetanus --- utils/src/arc_pool.rs | 657 ++++++++++++++++++++++++++++++++++++++++++ utils/src/lib.rs | 2 + 2 files changed, 659 insertions(+) create mode 100644 utils/src/arc_pool.rs diff --git a/utils/src/arc_pool.rs b/utils/src/arc_pool.rs new file mode 100644 index 000000000..1d5e71e1a --- /dev/null +++ b/utils/src/arc_pool.rs @@ -0,0 +1,657 @@ +use std::sync::{Mutex, RwLock, RwLockReadGuard, atomic::{AtomicU32, Ordering, AtomicPtr}}; +use std::mem::{self, MaybeUninit, ManuallyDrop}; +use std::ptr::{self, NonNull}; +use std::marker::PhantomData; +use std::num::NonZeroU64; +use std::ops::Deref; + +const DEFAULT_L: usize = 64; + +union SlotState { + empty_next: *mut Slot, + full_obj: ManuallyDrop, +} +struct Slot { + obj: SlotState, + free_lock: RwLock<()>, + ref_count: AtomicU32, + uid: u64, +} + +struct PoolMem { + mem: [MaybeUninit>; L], + pre: *mut PoolMem, +} + +/// A generic, *thread-safe*, fixed-sized memory allocator for instances of `T`. +/// New instances of `T` are packed together into arrays of size `L`, and allocated in bulk as one memory arena from the global allocator. +/// Arenas from the global allocator are not deallocated until the pool is dropped, and are re-used as instances of `T` are allocated and freed. +/// +/// This specific datastructure also supports generational indexing, which means that an arbitrary number of non-owning references to allocated instances of `T` can be generated safely. These references can outlive the underlying `T` they reference, and will safely report upon dereference that the original underlying `T` is gone. +/// +/// Atomic reference counting is also implemented allowing for exceedingly complex models of shared ownership. Multiple copies of both strong and weak references to the underlying `T` can be generated that are all memory safe and borrow-checked. +/// +/// Allocating from a pool results in very little internal and external fragmentation in the global heap, thus saving significant amounts of memory from being used by one's program. Pools also allocate memory significantly faster on average than the global allocator. This specific pool implementation supports guaranteed constant time `alloc` and `free`. +pub struct Pool (Mutex<(*mut Slot, u64, *mut PoolMem, usize)>); +unsafe impl Send for Pool {} +unsafe impl Sync for Pool {} + +impl Pool { + pub const DEFAULT_L: usize = DEFAULT_L; + + /// Creates a new `Pool` with packing length `L`. Packing length determines the number of instances of `T` that will fit in a page before it becomes full. Once all pages in a `Pool` are full a new page is allocated from the LocalNode allocator. Larger values of `L` are generally faster, but the returns are diminishing and vary by platform. + /// + /// A `Pool` cannot be interacted with directly, it requires a `impl StaticPool for Pool` implementation. See the `static_pool!` macro for automatically generated trait implementation. + #[inline] + pub const fn new() -> Self { + Pool (Mutex::new((ptr::null_mut(), 1, ptr::null_mut(), usize::MAX))) + } + + + #[inline(always)] + fn create_arr() -> [MaybeUninit>; L] { + unsafe { MaybeUninit::<[MaybeUninit>; L]>::uninit().assume_init() } + } + + /// Allocates uninitialized memory for an instance `T`. The returned pointer points to this memory. It is undefined what will be contained in this memory, it must be initiallized before being used. This pointer must be manually freed from the pool using `Pool::free_ptr` before being dropped, otherwise its memory will be leaked. If the pool is dropped before this pointer is freed, the destructor of `T` will not be run and this pointer will point to invalid memory. + unsafe fn alloc_ptr(&self, obj: T) -> NonNull> { + let mut mutex = self.0.lock().unwrap(); + let (mut first_free, uid, mut head_arena, mut head_size) = *mutex; + + let slot_ptr = if let Some(mut slot_ptr) = NonNull::new(first_free) { + let slot = slot_ptr.as_mut(); + let _announce_free = slot.free_lock.write().unwrap(); + first_free = slot.obj.empty_next; + slot.ref_count = AtomicU32::new(1); + slot.uid = uid; + slot.obj.full_obj = ManuallyDrop::new(obj); + slot_ptr + } else { + if head_size >= L { + let new = Box::leak(Box::new(PoolMem { + pre: head_arena, + mem: Self::create_arr(), + })); + head_arena = new; + head_size = 0; + } + let slot = Slot { + obj: SlotState {full_obj: ManuallyDrop::new(obj)}, + free_lock: RwLock::new(()), + ref_count: AtomicU32::new(1), + uid, + }; + let slot_ptr = &mut (*head_arena).mem[head_size]; + let slot_ptr = NonNull::new_unchecked(slot_ptr.write(slot)); + head_size += 1; + // We do not have to hold the free lock since we know this slot has never been touched before and nothing external references it + slot_ptr + }; + + *mutex = (first_free, uid.wrapping_add(1), head_arena, head_size); + slot_ptr + } + /// Frees memory allocated from the pool by `Pool::alloc_ptr`. This must be called only once on only pointers returned by `Pool::alloc_ptr` from the same pool. Once memory is freed the content of the memory is undefined, it should not be read or written. + /// + /// `drop` will be called on the `T` pointed to, be sure it has not been called already. + unsafe fn free_ptr(&self, mut slot_ptr: NonNull>) { + let slot = slot_ptr.as_mut(); + let _announce_free = slot.free_lock.write().unwrap(); + slot.uid = 0; + ManuallyDrop::::drop(&mut slot.obj.full_obj); + //linked-list insert + let mut mutex = self.0.lock().unwrap(); + + slot.obj.empty_next = mutex.0; + mutex.0 = slot_ptr.as_ptr(); + } +} +impl Drop for Pool { + fn drop(&mut self) { + let mutex = self.0.lock().unwrap(); + let (_, _, mut head_arena, _) = *mutex; + unsafe { + while !head_arena.is_null() { + let mem = Box::from_raw(head_arena); + head_arena = mem.pre; + drop(mem); + } + } + drop(mutex); + } +} + +pub trait StaticPool { + + /// Must return a pointer to an instance of a `Pool` with a static lifetime. That pointer must be cast to a `*const ()` to make the borrow-checker happy. + /// + /// **Safety**: The returned pointer must have originally been a `&'static Pool` reference. So it must have had a matching `T` and `L` and it must have the static lifetime. + /// + /// In order to borrow-split allocations from a `Pool`, we need to force the borrow-checker to not associate the lifetime of an instance of `T` with the lifetime of the pool. Otherwise the borrow-checker would require every allocated `T` to have the `'static` lifetime, to match the pool's lifetime. + /// The simplest way I have found to do this is to return the pointer to the static pool as an anonymous, lifetimeless `*const ()`. This introduces unnecessary safety concerns surrounding pointer casting unfortunately. If there is a better way to borrow-split from a pool I will gladly implement it. + unsafe fn get_static_pool() -> *const (); + + /// Allocates memory for an instance `T` and puts its pointer behind a memory-safe Arc. This `PoolArc` automatically frees itself on drop, and will cause the borrow checker to complain if you attempt to drop the pool before you drop this box. + /// + /// This `PoolArc` supports the ability to generate weak, non-owning references to the allocated `T`. + #[inline(always)] + fn alloc(obj: T) -> PoolArc where Self: Sized { + unsafe { + PoolArc { + ptr: (*Self::get_static_pool().cast::>()).alloc_ptr(obj), + _p: PhantomData + } + } + } +} + + +/// A multithreading lock guard that prevents another thread from freeing the underlying `T` while it is held. It does not prevent other threads from accessing the underlying `T`. +/// +/// If the same thread that holds this guard attempts to free `T` before dropping the guard, it will deadlock. +pub struct PoolGuard<'a, T> (RwLockReadGuard<'a, ()>, &'a T); +impl<'a, T> Deref for PoolGuard<'a, T> { + type Target = T; + #[inline] + fn deref(&self) -> &Self::Target { + &*self.1 + } +} + + +/// A rust-style RAII wrapper that drops and frees memory allocated from a pool automatically, the same as an `Arc`. This will run the destructor of `T` in place within the pool before freeing it, correctly maintaining the invariants that the borrow checker and rust compiler expect of generic types. +pub struct PoolArc, const L: usize = DEFAULT_L> { + ptr: NonNull>, + _p: PhantomData<*const OriginPool>, +} + +impl, const L: usize> PoolArc { + /// Obtain a non-owning reference to the `T` contained in this `PoolArc`. This reference has the special property that the underlying `T` can be dropped from the pool while neither making this reference invalid or unsafe nor leaking the memory of `T`. Instead attempts to `grab` the reference will safely return `None`. + #[inline] + pub fn downgrade(&self) -> PoolWeakRef { + unsafe { + // Since this is a Arc we know for certain the object has not been freed, so we don't have to hold the free lock + PoolWeakRef { + ptr: self.ptr, + uid: NonZeroU64::new_unchecked(self.ptr.as_ref().uid), + } + } + } + /// Returns a number that uniquely identifies this allocated `T` within this pool. No other instance of `T` may have this uid. + pub fn uid(&self) -> NonZeroU64 { + unsafe { + NonZeroU64::new_unchecked(self.ptr.as_ref().uid) + } + } +} +unsafe impl, const L: usize> Send for PoolArc where T: Send {} +unsafe impl, const L: usize> Sync for PoolArc where T: Sync {} + +impl, const L: usize> Deref for PoolArc { + type Target = T; + #[inline] + fn deref(&self) -> &Self::Target { + unsafe { + &self.ptr.as_ref().obj.full_obj + } + } +} +impl, const L: usize> Clone for PoolArc { + fn clone(&self) -> Self { + unsafe { + self.ptr.as_ref().ref_count.fetch_add(1, Ordering::Relaxed); + } + Self { + ptr: self.ptr, + _p: PhantomData, + } + } +} +impl, const L: usize> Drop for PoolArc { + #[inline] + fn drop(&mut self) { + unsafe { + if self.ptr.as_ref().ref_count.fetch_sub(1, Ordering::AcqRel) == 1 { + (*OriginPool::get_static_pool().cast::>()).free_ptr(self.ptr); + } + } + } +} + + +/// A non-owning reference to a `T` allocated by a pool. This reference has the special property that the underlying `T` can be dropped from the pool while neither making this reference invalid nor leaking the memory of `T`. Instead attempts to `grab` this reference will safely return `None` if the underlying `T` has been freed by any thread. +/// +/// Due to there thread safety and low overhead a `PoolWeakRef` implements clone and copy. +/// +/// The lifetime of this reference is tied to the lifetime of the pool it came from, because if it were allowed to live longer than its origin pool, it would no longer be safe to dereference and would most likely segfault. Instead the borrow-checker will enforce that this reference has a shorter lifetime that its origin pool. +pub struct PoolWeakRef { + /// A number that uniquely identifies this allocated `T` within this pool. No other instance of `T` may have this uid. This value is read-only. + pub uid: NonZeroU64, + ptr: NonNull>, +} + +impl PoolWeakRef { + /// Obtains a lock that allows the `T` contained in this `PoolWeakRef` to be dereferenced in a thread-safe manner. This lock does not prevent other threads from accessing `T` at the same time, so `T` ought to use interior mutability if it needs to be mutated in a thread-safe way. What this lock does guarantee is that `T` cannot be destructed and freed while it is being held. + /// + /// Do not attempt from within the same thread to drop the `PoolArc` that owns this `T` before dropping this lock, or else the thread will deadlock. Rust makes this quite hard to do accidentally but it's not strictly impossible. + #[inline] + pub fn grab<'b>(&self) -> Option> { + unsafe { + let slot = self.ptr.as_ref(); + let prevent_free_lock = slot.free_lock.read().unwrap(); + if slot.uid == self.uid.get() { + Some(PoolGuard(prevent_free_lock, &slot.obj.full_obj)) + } else { + None + } + } + } +} +unsafe impl Send for PoolWeakRef where T: Send {} +unsafe impl Sync for PoolWeakRef where T: Sync {} + +impl Clone for PoolWeakRef { + fn clone(&self) -> Self { + Self { + uid: self.uid, + ptr: self.ptr, + } + } +} +impl Copy for PoolWeakRef {} + + +pub struct PoolArcSwap, const L: usize = DEFAULT_L> { + ptr: AtomicPtr>, + reads: AtomicU32, + _p: PhantomData<*const OriginPool>, +} +impl, const L: usize> PoolArcSwap { + pub fn new(mut arc: PoolArc) -> Self { + unsafe { + let ret = Self { + ptr: AtomicPtr::new(arc.ptr.as_mut()), + reads: AtomicU32::new(0), + _p: arc._p, + }; + // Suppress reference decrement on new + mem::forget(arc); + ret + } + } + + pub fn swap(&self, arc: PoolArc) -> PoolArc { + unsafe { + let pre_ptr = self.ptr.swap(arc.ptr.as_ptr(), Ordering::Relaxed); + + while self.reads.load(Ordering::Acquire) > 0 { + std::hint::spin_loop() + } + + mem::forget(arc); + PoolArc { + ptr: NonNull::new_unchecked(pre_ptr), + _p: self._p, + } + } + } + + pub fn load(&self) -> PoolArc { + unsafe { + self.reads.fetch_add(1, Ordering::Acquire); + let ptr = self.ptr.load(Ordering::Relaxed); + (*ptr).ref_count.fetch_add(1, Ordering::Relaxed); + self.reads.fetch_sub(1, Ordering::Release); + PoolArc { + ptr: NonNull::new_unchecked(ptr), + _p: self._p, + } + } + } +} +unsafe impl, const L: usize> Send for PoolArcSwap where T: Send {} +unsafe impl, const L: usize> Sync for PoolArcSwap where T: Sync {} + +impl, const L: usize> Drop for PoolArcSwap { + #[inline] + fn drop(&mut self) { + unsafe { + let pre = self.ptr.load(Ordering::SeqCst); + PoolArc { + _p: self._p, + ptr: NonNull::new_unchecked(pre), + }; + } + } +} + + +pub struct PoolArcSwapRw, const L: usize = DEFAULT_L> { + ptr: RwLock>>, + _p: PhantomData<*const OriginPool>, +} + +impl, const L: usize> PoolArcSwapRw { + pub fn new(arc: PoolArc) -> Self { + let ret = Self { + ptr: RwLock::new(arc.ptr), + _p: arc._p, + }; + mem::forget(arc); + ret + } + + pub fn swap(&self, arc: PoolArc) -> PoolArc { + let mut w = self.ptr.write().unwrap(); + let pre = PoolArc { + ptr: *w, + _p: self._p, + }; + *w = arc.ptr; + mem::forget(arc); + pre + } + + pub fn load(&self) -> PoolArc { + let r = self.ptr.read().unwrap(); + unsafe { + r.as_ref().ref_count.fetch_add(1, Ordering::Relaxed); + } + let pre = PoolArc { + ptr: *r, + _p: self._p, + }; + pre + } +} +impl, const L: usize> Drop for PoolArcSwapRw { + #[inline] + fn drop(&mut self) { + let w = self.ptr.write().unwrap(); + PoolArc { + ptr: *w, + _p: self._p, + }; + } +} +unsafe impl, const L: usize> Send for PoolArcSwapRw where T: Send {} +unsafe impl, const L: usize> Sync for PoolArcSwapRw where T: Sync {} + +/// Automatically generates valid implementations of `StaticPool` onto a chosen identifier, allowing this module to allocate instances of `T` with `alloc`. Users have to generate implementations clientside because rust does not allow for generic globals. +/// +/// # Example +/// ``` +/// use zerotier_utils::arc_pool::{Pool, StaticPool, static_pool}; +/// +/// static_pool!(StaticPool MyPools { +/// Pool, Pool<&u32, 12> +/// }); +/// +/// let object = 1u32; +/// let arc_object = MyPools::alloc(object); +/// let arc_ref = MyPools::alloc(&object); +/// +/// assert_eq!(*arc_object, **arc_ref); +/// ``` +#[macro_export] +macro_rules! __static_pool__ { + ($m:ident $s:ident { $($($p:ident)::+<$t:ty$(, $l:tt)?>),+ $(,)?}) => { + struct $s {} + $( + impl $m<$t$(, $l)?> for $s { + #[inline(always)] + unsafe fn get_static_pool() -> *const () { + static POOL: $($p)::+<$t$(, $l)?> = $($p)::+::new(); + (&POOL as *const $($p)::+<$t$(, $l)?>).cast() + } + } + )* + }; + ($m:ident::$n:ident $s:ident { $($($p:ident)::+<$t:ty$(, $l:tt)?>),+ $(,)?}) => { + struct $s {} + $( + impl $m::$n<$t$(, $l)?> for $s { + #[inline(always)] + unsafe fn get_static_pool() -> *const () { + static POOL: $($p)::+<$t$(, $l)?> = $($p)::+::new(); + (&POOL as *const $($p)::+<$t$(, $l)?>).cast() + } + } + )* + }; +} +pub use __static_pool__ as static_pool; + +#[cfg(test)] +mod tests { + use std::{thread, sync::{Arc, atomic::AtomicU64}}; + use super::*; + + + fn rand(r: &mut u32) -> u32 { + /* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */ + *r ^= *r << 13; + *r ^= *r >> 17; + *r ^= *r << 5; + *r + } + const fn prob(p: u64) -> u32 { + (p*(u32::MAX as u64)/100) as u32 + } + fn rand_idx<'a, T>(v: &'a [T], r: &mut u32) -> Option<&'a T> { + if v.len() > 0 { + Some(&v[(rand(r) as usize)%v.len()]) + } else { + None + } + } + fn rand_i<'a, T>(v: &'a [T], r: &mut u32) -> Option { + if v.len() > 0 { + Some((rand(r) as usize)%v.len()) + } else { + None + } + } + + struct Item { + a: u32, + count: &'static AtomicU64, + b: u32, + } + impl Item { + fn new(r: u32, count: &'static AtomicU64) -> Item { + count.fetch_add(1, Ordering::Relaxed); + Item { + a: r, + count, + b: r, + } + } + fn check(&self, id: u32) { + assert_eq!(self.a, self.b); + assert_eq!(self.a, id); + } + } + impl Drop for Item { + fn drop(&mut self) { + let _a = self.count.fetch_sub(1, Ordering::Relaxed); + assert_eq!(self.a, self.b); + } + } + + const POOL_U32_LEN: usize = (5*12)<<2; + static_pool!(StaticPool TestPools { + Pool, Pool + }); + + #[test] + fn usage() { + + let num1 = TestPools::alloc(1u32); + let num2 = TestPools::alloc(2u32); + let num3 = TestPools::alloc(3u32); + let num4 = TestPools::alloc(4u32); + let num2_weak = num2.downgrade(); + + assert_eq!(*num2_weak.grab().unwrap(), 2); + drop(num2); + + assert_eq!(*num1, 1); + assert_eq!(*num3, 3); + assert_eq!(*num4, 4); + assert!(num2_weak.grab().is_none()); + } + #[test] + fn single_thread() { + + let mut history = Vec::new(); + + let num1 = TestPools::alloc(1u32); + let num2 = TestPools::alloc(2u32); + let num3 = TestPools::alloc(3u32); + let num4 = TestPools::alloc(4u32); + let num2_weak = num2.downgrade(); + + for i in 0..1000 { + history.push(TestPools::alloc(i as u32)); + } + for i in 0..100 { + let arc = history.remove((i*10)%history.len()); + assert!(*arc < 1000); + } + for i in 0..1000 { + history.push(TestPools::alloc(i as u32)); + } + + assert_eq!(*num2_weak.grab().unwrap(), 2); + drop(num2); + + assert_eq!(*num1, 1); + assert_eq!(*num3, 3); + assert_eq!(*num4, 4); + assert!(num2_weak.grab().is_none()); + } + + #[test] + fn multi_thread() { + const N: usize = 123456; + static COUNT: AtomicU64 = AtomicU64::new(0); + + let mut joins = Vec::new(); + for i in 0..32 { + joins.push(thread::spawn(move || { + let r = &mut (i + 1234); + + let mut items_dup = Vec::new(); + let mut items = Vec::new(); + for _ in 0..N { + let p = rand(r); + if p < prob(30) { + let id = rand(r); + let s = TestPools::alloc(Item::new(id, &COUNT)); + items.push((id, s.clone(), s.downgrade())); + s.check(id); + } else if p < prob(60) { + if let Some((id, s, w)) = rand_idx(&items, r) { + items_dup.push((*id, s.clone(), w.clone())); + s.check(*id); + } + } else if p < prob(80) { + if let Some(i) = rand_i(&items, r) { + let (id, s, w) = items.swap_remove(i); + w.grab().unwrap().check(id); + s.check(id); + } + } else if p < prob(100) { + if let Some(i) = rand_i(&items_dup, r) { + let (id, s, w) = items_dup.swap_remove(i); + w.grab().unwrap().check(id); + s.check(id); + } + } + } + for (id, s, w) in items_dup { + s.check(id); + w.grab().unwrap().check(id); + } + for (id, s, w) in items { + s.check(id); + w.grab().unwrap().check(id); + drop(s); + assert!(w.grab().is_none()) + } + })); + } + for j in joins { + j.join().unwrap(); + } + assert_eq!(COUNT.load(Ordering::Relaxed), 0); + } + + #[test] + fn multi_thread_swap() { + const N: usize = 1234; + static COUNT: AtomicU64 = AtomicU64::new(0); + + let s = Arc::new(PoolArcSwap::new(TestPools::alloc(Item::new(0, &COUNT)))); + + for _ in 0..1234 { + let mut joins = Vec::new(); + for _ in 0..8 { + let swaps = s.clone(); + joins.push(thread::spawn(move || { + let r = &mut 1474; + let mut new = TestPools::alloc(Item::new(rand(r), &COUNT)); + for _ in 0..N { + new = swaps.swap(new); + } + })); + } + for j in joins { + j.join().unwrap(); + } + } + drop(s); + assert_eq!(COUNT.load(Ordering::Relaxed), 0); + } + + #[test] + fn multi_thread_swap_load() { + const N: usize = 123456; + static COUNT: AtomicU64 = AtomicU64::new(0); + + let s: Arc<[_; 8]> = Arc::new(std::array::from_fn(|i| PoolArcSwap::new(TestPools::alloc(Item::new(i as u32, &COUNT))))); + + let mut joins = Vec::new(); + + for i in 0..4 { + let swaps = s.clone(); + joins.push(thread::spawn(move || { + let r = &mut (i + 2783); + for _ in 0..N { + if let Some(s) = rand_idx(&swaps[..], r) { + let new = TestPools::alloc(Item::new(rand(r), &COUNT)); + let _a = s.swap(new); + } + } + })); + } + for i in 0..28 { + let swaps = s.clone(); + joins.push(thread::spawn(move || { + let r = &mut (i + 4136); + for _ in 0..N { + if let Some(s) = rand_idx(&swaps[..], r) { + let _a = s.load(); + assert_eq!(_a.a, _a.b); + } + } + })); + } + for j in joins { + j.join().unwrap(); + } + drop(s); + assert_eq!(COUNT.load(Ordering::Relaxed), 0); + } +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 194f7af4e..30d87df93 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -29,6 +29,8 @@ pub mod reaper; pub mod ringbuffer; pub mod sync; pub mod varint; +pub mod arc_pool; + #[cfg(feature = "tokio")] pub use tokio; From 7ec194a6d1a3a824020e8fa1e74569024f30248b Mon Sep 17 00:00:00 2001 From: mamoniot Date: Mon, 20 Mar 2023 15:29:02 -0400 Subject: [PATCH 08/25] ran cargo fmt --- utils/src/arc_pool.rs | 115 ++++++++++++++---------------------------- utils/src/lib.rs | 3 +- 2 files changed, 39 insertions(+), 79 deletions(-) diff --git a/utils/src/arc_pool.rs b/utils/src/arc_pool.rs index 1d5e71e1a..d9eb269e5 100644 --- a/utils/src/arc_pool.rs +++ b/utils/src/arc_pool.rs @@ -1,9 +1,12 @@ -use std::sync::{Mutex, RwLock, RwLockReadGuard, atomic::{AtomicU32, Ordering, AtomicPtr}}; -use std::mem::{self, MaybeUninit, ManuallyDrop}; -use std::ptr::{self, NonNull}; use std::marker::PhantomData; +use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::num::NonZeroU64; use std::ops::Deref; +use std::ptr::{self, NonNull}; +use std::sync::{ + atomic::{AtomicPtr, AtomicU32, Ordering}, + Mutex, RwLock, RwLockReadGuard, +}; const DEFAULT_L: usize = 64; @@ -32,7 +35,7 @@ struct PoolMem { /// Atomic reference counting is also implemented allowing for exceedingly complex models of shared ownership. Multiple copies of both strong and weak references to the underlying `T` can be generated that are all memory safe and borrow-checked. /// /// Allocating from a pool results in very little internal and external fragmentation in the global heap, thus saving significant amounts of memory from being used by one's program. Pools also allocate memory significantly faster on average than the global allocator. This specific pool implementation supports guaranteed constant time `alloc` and `free`. -pub struct Pool (Mutex<(*mut Slot, u64, *mut PoolMem, usize)>); +pub struct Pool(Mutex<(*mut Slot, u64, *mut PoolMem, usize)>); unsafe impl Send for Pool {} unsafe impl Sync for Pool {} @@ -44,10 +47,9 @@ impl Pool { /// A `Pool` cannot be interacted with directly, it requires a `impl StaticPool for Pool` implementation. See the `static_pool!` macro for automatically generated trait implementation. #[inline] pub const fn new() -> Self { - Pool (Mutex::new((ptr::null_mut(), 1, ptr::null_mut(), usize::MAX))) + Pool(Mutex::new((ptr::null_mut(), 1, ptr::null_mut(), usize::MAX))) } - #[inline(always)] fn create_arr() -> [MaybeUninit>; L] { unsafe { MaybeUninit::<[MaybeUninit>; L]>::uninit().assume_init() } @@ -68,15 +70,12 @@ impl Pool { slot_ptr } else { if head_size >= L { - let new = Box::leak(Box::new(PoolMem { - pre: head_arena, - mem: Self::create_arr(), - })); + let new = Box::leak(Box::new(PoolMem { pre: head_arena, mem: Self::create_arr() })); head_arena = new; head_size = 0; } let slot = Slot { - obj: SlotState {full_obj: ManuallyDrop::new(obj)}, + obj: SlotState { full_obj: ManuallyDrop::new(obj) }, free_lock: RwLock::new(()), ref_count: AtomicU32::new(1), uid, @@ -122,7 +121,6 @@ impl Drop for Pool { } pub trait StaticPool { - /// Must return a pointer to an instance of a `Pool` with a static lifetime. That pointer must be cast to a `*const ()` to make the borrow-checker happy. /// /// **Safety**: The returned pointer must have originally been a `&'static Pool` reference. So it must have had a matching `T` and `L` and it must have the static lifetime. @@ -135,21 +133,23 @@ pub trait StaticPool { /// /// This `PoolArc` supports the ability to generate weak, non-owning references to the allocated `T`. #[inline(always)] - fn alloc(obj: T) -> PoolArc where Self: Sized { + fn alloc(obj: T) -> PoolArc + where + Self: Sized, + { unsafe { PoolArc { ptr: (*Self::get_static_pool().cast::>()).alloc_ptr(obj), - _p: PhantomData + _p: PhantomData, } } } } - /// A multithreading lock guard that prevents another thread from freeing the underlying `T` while it is held. It does not prevent other threads from accessing the underlying `T`. /// /// If the same thread that holds this guard attempts to free `T` before dropping the guard, it will deadlock. -pub struct PoolGuard<'a, T> (RwLockReadGuard<'a, ()>, &'a T); +pub struct PoolGuard<'a, T>(RwLockReadGuard<'a, ()>, &'a T); impl<'a, T> Deref for PoolGuard<'a, T> { type Target = T; #[inline] @@ -158,7 +158,6 @@ impl<'a, T> Deref for PoolGuard<'a, T> { } } - /// A rust-style RAII wrapper that drops and frees memory allocated from a pool automatically, the same as an `Arc`. This will run the destructor of `T` in place within the pool before freeing it, correctly maintaining the invariants that the borrow checker and rust compiler expect of generic types. pub struct PoolArc, const L: usize = DEFAULT_L> { ptr: NonNull>, @@ -179,9 +178,7 @@ impl, const L: usize> PoolArc } /// Returns a number that uniquely identifies this allocated `T` within this pool. No other instance of `T` may have this uid. pub fn uid(&self) -> NonZeroU64 { - unsafe { - NonZeroU64::new_unchecked(self.ptr.as_ref().uid) - } + unsafe { NonZeroU64::new_unchecked(self.ptr.as_ref().uid) } } } unsafe impl, const L: usize> Send for PoolArc where T: Send {} @@ -191,9 +188,7 @@ impl, const L: usize> Deref for PoolArc &Self::Target { - unsafe { - &self.ptr.as_ref().obj.full_obj - } + unsafe { &self.ptr.as_ref().obj.full_obj } } } impl, const L: usize> Clone for PoolArc { @@ -201,10 +196,7 @@ impl, const L: usize> Clone for PoolArc, const L: usize> Drop for PoolArc { @@ -218,7 +210,6 @@ impl, const L: usize> Drop for PoolArc Sync for PoolWeakRef where T: Sync {} impl Clone for PoolWeakRef { fn clone(&self) -> Self { - Self { - uid: self.uid, - ptr: self.ptr, - } + Self { uid: self.uid, ptr: self.ptr } } } impl Copy for PoolWeakRef {} - pub struct PoolArcSwap, const L: usize = DEFAULT_L> { ptr: AtomicPtr>, reads: AtomicU32, @@ -289,10 +276,7 @@ impl, const L: usize> PoolArcSwap, const L: usize> PoolArcSwap, const L: usize> Drop for PoolArcSwap, const L: usize = DEFAULT_L> { ptr: RwLock>>, _p: PhantomData<*const OriginPool>, @@ -333,20 +310,14 @@ pub struct PoolArcSwapRw, const L: usize = DEFAU impl, const L: usize> PoolArcSwapRw { pub fn new(arc: PoolArc) -> Self { - let ret = Self { - ptr: RwLock::new(arc.ptr), - _p: arc._p, - }; + let ret = Self { ptr: RwLock::new(arc.ptr), _p: arc._p }; mem::forget(arc); ret } pub fn swap(&self, arc: PoolArc) -> PoolArc { let mut w = self.ptr.write().unwrap(); - let pre = PoolArc { - ptr: *w, - _p: self._p, - }; + let pre = PoolArc { ptr: *w, _p: self._p }; *w = arc.ptr; mem::forget(arc); pre @@ -357,10 +328,7 @@ impl, const L: usize> PoolArcSwapRw, const L: usize> Drop for PoolArcSwapRw, const L: usize> Send for PoolArcSwapRw where T: Send {} @@ -424,9 +389,11 @@ pub use __static_pool__ as static_pool; #[cfg(test)] mod tests { - use std::{thread, sync::{Arc, atomic::AtomicU64}}; use super::*; - + use std::{ + sync::{atomic::AtomicU64, Arc}, + thread, + }; fn rand(r: &mut u32) -> u32 { /* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */ @@ -436,18 +403,18 @@ mod tests { *r } const fn prob(p: u64) -> u32 { - (p*(u32::MAX as u64)/100) as u32 + (p * (u32::MAX as u64) / 100) as u32 } fn rand_idx<'a, T>(v: &'a [T], r: &mut u32) -> Option<&'a T> { if v.len() > 0 { - Some(&v[(rand(r) as usize)%v.len()]) + Some(&v[(rand(r) as usize) % v.len()]) } else { None } } fn rand_i<'a, T>(v: &'a [T], r: &mut u32) -> Option { if v.len() > 0 { - Some((rand(r) as usize)%v.len()) + Some((rand(r) as usize) % v.len()) } else { None } @@ -461,11 +428,7 @@ mod tests { impl Item { fn new(r: u32, count: &'static AtomicU64) -> Item { count.fetch_add(1, Ordering::Relaxed); - Item { - a: r, - count, - b: r, - } + Item { a: r, count, b: r } } fn check(&self, id: u32) { assert_eq!(self.a, self.b); @@ -479,14 +442,13 @@ mod tests { } } - const POOL_U32_LEN: usize = (5*12)<<2; + const POOL_U32_LEN: usize = (5 * 12) << 2; static_pool!(StaticPool TestPools { Pool, Pool }); #[test] fn usage() { - let num1 = TestPools::alloc(1u32); let num2 = TestPools::alloc(2u32); let num3 = TestPools::alloc(3u32); @@ -503,7 +465,6 @@ mod tests { } #[test] fn single_thread() { - let mut history = Vec::new(); let num1 = TestPools::alloc(1u32); @@ -516,7 +477,7 @@ mod tests { history.push(TestPools::alloc(i as u32)); } for i in 0..100 { - let arc = history.remove((i*10)%history.len()); + let arc = history.remove((i * 10) % history.len()); assert!(*arc < 1000); } for i in 0..1000 { @@ -645,7 +606,7 @@ mod tests { let _a = s.load(); assert_eq!(_a.a, _a.b); } - } + } })); } for j in joins { diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 30d87df93..a3be5577b 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -6,6 +6,7 @@ * https://www.zerotier.com/ */ +pub mod arc_pool; pub mod arrayvec; pub mod base64; pub mod blob; @@ -29,8 +30,6 @@ pub mod reaper; pub mod ringbuffer; pub mod sync; pub mod varint; -pub mod arc_pool; - #[cfg(feature = "tokio")] pub use tokio; From b5d8290df7a57364237a13f504834befe0bc4b18 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Mon, 20 Mar 2023 17:23:29 -0400 Subject: [PATCH 09/25] fixed aes_gmac_siv --- crypto/src/aes_gmac_siv_openssl.rs | 161 +++++++++++++---------------- 1 file changed, 71 insertions(+), 90 deletions(-) diff --git a/crypto/src/aes_gmac_siv_openssl.rs b/crypto/src/aes_gmac_siv_openssl.rs index f2c81fabf..7350e3df3 100644 --- a/crypto/src/aes_gmac_siv_openssl.rs +++ b/crypto/src/aes_gmac_siv_openssl.rs @@ -9,37 +9,72 @@ use crate::{cipher_ctx::CipherCtx, ZEROES}; pub struct AesGmacSiv { tag: [u8; 16], tmp: [u8; 16], - k0: Vec, - k1: Vec, - ctr: Option, - gmac: Option, + ecb_enc: CipherCtx, + ecb_dec: CipherCtx, + ctr: CipherCtx, + gmac: CipherCtx, } impl AesGmacSiv { /// Create a new keyed instance of AES-GMAC-SIV /// The key may be of size 16, 24, or 32 bytes (128, 192, or 256 bits). Any other size will panic. pub fn new(k0: &[u8], k1: &[u8]) -> Self { - if k0.len() != 32 && k0.len() != 24 && k0.len() != 16 { - panic!("AES supports 128, 192, or 256 bits keys"); + let gmac = CipherCtx::new().unwrap(); + unsafe { + let t = match k0.len() { + 16 => ffi::EVP_aes_128_gcm(), + 24 => ffi::EVP_aes_192_gcm(), + 32 => ffi::EVP_aes_256_gcm(), + _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), + }; + gmac.cipher_init::(t, k0.as_ptr(), ptr::null_mut()).unwrap(); } - if k1.len() != k0.len() { - panic!("k0 and k1 must be of the same size"); + let ctr = CipherCtx::new().unwrap(); + unsafe { + let t = match k1.len() { + 16 => ffi::EVP_aes_128_ctr(), + 24 => ffi::EVP_aes_192_ctr(), + 32 => ffi::EVP_aes_256_ctr(), + _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), + }; + ctr.cipher_init::(t, k1.as_ptr(), ptr::null_mut()).unwrap(); } + let ecb_enc = CipherCtx::new().unwrap(); + unsafe { + let t = match k1.len() { + 16 => ffi::EVP_aes_128_ecb(), + 24 => ffi::EVP_aes_192_ecb(), + 32 => ffi::EVP_aes_256_ecb(), + _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), + }; + ecb_enc.cipher_init::(t, k1.as_ptr(), ptr::null_mut()).unwrap(); + ffi::EVP_CIPHER_CTX_set_padding(ecb_enc.as_ptr(), 0); + } + let ecb_dec = CipherCtx::new().unwrap(); + unsafe { + let t = match k1.len() { + 16 => ffi::EVP_aes_128_ecb(), + 24 => ffi::EVP_aes_192_ecb(), + 32 => ffi::EVP_aes_256_ecb(), + _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), + }; + ecb_dec.cipher_init::(t, k1.as_ptr(), ptr::null_mut()).unwrap(); + ffi::EVP_CIPHER_CTX_set_padding(ecb_dec.as_ptr(), 0); + } + AesGmacSiv { tag: [0_u8; 16], tmp: [0_u8; 16], - k0: k0.to_vec(), - k1: k1.to_vec(), - ctr: None, - gmac: None, + ecb_dec, + ecb_enc, + ctr, + gmac, } } /// Reset to prepare for another encrypt or decrypt operation. #[inline(always)] pub fn reset(&mut self) { - let _ = self.ctr.take(); - let _ = self.gmac.take(); } /// Initialize for encryption. @@ -47,31 +82,21 @@ impl AesGmacSiv { pub fn encrypt_init(&mut self, iv: &[u8]) { self.tag[0..8].copy_from_slice(iv); self.tag[8..12].fill(0); - - let ctx = CipherCtx::new().unwrap(); unsafe { - let t = match self.k0.len() { - 16 => ffi::EVP_aes_128_gcm(), - 24 => ffi::EVP_aes_192_gcm(), - 32 => ffi::EVP_aes_256_gcm(), - _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), - }; - ctx.cipher_init::(t, self.k0.as_mut_ptr(), self.tag[0..12].as_ptr()).unwrap(); + self.gmac.cipher_init::(ptr::null_mut(), ptr::null_mut(), self.tag[0..12].as_ptr()).unwrap(); } - let _ = self.gmac.replace(ctx); } /// Set additional authenticated data (data to be authenticated but not encrypted). /// This can currently only be called once. Multiple calls will result in corrupt data. #[inline(always)] pub fn encrypt_set_aad(&mut self, data: &[u8]) { - let gmac = self.gmac.as_mut().unwrap(); unsafe { - gmac.update::(data, ptr::null_mut()).unwrap(); + self.gmac.update::(data, ptr::null_mut()).unwrap(); let mut pad = data.len() & 0xf; if pad != 0 { pad = 16 - pad; - gmac.update::(&ZEROES[0..pad], ptr::null_mut()).unwrap(); + self.gmac.update::(&ZEROES[0..pad], ptr::null_mut()).unwrap(); } } } @@ -81,17 +106,16 @@ impl AesGmacSiv { #[inline(always)] pub fn encrypt_first_pass(&mut self, plaintext: &[u8]) { unsafe { - self.gmac.as_mut().unwrap().update::(plaintext, ptr::null_mut()).unwrap(); + self.gmac.update::(plaintext, ptr::null_mut()).unwrap(); } } /// Finish first pass and begin second pass. #[inline(always)] pub fn encrypt_first_pass_finish(&mut self) { - let gmac = self.gmac.as_mut().unwrap(); unsafe { - gmac.finalize::(self.tmp.as_mut_ptr()).unwrap(); - gmac.tag(&mut self.tmp).unwrap(); + self.gmac.finalize::(ptr::null_mut()).unwrap(); + self.gmac.tag(&mut self.tmp).unwrap(); } self.tag[8] = self.tmp[0] ^ self.tmp[8]; @@ -103,36 +127,19 @@ impl AesGmacSiv { self.tag[14] = self.tmp[6] ^ self.tmp[14]; self.tag[15] = self.tmp[7] ^ self.tmp[15]; - let mut tag_tmp = [0_u8; 32]; + let mut tag_tmp = [0_u8; 16]; - let ctx = CipherCtx::new().unwrap(); unsafe { - let t = match self.k1.len() { - 16 => ffi::EVP_aes_128_ecb(), - 24 => ffi::EVP_aes_192_ecb(), - 32 => ffi::EVP_aes_256_ecb(), - _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), - }; - ctx.cipher_init::(t, self.k1.as_mut_ptr(), ptr::null_mut()).unwrap(); - ffi::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); - ctx.update::(&self.tag, tag_tmp.as_mut_ptr()).unwrap(); + self.ecb_enc.update::(&self.tag, tag_tmp.as_mut_ptr()).unwrap(); } - self.tag.copy_from_slice(&tag_tmp[0..16]); - self.tmp.copy_from_slice(&tag_tmp[0..16]); + self.tag.copy_from_slice(&tag_tmp); + self.tmp.copy_from_slice(&tag_tmp); self.tmp[12] &= 0x7f; - let ctx = CipherCtx::new().unwrap(); unsafe { - let t = match self.k1.len() { - 16 => ffi::EVP_aes_128_ctr(), - 24 => ffi::EVP_aes_192_ctr(), - 32 => ffi::EVP_aes_256_ctr(), - _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), - }; - ctx.cipher_init::(t, self.k1.as_mut_ptr(), self.tmp.as_ptr()).unwrap(); + self.ctr.cipher_init::(ptr::null_mut(), ptr::null_mut(), self.tmp.as_ptr()).unwrap(); } - let _ = self.ctr.replace(ctx); } /// Feed plaintext for second pass and write ciphertext to supplied buffer. @@ -140,7 +147,7 @@ impl AesGmacSiv { #[inline(always)] pub fn encrypt_second_pass(&mut self, plaintext: &[u8], ciphertext: &mut [u8]) { unsafe { - self.ctr.as_mut().unwrap().update::(plaintext, ciphertext.as_mut_ptr()).unwrap(); + self.ctr.update::(plaintext, ciphertext.as_mut_ptr()).unwrap(); } } @@ -150,7 +157,7 @@ impl AesGmacSiv { pub fn encrypt_second_pass_in_place(&mut self, plaintext_to_ciphertext: &mut [u8]) { unsafe { let out = plaintext_to_ciphertext.as_mut_ptr(); - self.ctr.as_mut().unwrap().update::(plaintext_to_ciphertext, out).unwrap(); + self.ctr.update::(plaintext_to_ciphertext, out).unwrap(); } } @@ -168,46 +175,21 @@ impl AesGmacSiv { self.tmp.copy_from_slice(tag); self.tmp[12] &= 0x7f; - let ctx = CipherCtx::new().unwrap(); unsafe { - let t = match self.k1.len() { - 16 => ffi::EVP_aes_128_ctr(), - 24 => ffi::EVP_aes_192_ctr(), - 32 => ffi::EVP_aes_256_ctr(), - _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), - }; - ctx.cipher_init::(t, self.k1.as_mut_ptr(), self.tmp.as_ptr()).unwrap(); + self.ctr.cipher_init::(ptr::null_mut(), ptr::null_mut(), self.tmp.as_ptr()).unwrap(); } - let _ = self.ctr.replace(ctx); - let mut tag_tmp = [0_u8; 32]; + let mut tag_tmp = [0_u8; 16]; - let ctx = CipherCtx::new().unwrap(); unsafe { - let t = match self.k1.len() { - 16 => ffi::EVP_aes_128_ecb(), - 24 => ffi::EVP_aes_192_ecb(), - 32 => ffi::EVP_aes_256_ecb(), - _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), - }; - ctx.cipher_init::(t, self.k1.as_mut_ptr(), ptr::null_mut()).unwrap(); - ffi::EVP_CIPHER_CTX_set_padding(ctx.as_ptr(), 0); - ctx.update::(&self.tag, tag_tmp.as_mut_ptr()).unwrap(); + self.ecb_dec.update::(&tag, tag_tmp.as_mut_ptr()).unwrap(); } - self.tag.copy_from_slice(&tag_tmp[0..16]); + self.tag.copy_from_slice(&tag_tmp); tag_tmp[8..12].fill(0); - let ctx = CipherCtx::new().unwrap(); unsafe { - let t = match self.k0.len() { - 16 => ffi::EVP_aes_128_gcm(), - 24 => ffi::EVP_aes_192_gcm(), - 32 => ffi::EVP_aes_256_gcm(), - _ => panic!("Aes KEY_SIZE must be 16, 24 or 32"), - }; - ctx.cipher_init::(t, self.k0.as_mut_ptr(), self.tag[0..12].as_ptr()).unwrap(); + self.gmac.cipher_init::(ptr::null_mut(), ptr::null_mut(), tag_tmp.as_ptr()).unwrap(); } - let _ = self.gmac.replace(ctx); } /// Set additional authenticated data to be checked. @@ -221,8 +203,8 @@ impl AesGmacSiv { #[inline(always)] pub fn decrypt(&mut self, ciphertext: &[u8], plaintext: &mut [u8]) { unsafe { - self.ctr.as_mut().unwrap().update::(ciphertext, plaintext.as_mut_ptr()).unwrap(); - self.gmac.as_mut().unwrap().update::(plaintext, ptr::null_mut()).unwrap(); + self.ctr.update::(ciphertext, plaintext.as_mut_ptr()).unwrap(); + self.gmac.update::(plaintext, ptr::null_mut()).unwrap(); } } @@ -240,10 +222,9 @@ impl AesGmacSiv { /// If this returns false the message should be dropped. #[inline(always)] pub fn decrypt_finish(&mut self) -> Option<&[u8; 16]> { - let gmac = self.gmac.as_mut().unwrap(); unsafe { - gmac.finalize::(self.tmp.as_mut_ptr()).unwrap(); - gmac.tag(&mut self.tmp).unwrap(); + self.gmac.finalize::(self.tmp.as_mut_ptr()).unwrap(); + self.gmac.tag(&mut self.tmp).unwrap(); } if (self.tag[8] == self.tmp[0] ^ self.tmp[8]) && (self.tag[9] == self.tmp[1] ^ self.tmp[9]) From 15a80d9a1200bc206d772b77c607e46980093d87 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Mon, 20 Mar 2023 17:24:31 -0400 Subject: [PATCH 10/25] ran cargo fmt --- crypto/src/aes_gmac_siv_openssl.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crypto/src/aes_gmac_siv_openssl.rs b/crypto/src/aes_gmac_siv_openssl.rs index 7350e3df3..a5399aa5e 100644 --- a/crypto/src/aes_gmac_siv_openssl.rs +++ b/crypto/src/aes_gmac_siv_openssl.rs @@ -74,8 +74,7 @@ impl AesGmacSiv { /// Reset to prepare for another encrypt or decrypt operation. #[inline(always)] - pub fn reset(&mut self) { - } + pub fn reset(&mut self) {} /// Initialize for encryption. #[inline(always)] @@ -83,7 +82,9 @@ impl AesGmacSiv { self.tag[0..8].copy_from_slice(iv); self.tag[8..12].fill(0); unsafe { - self.gmac.cipher_init::(ptr::null_mut(), ptr::null_mut(), self.tag[0..12].as_ptr()).unwrap(); + self.gmac + .cipher_init::(ptr::null_mut(), ptr::null_mut(), self.tag[0..12].as_ptr()) + .unwrap(); } } @@ -176,7 +177,9 @@ impl AesGmacSiv { self.tmp[12] &= 0x7f; unsafe { - self.ctr.cipher_init::(ptr::null_mut(), ptr::null_mut(), self.tmp.as_ptr()).unwrap(); + self.ctr + .cipher_init::(ptr::null_mut(), ptr::null_mut(), self.tmp.as_ptr()) + .unwrap(); } let mut tag_tmp = [0_u8; 16]; From 984782d7798f11168ee8e0bae2e4054ed0498af8 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Mon, 20 Mar 2023 17:32:43 -0400 Subject: [PATCH 11/25] made the correctness tests less aggressive --- utils/src/arc_pool.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/src/arc_pool.rs b/utils/src/arc_pool.rs index d9eb269e5..b4fd8c88f 100644 --- a/utils/src/arc_pool.rs +++ b/utils/src/arc_pool.rs @@ -495,7 +495,7 @@ mod tests { #[test] fn multi_thread() { - const N: usize = 123456; + const N: usize = 12345; static COUNT: AtomicU64 = AtomicU64::new(0); let mut joins = Vec::new(); @@ -556,7 +556,7 @@ mod tests { let s = Arc::new(PoolArcSwap::new(TestPools::alloc(Item::new(0, &COUNT)))); - for _ in 0..1234 { + for _ in 0..123 { let mut joins = Vec::new(); for _ in 0..8 { let swaps = s.clone(); @@ -578,7 +578,7 @@ mod tests { #[test] fn multi_thread_swap_load() { - const N: usize = 123456; + const N: usize = 12345; static COUNT: AtomicU64 = AtomicU64::new(0); let s: Arc<[_; 8]> = Arc::new(std::array::from_fn(|i| PoolArcSwap::new(TestPools::alloc(Item::new(i as u32, &COUNT))))); From c2125db444961ed7da3cf19ab76d943d2a7bbc11 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Tue, 21 Mar 2023 08:25:15 -0400 Subject: [PATCH 12/25] added pub option --- utils/src/arc_pool.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/utils/src/arc_pool.rs b/utils/src/arc_pool.rs index b4fd8c88f..5fd782a1d 100644 --- a/utils/src/arc_pool.rs +++ b/utils/src/arc_pool.rs @@ -348,7 +348,7 @@ unsafe impl, const L: usize> Sync for PoolArcSwa /// ``` /// use zerotier_utils::arc_pool::{Pool, StaticPool, static_pool}; /// -/// static_pool!(StaticPool MyPools { +/// static_pool!(pub StaticPool MyPools { /// Pool, Pool<&u32, 12> /// }); /// @@ -384,6 +384,30 @@ macro_rules! __static_pool__ { } )* }; + (pub $m:ident $s:ident { $($($p:ident)::+<$t:ty$(, $l:tt)?>),+ $(,)?}) => { + pub struct $s {} + $( + impl $m<$t$(, $l)?> for $s { + #[inline(always)] + unsafe fn get_static_pool() -> *const () { + static POOL: $($p)::+<$t$(, $l)?> = $($p)::+::new(); + (&POOL as *const $($p)::+<$t$(, $l)?>).cast() + } + } + )* + }; + (pub $m:ident::$n:ident $s:ident { $($($p:ident)::+<$t:ty$(, $l:tt)?>),+ $(,)?}) => { + pub struct $s {} + $( + impl $m::$n<$t$(, $l)?> for $s { + #[inline(always)] + unsafe fn get_static_pool() -> *const () { + static POOL: $($p)::+<$t$(, $l)?> = $($p)::+::new(); + (&POOL as *const $($p)::+<$t$(, $l)?>).cast() + } + } + )* + }; } pub use __static_pool__ as static_pool; From 1e32e0ad2cd8892ee8bbd11460daf89d783ee160 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Wed, 22 Mar 2023 18:29:14 -0400 Subject: [PATCH 13/25] begun changes --- zssp/src/fragged.rs | 47 ++++++++++++++++++++++++--------------------- zssp/src/proto.rs | 1 + zssp/src/zssp.rs | 19 +++--------------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs index 01a0c3b20..6a9d2da33 100644 --- a/zssp/src/fragged.rs +++ b/zssp/src/fragged.rs @@ -3,6 +3,7 @@ use std::ptr::slice_from_raw_parts; /// Fast packet defragmenter pub struct Fragged { + count: u32, have: u64, counter: u64, frags: [MaybeUninit; MAX_FRAGMENTS], @@ -35,9 +36,9 @@ impl Fragged { // that the array of MaybeUninit can be freely cast into an array of // Fragment. They also check that the maximum number of fragments is not too large // for the fact that we use bits in a u64 to track which fragments are received. - assert!(MAX_FRAGMENTS <= 64); - assert_eq!(size_of::>(), size_of::()); - assert_eq!( + debug_assert!(MAX_FRAGMENTS <= 64); + debug_assert_eq!(size_of::>(), size_of::()); + debug_assert_eq!( size_of::<[MaybeUninit; MAX_FRAGMENTS]>(), size_of::<[Fragment; MAX_FRAGMENTS]>() ); @@ -51,12 +52,11 @@ impl Fragged { #[inline(always)] pub fn assemble(&mut self, counter: u64, fragment: Fragment, fragment_no: u8, fragment_count: u8) -> Option> { if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { - let mut have = self.have; // If the counter has changed, reset the structure to receive a new packet. if counter != self.counter { - self.counter = counter; if needs_drop::() { + let mut have = self.have; let mut i = 0; while have != 0 { if (have & 1) != 0 { @@ -66,26 +66,29 @@ impl Fragged { have = have.wrapping_shr(1); i += 1; } - } else { - have = 0; + } + self.have = 0; + self.count = fragment_count as u32; + self.counter = counter; + } + + let got = 1u64.wrapping_shl(fragment_no as u32); + if got & self.have == 0 && self.count as u8 == fragment_count { + self.have |= got; + unsafe { + self.frags.get_unchecked_mut(fragment_no as usize).write(fragment); + } + if self.have == 1u64.wrapping_shl(self.count) - 1 { + self.have = 0; + self.count = 0; + self.counter = 0; + // Setting 'have' to 0 resets the state of this object, and the fragments + // are effectively moved into the Assembled<> container and returned. That + // container will drop them when it is dropped. + return Some(Assembled(unsafe { std::mem::transmute_copy(&self.frags) }, fragment_count as usize)); } } - unsafe { - self.frags.get_unchecked_mut(fragment_no as usize).write(fragment); - } - - let want = 0xffffffffffffffffu64.wrapping_shr((64 - fragment_count) as u32); - have |= 1u64.wrapping_shl(fragment_no as u32); - if (have & want) == want { - self.have = 0; - // Setting 'have' to 0 resets the state of this object, and the fragments - // are effectively moved into the Assembled<> container and returned. That - // container will drop them when it is dropped. - return Some(Assembled(unsafe { std::mem::transmute_copy(&self.frags) }, fragment_count as usize)); - } else { - self.have = have; - } } return None; } diff --git a/zssp/src/proto.rs b/zssp/src/proto.rs index 826371983..06dc1dfaa 100644 --- a/zssp/src/proto.rs +++ b/zssp/src/proto.rs @@ -63,6 +63,7 @@ pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_ALICE_TO_BOB: u8 = b'A'; // AES-G pub(crate) const KBKDF_KEY_USAGE_LABEL_AES_GCM_BOB_TO_ALICE: u8 = b'B'; // AES-GCM in B->A direction pub(crate) const KBKDF_KEY_USAGE_LABEL_RATCHET: u8 = b'R'; // Key used in derivatin of next session key +pub(crate) const MAX_INCOMPLETE_SESSION_QUEUE_SIZE: usize = 32; pub(crate) const MAX_FRAGMENTS: usize = 48; // hard protocol max: 63 pub(crate) const MAX_NOISE_HANDSHAKE_FRAGMENTS: usize = 16; // enough room for p384 + ZT identity + kyber1024 + tag/hmac/etc. pub(crate) const MAX_NOISE_HANDSHAKE_SIZE: usize = MAX_NOISE_HANDSHAKE_FRAGMENTS * MIN_TRANSPORT_MTU; diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 660b3e65d..c20ce1e14 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -36,17 +36,8 @@ const GCM_CIPHER_POOL_SIZE: usize = 4; /// Each application using ZSSP must create an instance of this to own sessions and /// defragment incoming packets that are not yet associated with a session. pub struct Context { - max_incomplete_session_queue_size: usize, default_physical_mtu: AtomicUsize, - defrag: Mutex< - HashMap< - (Application::PhysicalPath, u64), - Arc<( - Mutex>, - i64, // creation timestamp - )>, - >, - >, + defrag: Mutex<[Fragged; MAX_INCOMPLETE_SESSION_QUEUE_SIZE]>, sessions: RwLock>, } @@ -154,11 +145,10 @@ impl Context { /// Create a new session context. /// /// * `max_incomplete_session_queue_size` - Maximum number of incomplete sessions in negotiation phase - pub fn new(max_incomplete_session_queue_size: usize, default_physical_mtu: usize) -> Self { + pub fn new(default_physical_mtu: usize) -> Self { Self { - max_incomplete_session_queue_size, default_physical_mtu: AtomicUsize::new(default_physical_mtu), - defrag: Mutex::new(HashMap::new()), + defrag: Mutex::new(std::array::from_fn(|_| Fragged::new())), sessions: RwLock::new(SessionsById { active: HashMap::with_capacity(64), incoming: HashMap::with_capacity(64), @@ -262,9 +252,6 @@ impl Context { } } - // Delete any expired defragmentation queue items not associated with a session. - self.defrag.lock().unwrap().retain(|_, fragged| fragged.1 > negotiation_timeout_cutoff); - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS.min(Application::RETRY_INTERVAL) } From 9e4e0998434adb617d17560efebbed7392ac4080 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Wed, 22 Mar 2023 18:55:35 -0400 Subject: [PATCH 14/25] got new defragmenter working --- zssp/src/fragged.rs | 4 +++ zssp/src/main.rs | 4 +-- zssp/src/zssp.rs | 76 +++++++++++++++++---------------------------- 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs index 6a9d2da33..264d713b4 100644 --- a/zssp/src/fragged.rs +++ b/zssp/src/fragged.rs @@ -45,6 +45,10 @@ impl Fragged { unsafe { zeroed() } } + #[inline(always)] + pub fn counter(&self) -> u64 { + self.counter + } /// Add a fragment and return an assembled packet container if all fragments have been received. /// /// When a fully assembled packet is returned the internal state is reset and this object can diff --git a/zssp/src/main.rs b/zssp/src/main.rs index 8bf5a9684..0df766689 100644 --- a/zssp/src/main.rs +++ b/zssp/src/main.rs @@ -46,7 +46,7 @@ fn alice_main( alice_out: mpsc::SyncSender>, alice_in: mpsc::Receiver>, ) { - let context = zssp::Context::::new(16, TEST_MTU); + let context = zssp::Context::::new(TEST_MTU); let mut data_buf = [0u8; 65536]; let mut next_service = ms_monotonic() + 500; let mut last_ratchet_count = 0; @@ -157,7 +157,7 @@ fn bob_main( bob_out: mpsc::SyncSender>, bob_in: mpsc::Receiver>, ) { - let context = zssp::Context::::new(16, TEST_MTU); + let context = zssp::Context::::new(TEST_MTU); let mut data_buf = [0u8; 65536]; let mut data_buf_2 = [0u8; TEST_MTU]; let mut last_ratchet_count = 0; diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index c20ce1e14..f03430f64 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -9,7 +9,10 @@ // ZSSP: ZeroTier Secure Session Protocol // FIPS compliant Noise_XK with Jedi powers (Kyber1024) and built-in attack-resistant large payload (fragmentation) support. -use std::collections::{HashMap, HashSet}; +use std::collections::hash_map::RandomState; +//use std::collections::hash_map::DefaultHasher; +use std::hash::{BuildHasher, Hash, Hasher}; +use std::collections::HashMap; use std::num::NonZeroU64; use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; @@ -37,7 +40,8 @@ const GCM_CIPHER_POOL_SIZE: usize = 4; /// defragment incoming packets that are not yet associated with a session. pub struct Context { default_physical_mtu: AtomicUsize, - defrag: Mutex<[Fragged; MAX_INCOMPLETE_SESSION_QUEUE_SIZE]>, + defrag_hasher: RandomState, + defrag: [Mutex>; MAX_INCOMPLETE_SESSION_QUEUE_SIZE], sessions: RwLock>, } @@ -148,7 +152,8 @@ impl Context { pub fn new(default_physical_mtu: usize) -> Self { Self { default_physical_mtu: AtomicUsize::new(default_physical_mtu), - defrag: Mutex::new(std::array::from_fn(|_| Fragged::new())), + defrag_hasher: RandomState::new(), + defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), sessions: RwLock::new(SessionsById { active: HashMap::with_capacity(64), incoming: HashMap::with_capacity(64), @@ -522,48 +527,34 @@ impl Context { } else { let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_physical_packet); - let (assembled_packet, incoming_packet_buf_arr); + let assembled; let incoming_packet = if fragment_count > 1 { - assembled_packet = { - let mut defrag = self.defrag.lock().unwrap(); - let f = defrag - .entry((source.clone(), incoming_counter)) - .or_insert_with(|| Arc::new((Mutex::new(Fragged::new()), current_time))) - .clone(); + let mut hasher = self.defrag_hasher.build_hasher(); + source.hash(&mut hasher); + hasher.write_u64(incoming_counter); + let offer_id = hasher.finish(); + let idx0 = (offer_id as usize)%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; + let idx1 = (offer_id as usize)/MAX_INCOMPLETE_SESSION_QUEUE_SIZE%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; - // Anti-DOS overflow purge of the incoming defragmentation queue for packets not associated with known sessions. - if defrag.len() >= self.max_incomplete_session_queue_size { - // First, drop all entries that are timed out or whose physical source duplicates another entry. - let mut sources = HashSet::with_capacity(defrag.len()); - let negotiation_timeout_cutoff = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; - defrag - .retain(|k, fragged| (fragged.1 > negotiation_timeout_cutoff && sources.insert(k.0.clone())) || Arc::ptr_eq(fragged, &f)); - - // Then, if we are still at or over the limit, drop 10% of remaining entries at random. - if defrag.len() >= self.max_incomplete_session_queue_size { - let mut rn = random::next_u32_secure(); - defrag.retain(|_, fragged| { - rn = prng32(rn); - rn > (u32::MAX / 10) || Arc::ptr_eq(fragged, &f) - }); - } + let mut slot0 = self.defrag[idx0].lock().unwrap(); + if slot0.counter() == offer_id { + assembled = slot0.assemble(offer_id, incoming_physical_packet_buf, fragment_no, fragment_count); + } else { + let mut slot1 = self.defrag[idx1].lock().unwrap(); + if slot1.counter() == offer_id || slot1.counter() == 0 { + assembled = slot1.assemble(offer_id, incoming_physical_packet_buf, fragment_no, fragment_count); + } else { + assembled = slot0.assemble(offer_id, incoming_physical_packet_buf, fragment_no, fragment_count); } - - f } - .0 - .lock() - .unwrap() - .assemble(incoming_counter, incoming_physical_packet_buf, fragment_no, fragment_count); - if let Some(assembled_packet) = assembled_packet.as_ref() { - self.defrag.lock().unwrap().remove(&(source.clone(), incoming_counter)); + + if let Some(assembled_packet) = &assembled { assembled_packet.as_ref() } else { return Ok(ReceiveResult::Ok(None)); } } else { - incoming_packet_buf_arr = [incoming_physical_packet_buf]; - &incoming_packet_buf_arr + std::array::from_ref(&incoming_physical_packet_buf) }; return self.process_complete_incoming_packet( @@ -788,7 +779,7 @@ impl Context { // If this queue is too big, we remove the latest entry and replace it. The latest // is used because under flood conditions this is most likely to be another bogus // entry. If we find one that is actually timed out, that one is replaced instead. - if sessions.incoming.len() >= self.max_incomplete_session_queue_size { + if sessions.incoming.len() >= MAX_INCOMPLETE_SESSION_QUEUE_SIZE { let mut newest = i64::MIN; let mut replace_id = None; let cutoff_time = current_time - Application::INCOMING_SESSION_NEGOTIATION_TIMEOUT_MS; @@ -1697,14 +1688,3 @@ fn kbkdf512(key: &Secret) -> Secret(key: &Secret) -> Secret<32> { hmac_sha512_secret256(key.as_bytes(), &[1, b'Z', b'T', LABEL, 0x00, 0, 1u8, 0u8]) } - -fn prng32(mut x: u32) -> u32 { - // based on lowbias32 from https://nullprogram.com/blog/2018/07/31/ - x = x.wrapping_add(1); // don't get stuck on 0 - x ^= x.wrapping_shr(16); - x = x.wrapping_mul(0x7feb352d); - x ^= x.wrapping_shr(15); - x = x.wrapping_mul(0x846ca68b); - x ^= x.wrapping_shr(16); - x -} From 562631f18d9f6df7e03e60f70eb831dcfe8fbbe8 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Wed, 22 Mar 2023 19:14:25 -0400 Subject: [PATCH 15/25] added counter randomization --- zssp/src/fragged.rs | 2 ++ zssp/src/zssp.rs | 40 ++++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs index 264d713b4..feadde5a1 100644 --- a/zssp/src/fragged.rs +++ b/zssp/src/fragged.rs @@ -45,6 +45,8 @@ impl Fragged { unsafe { zeroed() } } + /// Returns the counter value associated with the packet currently being assembled. + /// If no packet is currently being assembled it returns 0. #[inline(always)] pub fn counter(&self) -> u64 { self.counter diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index f03430f64..3721b8868 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -198,7 +198,7 @@ impl Context { PACKET_TYPE_ALICE_NOISE_XK_INIT, None, 0, - 1, + random::next_u64_secure(), None, ); } @@ -378,7 +378,7 @@ impl Context { PACKET_TYPE_ALICE_NOISE_XK_INIT, None, 0, - 1, + random::next_u64_secure(), None, )?; } @@ -450,7 +450,7 @@ impl Context { let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_physical_packet); if session.check_receive_window(incoming_counter) { - let (assembled_packet, incoming_packet_buf_arr); + let assembled_packet; let incoming_packet = if fragment_count > 1 { assembled_packet = session.defrag[(incoming_counter as usize) % COUNTER_WINDOW_MAX_OOO] .lock() @@ -462,8 +462,7 @@ impl Context { return Ok(ReceiveResult::Ok(Some(session))); } } else { - incoming_packet_buf_arr = [incoming_physical_packet_buf]; - &incoming_packet_buf_arr + std::array::from_ref(&incoming_physical_packet_buf) }; return self.process_complete_incoming_packet( @@ -491,7 +490,7 @@ impl Context { .decrypt_block_in_place(&mut incoming_physical_packet[HEADER_PROTECT_ENCRYPT_START..HEADER_PROTECT_ENCRYPT_END]); let (key_index, packet_type, fragment_count, fragment_no, incoming_counter) = parse_packet_header(&incoming_physical_packet); - let (assembled_packet, incoming_packet_buf_arr); + let assembled_packet; let incoming_packet = if fragment_count > 1 { assembled_packet = incoming.defrag[(incoming_counter as usize) % COUNTER_WINDOW_MAX_OOO] .lock() @@ -503,8 +502,7 @@ impl Context { return Ok(ReceiveResult::Ok(None)); } } else { - incoming_packet_buf_arr = [incoming_physical_packet_buf]; - &incoming_packet_buf_arr + std::array::from_ref(&incoming_physical_packet_buf) }; return self.process_complete_incoming_packet( @@ -532,19 +530,25 @@ impl Context { let mut hasher = self.defrag_hasher.build_hasher(); source.hash(&mut hasher); hasher.write_u64(incoming_counter); - let offer_id = hasher.finish(); - let idx0 = (offer_id as usize)%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; - let idx1 = (offer_id as usize)/MAX_INCOMPLETE_SESSION_QUEUE_SIZE%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; + let hashed_counter = hasher.finish(); + let idx0 = (hashed_counter as usize)%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; + let idx1 = (hashed_counter as usize)/MAX_INCOMPLETE_SESSION_QUEUE_SIZE%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; + // Open hash lookup of just 2 slots. + // By only checking 2 slots we avoid an expensive hash table lookup while also minimizing the chance that 2 offers collide. + // To DOS, an adversary would either need to volumetrically spam the defrag table to keep all slots full + // or replay Alice's packet header before Alice's packet is fully assembled. + // Since Alice's packet header has a randomly generated counter value replaying it in time is very hard. let mut slot0 = self.defrag[idx0].lock().unwrap(); - if slot0.counter() == offer_id { - assembled = slot0.assemble(offer_id, incoming_physical_packet_buf, fragment_no, fragment_count); + if slot0.counter() == hashed_counter { + assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } else { let mut slot1 = self.defrag[idx1].lock().unwrap(); - if slot1.counter() == offer_id || slot1.counter() == 0 { - assembled = slot1.assemble(offer_id, incoming_physical_packet_buf, fragment_no, fragment_count); + if slot1.counter() == hashed_counter || slot1.counter() == 0 { + assembled = slot1.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } else { - assembled = slot0.assemble(offer_id, incoming_physical_packet_buf, fragment_no, fragment_count); + // Both slots are full so kick out the unassembled packet in slot0 to make more room. + assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } } @@ -563,7 +567,7 @@ impl Context { &mut check_allow_incoming_session, &mut check_accept_session, data_buf, - incoming_counter, + 1,// The incoming_counter on init packets is only meant for DOS resistant defragmentation, we do not want to use it for anything noise related. incoming_packet, packet_type, None, @@ -702,7 +706,7 @@ impl Context { * identity, then responds with his ephemeral keys. */ - if incoming_counter != 1 || session.is_some() || incoming.is_some() { + if session.is_some() || incoming.is_some() { return Err(Error::OutOfSequence); } if pkt_assembled.len() != AliceNoiseXKInit::SIZE { From adcf553f18c895bcd39c0c5e095eb30213fc9fa9 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Wed, 22 Mar 2023 19:15:07 -0400 Subject: [PATCH 16/25] corrected comment --- zssp/src/zssp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 3721b8868..f9ed6d4e8 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -537,7 +537,7 @@ impl Context { // Open hash lookup of just 2 slots. // By only checking 2 slots we avoid an expensive hash table lookup while also minimizing the chance that 2 offers collide. // To DOS, an adversary would either need to volumetrically spam the defrag table to keep all slots full - // or replay Alice's packet header before Alice's packet is fully assembled. + // or replay Alice's packet header from a spoofed physical path before Alice's packet is fully assembled. // Since Alice's packet header has a randomly generated counter value replaying it in time is very hard. let mut slot0 = self.defrag[idx0].lock().unwrap(); if slot0.counter() == hashed_counter { From 1925d0f98eb9db43a7cc33712e8a9164b38b9a48 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Wed, 22 Mar 2023 23:27:37 -0400 Subject: [PATCH 17/25] added comments and reduced path requirements --- zssp/src/applicationlayer.rs | 2 +- zssp/src/zssp.rs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/zssp/src/applicationlayer.rs b/zssp/src/applicationlayer.rs index 1c89ff72f..ef8df25eb 100644 --- a/zssp/src/applicationlayer.rs +++ b/zssp/src/applicationlayer.rs @@ -71,7 +71,7 @@ pub trait ApplicationLayer: Sized { /// /// A physical path could be an IP address or IP plus device in the case of UDP, a socket in the /// case of TCP, etc. - type PhysicalPath: PartialEq + Eq + Hash + Clone; + type PhysicalPath: Hash; /// Get a reference to this host's static public key blob. /// diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index f9ed6d4e8..85b66a9e5 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -40,7 +40,7 @@ const GCM_CIPHER_POOL_SIZE: usize = 4; /// defragment incoming packets that are not yet associated with a session. pub struct Context { default_physical_mtu: AtomicUsize, - defrag_hasher: RandomState, + defrag_salt: RandomState, defrag: [Mutex>; MAX_INCOMPLETE_SESSION_QUEUE_SIZE], sessions: RwLock>, } @@ -152,7 +152,7 @@ impl Context { pub fn new(default_physical_mtu: usize) -> Self { Self { default_physical_mtu: AtomicUsize::new(default_physical_mtu), - defrag_hasher: RandomState::new(), + defrag_salt: RandomState::new(), defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), sessions: RwLock::new(SessionsById { active: HashMap::with_capacity(64), @@ -527,7 +527,9 @@ impl Context { let assembled; let incoming_packet = if fragment_count > 1 { - let mut hasher = self.defrag_hasher.build_hasher(); + // Using just incoming_counter unhashed would be good DOS resistant, + // but why not make it harder by mixing in a random value and the physical path in as well. + let mut hasher = self.defrag_salt.build_hasher(); source.hash(&mut hasher); hasher.write_u64(incoming_counter); let hashed_counter = hasher.finish(); @@ -535,10 +537,12 @@ impl Context { let idx1 = (hashed_counter as usize)/MAX_INCOMPLETE_SESSION_QUEUE_SIZE%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; // Open hash lookup of just 2 slots. - // By only checking 2 slots we avoid an expensive hash table lookup while also minimizing the chance that 2 offers collide. + // By only checking 2 slots we avoid a full table lookup while also minimizing the chance that 2 offers collide. // To DOS, an adversary would either need to volumetrically spam the defrag table to keep all slots full // or replay Alice's packet header from a spoofed physical path before Alice's packet is fully assembled. - // Since Alice's packet header has a randomly generated counter value replaying it in time is very hard. + // Volumetric spam is quite difficult since without the `defrag_salt: RandomState` value an adversary + // cannot control which slots their fragments index to. And since Alice's packet header has a randomly + // generated counter value replaying it in time requires extreme amounts of network control. let mut slot0 = self.defrag[idx0].lock().unwrap(); if slot0.counter() == hashed_counter { assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); @@ -547,7 +551,7 @@ impl Context { if slot1.counter() == hashed_counter || slot1.counter() == 0 { assembled = slot1.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } else { - // Both slots are full so kick out the unassembled packet in slot0 to make more room. + // slot1 is full so kick out whatever is in slot0 to make more room. assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } } From 8ddc054cfb86899f1189efc5f9454fff5975f94a Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 08:52:10 -0400 Subject: [PATCH 18/25] ran cargo fmt --- zssp/src/fragged.rs | 2 -- zssp/src/zssp.rs | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs index feadde5a1..f3f5f1eae 100644 --- a/zssp/src/fragged.rs +++ b/zssp/src/fragged.rs @@ -58,7 +58,6 @@ impl Fragged { #[inline(always)] pub fn assemble(&mut self, counter: u64, fragment: Fragment, fragment_no: u8, fragment_count: u8) -> Option> { if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { - // If the counter has changed, reset the structure to receive a new packet. if counter != self.counter { if needs_drop::() { @@ -94,7 +93,6 @@ impl Fragged { return Some(Assembled(unsafe { std::mem::transmute_copy(&self.frags) }, fragment_count as usize)); } } - } return None; } diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index 85b66a9e5..f07260799 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -11,8 +11,8 @@ use std::collections::hash_map::RandomState; //use std::collections::hash_map::DefaultHasher; -use std::hash::{BuildHasher, Hash, Hasher}; use std::collections::HashMap; +use std::hash::{BuildHasher, Hash, Hasher}; use std::num::NonZeroU64; use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; @@ -533,8 +533,8 @@ impl Context { source.hash(&mut hasher); hasher.write_u64(incoming_counter); let hashed_counter = hasher.finish(); - let idx0 = (hashed_counter as usize)%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; - let idx1 = (hashed_counter as usize)/MAX_INCOMPLETE_SESSION_QUEUE_SIZE%MAX_INCOMPLETE_SESSION_QUEUE_SIZE; + let idx0 = (hashed_counter as usize) % MAX_INCOMPLETE_SESSION_QUEUE_SIZE; + let idx1 = (hashed_counter as usize) / MAX_INCOMPLETE_SESSION_QUEUE_SIZE % MAX_INCOMPLETE_SESSION_QUEUE_SIZE; // Open hash lookup of just 2 slots. // By only checking 2 slots we avoid a full table lookup while also minimizing the chance that 2 offers collide. @@ -571,7 +571,7 @@ impl Context { &mut check_allow_incoming_session, &mut check_accept_session, data_buf, - 1,// The incoming_counter on init packets is only meant for DOS resistant defragmentation, we do not want to use it for anything noise related. + 1, // The incoming_counter on init packets is only meant for DOS resistant defragmentation, we do not want to use it for anything noise related. incoming_packet, packet_type, None, From 4c26109a090795873c18a8c7eda909c70ce53668 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 10:29:41 -0400 Subject: [PATCH 19/25] improved documentation and added debug --- utils/src/arc_pool.rs | 197 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 162 insertions(+), 35 deletions(-) diff --git a/utils/src/arc_pool.rs b/utils/src/arc_pool.rs index 5fd782a1d..4375bf0db 100644 --- a/utils/src/arc_pool.rs +++ b/utils/src/arc_pool.rs @@ -1,3 +1,4 @@ +use std::fmt::{Debug, Display}; use std::marker::PhantomData; use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::num::NonZeroU64; @@ -63,6 +64,7 @@ impl Pool { let slot_ptr = if let Some(mut slot_ptr) = NonNull::new(first_free) { let slot = slot_ptr.as_mut(); let _announce_free = slot.free_lock.write().unwrap(); + debug_assert_eq!(slot.uid, 0); first_free = slot.obj.empty_next; slot.ref_count = AtomicU32::new(1); slot.uid = uid; @@ -93,9 +95,10 @@ impl Pool { /// Frees memory allocated from the pool by `Pool::alloc_ptr`. This must be called only once on only pointers returned by `Pool::alloc_ptr` from the same pool. Once memory is freed the content of the memory is undefined, it should not be read or written. /// /// `drop` will be called on the `T` pointed to, be sure it has not been called already. + /// + /// The free lock must be held by the caller. unsafe fn free_ptr(&self, mut slot_ptr: NonNull>) { let slot = slot_ptr.as_mut(); - let _announce_free = slot.free_lock.write().unwrap(); slot.uid = 0; ManuallyDrop::::drop(&mut slot.obj.full_obj); //linked-list insert @@ -146,18 +149,6 @@ pub trait StaticPool { } } -/// A multithreading lock guard that prevents another thread from freeing the underlying `T` while it is held. It does not prevent other threads from accessing the underlying `T`. -/// -/// If the same thread that holds this guard attempts to free `T` before dropping the guard, it will deadlock. -pub struct PoolGuard<'a, T>(RwLockReadGuard<'a, ()>, &'a T); -impl<'a, T> Deref for PoolGuard<'a, T> { - type Target = T; - #[inline] - fn deref(&self) -> &Self::Target { - &*self.1 - } -} - /// A rust-style RAII wrapper that drops and frees memory allocated from a pool automatically, the same as an `Arc`. This will run the destructor of `T` in place within the pool before freeing it, correctly maintaining the invariants that the borrow checker and rust compiler expect of generic types. pub struct PoolArc, const L: usize = DEFAULT_L> { ptr: NonNull>, @@ -166,13 +157,16 @@ pub struct PoolArc, const L: usize = DEFAULT_L> impl, const L: usize> PoolArc { /// Obtain a non-owning reference to the `T` contained in this `PoolArc`. This reference has the special property that the underlying `T` can be dropped from the pool while neither making this reference invalid or unsafe nor leaking the memory of `T`. Instead attempts to `grab` the reference will safely return `None`. + /// + /// `T` is guaranteed to be dropped when all `PoolArc` are dropped, regardless of how many `PoolWeakRef` still exist. #[inline] - pub fn downgrade(&self) -> PoolWeakRef { + pub fn downgrade(&self) -> PoolWeakRef { unsafe { // Since this is a Arc we know for certain the object has not been freed, so we don't have to hold the free lock PoolWeakRef { ptr: self.ptr, uid: NonZeroU64::new_unchecked(self.ptr.as_ref().uid), + _p: PhantomData, } } } @@ -181,8 +175,6 @@ impl, const L: usize> PoolArc unsafe { NonZeroU64::new_unchecked(self.ptr.as_ref().uid) } } } -unsafe impl, const L: usize> Send for PoolArc where T: Send {} -unsafe impl, const L: usize> Sync for PoolArc where T: Sync {} impl, const L: usize> Deref for PoolArc { type Target = T; @@ -203,25 +195,51 @@ impl, const L: usize> Drop for PoolArc>()).free_ptr(self.ptr); + let slot = self.ptr.as_ref(); + if slot.ref_count.fetch_sub(1, Ordering::AcqRel) == 1 { + let _announce_free = slot.free_lock.write().unwrap(); + // We have to check twice in case a weakref was upgraded before the lock was acquired + if slot.ref_count.load(Ordering::Relaxed) == 0 { + (*OriginPool::get_static_pool().cast::>()).free_ptr(self.ptr); + } } } } } +unsafe impl, const L: usize> Send for PoolArc where T: Send {} +unsafe impl, const L: usize> Sync for PoolArc where T: Sync {} +impl, const L: usize> Debug for PoolArc +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("PoolArc").field(self.deref()).finish() + } +} +impl, const L: usize> Display for PoolArc +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.deref().fmt(f) + } +} /// A non-owning reference to a `T` allocated by a pool. This reference has the special property that the underlying `T` can be dropped from the pool while neither making this reference invalid nor leaking the memory of `T`. Instead attempts to `grab` this reference will safely return `None` if the underlying `T` has been freed by any thread. /// -/// Due to there thread safety and low overhead a `PoolWeakRef` implements clone and copy. +/// Due to their thread safety and low overhead a `PoolWeakRef` implements clone and copy. /// /// The lifetime of this reference is tied to the lifetime of the pool it came from, because if it were allowed to live longer than its origin pool, it would no longer be safe to dereference and would most likely segfault. Instead the borrow-checker will enforce that this reference has a shorter lifetime that its origin pool. -pub struct PoolWeakRef { +/// +/// For technical reasons a `RwLock>` will always be the fastest implementation of a `PoolWeakRefSwap`, which is why this library does not provide a `PoolWeakRefSwap` type. +pub struct PoolWeakRef, const L: usize = DEFAULT_L> { /// A number that uniquely identifies this allocated `T` within this pool. No other instance of `T` may have this uid. This value is read-only. pub uid: NonZeroU64, ptr: NonNull>, + _p: PhantomData<*const OriginPool>, } -impl PoolWeakRef { +impl, const L: usize> PoolWeakRef { /// Obtains a lock that allows the `T` contained in this `PoolWeakRef` to be dereferenced in a thread-safe manner. This lock does not prevent other threads from accessing `T` at the same time, so `T` ought to use interior mutability if it needs to be mutated in a thread-safe way. What this lock does guarantee is that `T` cannot be destructed and freed while it is being held. /// /// Do not attempt from within the same thread to drop the `PoolArc` that owns this `T` before dropping this lock, or else the thread will deadlock. Rust makes this quite hard to do accidentally but it's not strictly impossible. @@ -237,23 +255,86 @@ impl PoolWeakRef { } } } -} -unsafe impl Send for PoolWeakRef where T: Send {} -unsafe impl Sync for PoolWeakRef where T: Sync {} - -impl Clone for PoolWeakRef { - fn clone(&self) -> Self { - Self { uid: self.uid, ptr: self.ptr } + /// Attempts to create an owning `PoolArc` from this `PoolWeakRef` of the underlying `T`. Will return `None` if the underlying `T` has already been dropped. + pub fn upgrade(&self) -> Option> { + unsafe { + let slot = self.ptr.as_ref(); + let _prevent_free_lock = slot.free_lock.read().unwrap(); + if slot.uid == self.uid.get() { + self.ptr.as_ref().ref_count.fetch_add(1, Ordering::Relaxed); + Some(PoolArc { ptr: self.ptr, _p: PhantomData }) + } else { + None + } + } + } +} +impl, const L: usize> Clone for PoolWeakRef { + fn clone(&self) -> Self { + Self { uid: self.uid, ptr: self.ptr, _p: PhantomData } + } +} +impl, const L: usize> Copy for PoolWeakRef {} +unsafe impl, const L: usize> Send for PoolWeakRef where T: Send {} +unsafe impl, const L: usize> Sync for PoolWeakRef where T: Sync {} +impl, const L: usize> Debug for PoolWeakRef +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = self.grab(); + f.debug_tuple("PoolWeakRef").field(&inner).finish() + } +} +impl, const L: usize> Display for PoolWeakRef +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(inner) = self.grab() { + inner.fmt(f) + } else { + f.write_str("Empty") + } } } -impl Copy for PoolWeakRef {} +/// A multithreading lock guard that prevents another thread from freeing the underlying `T` while it is held. It does not prevent other threads from accessing the underlying `T`. +/// +/// If the same thread that holds this guard attempts to free `T` before dropping the guard, it will deadlock. +pub struct PoolGuard<'a, T>(RwLockReadGuard<'a, ()>, &'a T); +impl<'a, T> Deref for PoolGuard<'a, T> { + type Target = T; + #[inline] + fn deref(&self) -> &Self::Target { + &*self.1 + } +} +impl<'a, T> Debug for PoolGuard<'a, T> +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("PoolGuard").field(self.deref()).finish() + } +} +impl<'a, T> Display for PoolGuard<'a, T> +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.deref().fmt(f) + } +} + +/// Allows for the Atomic Swapping and Loading of a `PoolArc`, similar to how a `RwLock>` would function, but much faster and less verbose. pub struct PoolArcSwap, const L: usize = DEFAULT_L> { ptr: AtomicPtr>, reads: AtomicU32, _p: PhantomData<*const OriginPool>, } impl, const L: usize> PoolArcSwap { + /// Creates a new `PoolArcSwap`, consuming `arc` in the process. pub fn new(mut arc: PoolArc) -> Self { unsafe { let ret = Self { @@ -266,7 +347,7 @@ impl, const L: usize> PoolArcSwap) -> PoolArc { unsafe { let pre_ptr = self.ptr.swap(arc.ptr.as_ptr(), Ordering::Relaxed); @@ -280,6 +361,7 @@ impl, const L: usize> PoolArcSwap PoolArc { unsafe { self.reads.fetch_add(1, Ordering::Acquire); @@ -290,9 +372,6 @@ impl, const L: usize> PoolArcSwap, const L: usize> Send for PoolArcSwap where T: Send {} -unsafe impl, const L: usize> Sync for PoolArcSwap where T: Sync {} - impl, const L: usize> Drop for PoolArcSwap { #[inline] fn drop(&mut self) { @@ -302,19 +381,42 @@ impl, const L: usize> Drop for PoolArcSwap, const L: usize> Send for PoolArcSwap where T: Send {} +unsafe impl, const L: usize> Sync for PoolArcSwap where T: Sync {} +impl, const L: usize> Debug for PoolArcSwap +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("PoolArcSwap").field(&self.load()).finish() + } +} +impl, const L: usize> Display for PoolArcSwap +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (&self.load()).fmt(f) + } +} +/// Another implementation of a `PoolArcSwap` utalizing a RwLock instead of atomics. +/// This implementation has slower a `load` but a faster `swap` than the previous implementation of `PoolArcSwap`. +/// If you plan on swapping way more often than loading, this may be a better choice. pub struct PoolArcSwapRw, const L: usize = DEFAULT_L> { ptr: RwLock>>, _p: PhantomData<*const OriginPool>, } impl, const L: usize> PoolArcSwapRw { + /// Creates a new `PoolArcSwap`, consuming `arc` in the process. pub fn new(arc: PoolArc) -> Self { let ret = Self { ptr: RwLock::new(arc.ptr), _p: arc._p }; mem::forget(arc); ret } + /// Atomically swaps the currently stored `PoolArc` with a new one, returning the previous one. pub fn swap(&self, arc: PoolArc) -> PoolArc { let mut w = self.ptr.write().unwrap(); let pre = PoolArc { ptr: *w, _p: self._p }; @@ -323,6 +425,7 @@ impl, const L: usize> PoolArcSwapRw PoolArc { let r = self.ptr.read().unwrap(); unsafe { @@ -341,22 +444,46 @@ impl, const L: usize> Drop for PoolArcSwapRw, const L: usize> Send for PoolArcSwapRw where T: Send {} unsafe impl, const L: usize> Sync for PoolArcSwapRw where T: Sync {} +impl, const L: usize> Debug for PoolArcSwapRw +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("PoolArcSwapRw").field(&self.load()).finish() + } +} +impl, const L: usize> Display for PoolArcSwapRw +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (&self.load()).fmt(f) + } +} /// Automatically generates valid implementations of `StaticPool` onto a chosen identifier, allowing this module to allocate instances of `T` with `alloc`. Users have to generate implementations clientside because rust does not allow for generic globals. /// +/// The chosen identifier is declared to be a struct with no fields, and instead contains a static global `Pool` for every implementation of `StaticPool` requested. +/// /// # Example /// ``` -/// use zerotier_utils::arc_pool::{Pool, StaticPool, static_pool}; +/// use zerotier_utils::arc_pool::{static_pool, StaticPool, Pool, PoolArc}; /// /// static_pool!(pub StaticPool MyPools { /// Pool, Pool<&u32, 12> /// }); /// +/// struct Container { +/// item: PoolArc +/// } +/// /// let object = 1u32; /// let arc_object = MyPools::alloc(object); /// let arc_ref = MyPools::alloc(&object); +/// let arc_container = Container {item: MyPools::alloc(object)}; /// /// assert_eq!(*arc_object, **arc_ref); +/// assert_eq!(*arc_object, *arc_container.item); /// ``` #[macro_export] macro_rules! __static_pool__ { @@ -538,7 +665,7 @@ mod tests { s.check(id); } else if p < prob(60) { if let Some((id, s, w)) = rand_idx(&items, r) { - items_dup.push((*id, s.clone(), w.clone())); + items_dup.push((*id, s.clone(), (*w).clone())); s.check(*id); } } else if p < prob(80) { From cada04545e0cb04b9fd1f21107e452dd8865a2c4 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 12:04:48 -0400 Subject: [PATCH 20/25] added init packet expiry --- zssp/src/fragged.rs | 30 +++++++++++++---------------- zssp/src/zssp.rs | 46 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/zssp/src/fragged.rs b/zssp/src/fragged.rs index f3f5f1eae..47370158b 100644 --- a/zssp/src/fragged.rs +++ b/zssp/src/fragged.rs @@ -55,24 +55,11 @@ impl Fragged { /// /// When a fully assembled packet is returned the internal state is reset and this object can /// be reused to assemble another packet. - #[inline(always)] pub fn assemble(&mut self, counter: u64, fragment: Fragment, fragment_no: u8, fragment_count: u8) -> Option> { if fragment_no < fragment_count && (fragment_count as usize) <= MAX_FRAGMENTS { // If the counter has changed, reset the structure to receive a new packet. if counter != self.counter { - if needs_drop::() { - let mut have = self.have; - let mut i = 0; - while have != 0 { - if (have & 1) != 0 { - debug_assert!(i < MAX_FRAGMENTS); - unsafe { self.frags.get_unchecked_mut(i).assume_init_drop() }; - } - have = have.wrapping_shr(1); - i += 1; - } - } - self.have = 0; + self.drop_in_place(); self.count = fragment_count as u32; self.counter = counter; } @@ -96,11 +83,10 @@ impl Fragged { } return None; } -} -impl Drop for Fragged { + /// Drops any remaining fragments and resets this object. #[inline(always)] - fn drop(&mut self) { + pub fn drop_in_place(&mut self) { if needs_drop::() { let mut have = self.have; let mut i = 0; @@ -113,5 +99,15 @@ impl Drop for Fragged Drop for Fragged { + #[inline(always)] + fn drop(&mut self) { + self.drop_in_place(); } } diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index f07260799..b8d522b14 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -14,7 +14,7 @@ use std::collections::hash_map::RandomState; use std::collections::HashMap; use std::hash::{BuildHasher, Hash, Hasher}; use std::num::NonZeroU64; -use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering, AtomicBool}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesGcm}; @@ -41,7 +41,8 @@ const GCM_CIPHER_POOL_SIZE: usize = 4; pub struct Context { default_physical_mtu: AtomicUsize, defrag_salt: RandomState, - defrag: [Mutex>; MAX_INCOMPLETE_SESSION_QUEUE_SIZE], + defrag_has_pending: AtomicBool, // Allowed to be falsely positive + defrag: [Mutex<(Fragged, i64)>; MAX_INCOMPLETE_SESSION_QUEUE_SIZE], sessions: RwLock>, } @@ -153,7 +154,8 @@ impl Context { Self { default_physical_mtu: AtomicUsize::new(default_physical_mtu), defrag_salt: RandomState::new(), - defrag: std::array::from_fn(|_| Mutex::new(Fragged::new())), + defrag_has_pending: AtomicBool::new(false), + defrag: std::array::from_fn(|_| Mutex::new((Fragged::new(), i64::MAX))), sessions: RwLock::new(SessionsById { active: HashMap::with_capacity(64), incoming: HashMap::with_capacity(64), @@ -245,6 +247,24 @@ impl Context { dead_pending.push(*id); } } + + } + // Only check for expiration if we have a pending packet. + // This check is allowed to have false positives for simplicity's sake. + if self.defrag_has_pending.swap(false, Ordering::Relaxed) { + let mut has_pending = false; + for m in &self.defrag { + let mut pending = m.lock().unwrap(); + if pending.1 <= negotiation_timeout_cutoff { + pending.1 = i64::MAX; + pending.0.drop_in_place(); + } else if pending.0.counter() != 0 { + has_pending = true; + } + } + if has_pending { + self.defrag_has_pending.store(true, Ordering::Relaxed); + } } if !dead_active.is_empty() || !dead_pending.is_empty() { @@ -544,15 +564,23 @@ impl Context { // cannot control which slots their fragments index to. And since Alice's packet header has a randomly // generated counter value replaying it in time requires extreme amounts of network control. let mut slot0 = self.defrag[idx0].lock().unwrap(); - if slot0.counter() == hashed_counter { - assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + if slot0.0.counter() == hashed_counter { + assembled = slot0.0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + if assembled.is_some() { slot0.1 = i64::MAX } } else { let mut slot1 = self.defrag[idx1].lock().unwrap(); - if slot1.counter() == hashed_counter || slot1.counter() == 0 { - assembled = slot1.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + if slot1.0.counter() == hashed_counter || slot1.0.counter() == 0 { + if slot1.0.counter() == 0 { + slot1.1 = current_time; + self.defrag_has_pending.store(true, Ordering::Relaxed); + } + assembled = slot1.0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + if assembled.is_some() { slot1.1 = i64::MAX } } else { - // slot1 is full so kick out whatever is in slot0 to make more room. - assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + // slot0 is either occupied or empty so we overwrite whatever is there to make more room. + slot0.1 = current_time; + self.defrag_has_pending.store(true, Ordering::Relaxed); + assembled = slot0.0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } } From 4f064dd3892ea417e78c94cd30e24e419600b9bc Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 12:08:58 -0400 Subject: [PATCH 21/25] ran cargo fmt --- zssp/src/zssp.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index b8d522b14..cb9410e48 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -14,7 +14,7 @@ use std::collections::hash_map::RandomState; use std::collections::HashMap; use std::hash::{BuildHasher, Hash, Hasher}; use std::num::NonZeroU64; -use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering, AtomicBool}; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; use zerotier_crypto::aes::{Aes, AesGcm}; @@ -247,7 +247,6 @@ impl Context { dead_pending.push(*id); } } - } // Only check for expiration if we have a pending packet. // This check is allowed to have false positives for simplicity's sake. @@ -565,8 +564,12 @@ impl Context { // generated counter value replaying it in time requires extreme amounts of network control. let mut slot0 = self.defrag[idx0].lock().unwrap(); if slot0.0.counter() == hashed_counter { - assembled = slot0.0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); - if assembled.is_some() { slot0.1 = i64::MAX } + assembled = slot0 + .0 + .assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + if assembled.is_some() { + slot0.1 = i64::MAX + } } else { let mut slot1 = self.defrag[idx1].lock().unwrap(); if slot1.0.counter() == hashed_counter || slot1.0.counter() == 0 { @@ -574,13 +577,19 @@ impl Context { slot1.1 = current_time; self.defrag_has_pending.store(true, Ordering::Relaxed); } - assembled = slot1.0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); - if assembled.is_some() { slot1.1 = i64::MAX } + assembled = slot1 + .0 + .assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + if assembled.is_some() { + slot1.1 = i64::MAX + } } else { // slot0 is either occupied or empty so we overwrite whatever is there to make more room. slot0.1 = current_time; self.defrag_has_pending.store(true, Ordering::Relaxed); - assembled = slot0.0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + assembled = slot0 + .0 + .assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } } From d0c00becdc4b5ec213cf68bf40b3bffe368915c6 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 12:13:29 -0400 Subject: [PATCH 22/25] changed variables so cargo fmt is less ugly --- zssp/src/zssp.rs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index cb9410e48..be6d66180 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -562,34 +562,28 @@ impl Context { // Volumetric spam is quite difficult since without the `defrag_salt: RandomState` value an adversary // cannot control which slots their fragments index to. And since Alice's packet header has a randomly // generated counter value replaying it in time requires extreme amounts of network control. - let mut slot0 = self.defrag[idx0].lock().unwrap(); - if slot0.0.counter() == hashed_counter { - assembled = slot0 - .0 - .assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + let (slot0, timestamp0) = &mut *self.defrag[idx0].lock().unwrap(); + if slot0.counter() == hashed_counter { + assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); if assembled.is_some() { - slot0.1 = i64::MAX + *timestamp0 = i64::MAX; } } else { - let mut slot1 = self.defrag[idx1].lock().unwrap(); - if slot1.0.counter() == hashed_counter || slot1.0.counter() == 0 { - if slot1.0.counter() == 0 { - slot1.1 = current_time; + let (slot1, timestamp1) = &mut *self.defrag[idx1].lock().unwrap(); + if slot1.counter() == hashed_counter || slot1.counter() == 0 { + if slot1.counter() == 0 { + *timestamp1 = current_time; self.defrag_has_pending.store(true, Ordering::Relaxed); } - assembled = slot1 - .0 - .assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + assembled = slot1.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); if assembled.is_some() { - slot1.1 = i64::MAX + *timestamp1 = i64::MAX; } } else { // slot0 is either occupied or empty so we overwrite whatever is there to make more room. - slot0.1 = current_time; + *timestamp0 = current_time; self.defrag_has_pending.store(true, Ordering::Relaxed); - assembled = slot0 - .0 - .assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } } From 7cc8db2187cbc987a734cbe184ba25ef49e8311d Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 13:18:00 -0400 Subject: [PATCH 23/25] fixed comment --- zssp/src/zssp.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index be6d66180..fcdad8b50 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -546,8 +546,9 @@ impl Context { let assembled; let incoming_packet = if fragment_count > 1 { - // Using just incoming_counter unhashed would be good DOS resistant, - // but why not make it harder by mixing in a random value and the physical path in as well. + // incoming_counter is expected to be a random u64 generated by the remote peer. + // Using just incoming_counter to defragment would be good DOS resistance, + // but why not make it harder by hasing it with a random salt and the physical path in as well. let mut hasher = self.defrag_salt.build_hasher(); source.hash(&mut hasher); hasher.write_u64(incoming_counter); @@ -559,8 +560,8 @@ impl Context { // By only checking 2 slots we avoid a full table lookup while also minimizing the chance that 2 offers collide. // To DOS, an adversary would either need to volumetrically spam the defrag table to keep all slots full // or replay Alice's packet header from a spoofed physical path before Alice's packet is fully assembled. - // Volumetric spam is quite difficult since without the `defrag_salt: RandomState` value an adversary - // cannot control which slots their fragments index to. And since Alice's packet header has a randomly + // Volumetric spam is quite difficult since without the `defrag_salt` value an adversary cannot + // control which slots their fragments index to. And since Alice's packet header has a randomly // generated counter value replaying it in time requires extreme amounts of network control. let (slot0, timestamp0) = &mut *self.defrag[idx0].lock().unwrap(); if slot0.counter() == hashed_counter { From 5bf62d823691c798343a91c026760788393f5119 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 13:20:45 -0400 Subject: [PATCH 24/25] fixed typo --- zssp/src/zssp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index fcdad8b50..d79fbd671 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -548,7 +548,7 @@ impl Context { let incoming_packet = if fragment_count > 1 { // incoming_counter is expected to be a random u64 generated by the remote peer. // Using just incoming_counter to defragment would be good DOS resistance, - // but why not make it harder by hasing it with a random salt and the physical path in as well. + // but why not make it harder by hasing it with a random salt and the physical path as well. let mut hasher = self.defrag_salt.build_hasher(); source.hash(&mut hasher); hasher.write_u64(incoming_counter); From af7af4d4db74ba84456e2df45fa346bea58b45e4 Mon Sep 17 00:00:00 2001 From: mamoniot Date: Thu, 23 Mar 2023 13:34:28 -0400 Subject: [PATCH 25/25] implemented better lookup --- zssp/src/zssp.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/zssp/src/zssp.rs b/zssp/src/zssp.rs index d79fbd671..544368e6c 100644 --- a/zssp/src/zssp.rs +++ b/zssp/src/zssp.rs @@ -571,20 +571,20 @@ impl Context { } } else { let (slot1, timestamp1) = &mut *self.defrag[idx1].lock().unwrap(); - if slot1.counter() == hashed_counter || slot1.counter() == 0 { - if slot1.counter() == 0 { - *timestamp1 = current_time; - self.defrag_has_pending.store(true, Ordering::Relaxed); - } + if slot1.counter() == hashed_counter { assembled = slot1.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); if assembled.is_some() { *timestamp1 = i64::MAX; } - } else { - // slot0 is either occupied or empty so we overwrite whatever is there to make more room. + } else if slot0.counter() == 0 { *timestamp0 = current_time; self.defrag_has_pending.store(true, Ordering::Relaxed); assembled = slot0.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); + } else { + // slot1 is either occupied or empty so we overwrite whatever is there to make more room. + *timestamp1 = current_time; + self.defrag_has_pending.store(true, Ordering::Relaxed); + assembled = slot1.assemble(hashed_counter, incoming_physical_packet_buf, fragment_no, fragment_count); } }