mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-07 21:13:44 +02:00
implemented unfinished architecture
This commit is contained in:
parent
e445088cf2
commit
fb20bbc538
4 changed files with 68 additions and 169 deletions
|
@ -78,7 +78,6 @@ pub(crate) const SESSION_ID_SIZE: usize = 6;
|
|||
pub(crate) const KEY_HISTORY_SIZE: usize = 3;
|
||||
|
||||
/// Maximum difference between out-of-order incoming packet counters, and size of deduplication buffer.
|
||||
pub(crate) const COUNTER_MAX_DELTA: u32 = 16;
|
||||
pub(crate) const COUNTER_MAX_ALLOWED_OOO: usize = 16;
|
||||
|
||||
// Packet types can range from 0 to 15 (4 bits) -- 0-3 are defined and 4-15 are reserved for future use
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use std::sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Mutex, RwLock,
|
||||
};
|
||||
use std::{sync::{
|
||||
atomic::{AtomicU64, Ordering, AtomicU32, AtomicI32, AtomicBool}
|
||||
}, mem};
|
||||
|
||||
use zerotier_crypto::random;
|
||||
|
||||
use crate::constants::{COUNTER_MAX_DELTA, COUNTER_MAX_ALLOWED_OOO};
|
||||
use crate::constants::COUNTER_MAX_ALLOWED_OOO;
|
||||
|
||||
/// Outgoing packet counter with strictly ordered atomic semantics.
|
||||
///
|
||||
|
@ -20,7 +19,7 @@ impl Counter {
|
|||
pub fn new() -> Self {
|
||||
// Using a random value has no security implication. Zero would be fine. This just
|
||||
// helps randomize packet contents a bit.
|
||||
Self(AtomicU64::new(random::next_u32_secure() as u64))
|
||||
Self(AtomicU64::new((random::next_u32_secure()/2) as u64))
|
||||
}
|
||||
|
||||
/// Get the value most recently used to send a packet.
|
||||
|
@ -59,132 +58,51 @@ impl CounterValue {
|
|||
}
|
||||
|
||||
/// Incoming packet deduplication and replay protection window.
|
||||
pub(crate) struct CounterWindowAlt(RwLock<(u32, [u32; COUNTER_MAX_DELTA as usize])>);
|
||||
|
||||
impl CounterWindowAlt {
|
||||
#[inline(always)]
|
||||
pub fn new(initial: u32) -> Self {
|
||||
Self(RwLock::new((initial, std::array::from_fn(|_| initial))))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn message_received(&self, received_counter_value: u32) -> bool {
|
||||
let idx = (received_counter_value % COUNTER_MAX_DELTA) as usize;
|
||||
let data = self.0.read().unwrap();
|
||||
let max_counter_seen = data.0;
|
||||
let lower_window = max_counter_seen.wrapping_sub(COUNTER_MAX_DELTA / 2);
|
||||
let upper_window = max_counter_seen.wrapping_add(COUNTER_MAX_DELTA / 2);
|
||||
if lower_window < upper_window {
|
||||
if (lower_window <= received_counter_value) & (received_counter_value < upper_window) {
|
||||
if data.1[idx] != received_counter_value {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (lower_window <= received_counter_value) | (received_counter_value < upper_window) {
|
||||
if data.1[idx] != received_counter_value {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn message_authenticated(&self, received_counter_value: u32) -> bool {
|
||||
let idx = (received_counter_value % COUNTER_MAX_DELTA) as usize;
|
||||
let mut data = self.0.write().unwrap();
|
||||
let max_counter_seen = data.0;
|
||||
let lower_window = max_counter_seen.wrapping_sub(COUNTER_MAX_DELTA / 2);
|
||||
let upper_window = max_counter_seen.wrapping_add(COUNTER_MAX_DELTA / 2);
|
||||
if lower_window < upper_window {
|
||||
if (lower_window <= received_counter_value) & (received_counter_value < upper_window) {
|
||||
if data.1[idx] != received_counter_value {
|
||||
data.1[idx] = received_counter_value;
|
||||
data.0 = max_counter_seen.max(received_counter_value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (lower_window <= received_counter_value) | (received_counter_value < upper_window) {
|
||||
if data.1[idx] != received_counter_value {
|
||||
data.1[idx] = received_counter_value;
|
||||
data.0 = (max_counter_seen as i32).max(received_counter_value as i32) as u32;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CounterWindow(Mutex<Option<[u64; COUNTER_MAX_ALLOWED_OOO as usize]>>);
|
||||
pub(crate) struct CounterWindow(AtomicBool, AtomicBool, [AtomicU32; COUNTER_MAX_ALLOWED_OOO]);
|
||||
|
||||
impl CounterWindow {
|
||||
#[inline(always)]
|
||||
pub fn new(initial: u32) -> Self {
|
||||
// we want our nonces to wrap at the exact same time that the counter wraps, so we shift them up to the u64 boundary
|
||||
let initial_nonce = (initial as u64).wrapping_shl(32);
|
||||
Self(Mutex::new(Some([initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize])))
|
||||
Self(AtomicBool::new(true), AtomicBool::new(false), std::array::from_fn(|_| AtomicU32::new(initial)))
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn new_uninit() -> Self {
|
||||
Self(Mutex::new(None))
|
||||
Self(AtomicBool::new(false), AtomicBool::new(false), std::array::from_fn(|_| AtomicU32::new(0)))
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn init(&self, initial: u32) {
|
||||
let initial_nonce = (initial as u64).wrapping_shl(6);
|
||||
let mut data = self.0.lock().unwrap();
|
||||
*data = Some([initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize]);
|
||||
pub fn init_authenticated(&self, received_counter_value: u32) {
|
||||
self.1.store((u32::MAX/4 < received_counter_value) & (received_counter_value <= u32::MAX/4*3), Ordering::SeqCst);
|
||||
for i in 1..COUNTER_MAX_ALLOWED_OOO {
|
||||
self.2[i].store(received_counter_value, Ordering::SeqCst);
|
||||
}
|
||||
self.0.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
|
||||
#[inline(always)]
|
||||
pub fn message_received(&self, received_counter_value: u32, received_fragment_no: u8) -> bool {
|
||||
let fragment_nonce = (received_counter_value as u64).wrapping_shl(32) | (received_fragment_no as u64);
|
||||
//everything past this point must be atomic, i.e. these instructions must be run mutually exclusive to completion;
|
||||
//atomic instructions are only ever atomic within themselves;
|
||||
//sequentially consistent atomics do not guarantee that the thread is not preempted between individual atomic instructions
|
||||
if let Some(history) = self.0.lock().unwrap().as_mut() {
|
||||
let mut is_in = false;
|
||||
let mut idx = 0;
|
||||
let mut smallest = fragment_nonce;
|
||||
for i in 0..history.len() {
|
||||
let nonce = history[i];
|
||||
is_in |= nonce == fragment_nonce;
|
||||
let delta = (smallest as i64).wrapping_sub(nonce as i64);
|
||||
if delta > 0 {
|
||||
smallest = nonce;
|
||||
idx = i;
|
||||
}
|
||||
pub fn message_received(&self, received_counter_value: u32) -> bool {
|
||||
if self.0.load(Ordering::SeqCst) {
|
||||
let idx = (received_counter_value % COUNTER_MAX_ALLOWED_OOO as u32) as usize;
|
||||
let pre = self.2[idx].load(Ordering::SeqCst);
|
||||
if self.1.load(Ordering::Relaxed) {
|
||||
return pre < received_counter_value;
|
||||
} else {
|
||||
return (pre as i32) < (received_counter_value as i32);
|
||||
}
|
||||
if !is_in & (smallest != fragment_nonce) {
|
||||
history[idx] = fragment_nonce;
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn purge(&self, inauthentic_counter_value: u32, inauthentic_fragment_no: u8) {
|
||||
let inauthentic_nonce = (inauthentic_counter_value as u64).wrapping_shl(32) | (inauthentic_fragment_no as u64);
|
||||
//everything past this point must be atomic, i.e. these instructions must be run mutually exclusive to completion;
|
||||
//atomic instructions are only ever atomic within themselves;
|
||||
//sequentially consistent atomics do not guarantee that the thread is not preempted between individual atomic instructions
|
||||
if let Some(history) = self.0.lock().unwrap().as_mut() {
|
||||
let mut idx = 0;
|
||||
let mut smallest = history[0];
|
||||
for i in 0..history.len() {
|
||||
let nonce = history[i];
|
||||
if nonce == inauthentic_nonce {
|
||||
idx = i;
|
||||
} else {
|
||||
let delta = (smallest as i64).wrapping_sub(nonce as i64);
|
||||
if delta > 0 {
|
||||
smallest = nonce;
|
||||
}
|
||||
}
|
||||
}
|
||||
history[idx] = smallest;
|
||||
pub fn message_authenticated(&self, received_counter_value: u32) -> bool {
|
||||
//if a valid message is received but one of its fragments was lost, it can technically be replayed, since the message is incomplete, we know it still exists in the gather array, so the gather array will deduplicate the replayed message
|
||||
//eventually the counter of that message will be too OOO to be accepted anymore so it can't be used to DOS
|
||||
let idx = (received_counter_value % COUNTER_MAX_ALLOWED_OOO as u32) as usize;
|
||||
if self.1.swap((u32::MAX/4 < received_counter_value) & (received_counter_value <= u32::MAX/4*3), Ordering::SeqCst) {
|
||||
return self.2[idx].fetch_max(received_counter_value, Ordering::SeqCst) < received_counter_value;
|
||||
} else {
|
||||
let pre_as_signed: &AtomicI32 = unsafe {mem::transmute(&self.2[idx])};
|
||||
return pre_as_signed.fetch_max(received_counter_value as i32, Ordering::SeqCst) < received_counter_value as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,56 +229,36 @@ mod tests {
|
|||
#[test]
|
||||
fn counter_window() {
|
||||
let mut rng = 84632;
|
||||
let mut counter = u32::MAX - 16;
|
||||
let mut fragment_no: u8 = 0;
|
||||
let mut history = Vec::<(u32, u8)>::new();
|
||||
let mut counter = 1u32;
|
||||
let mut history = Vec::new();
|
||||
|
||||
let mut w = CounterWindow::new(counter.wrapping_sub(1));
|
||||
let mut w = CounterWindow::new(counter);
|
||||
for i in 0..1000000 {
|
||||
let p = xorshift64(&mut rng) as f32/(u32::MAX as f32 + 1.0);
|
||||
let c;
|
||||
let f;
|
||||
if p < 0.5 {
|
||||
let r = xorshift64(&mut rng);
|
||||
c = counter.wrapping_add(r%(COUNTER_MAX_ALLOWED_OOO - 2) as u32 + 1);
|
||||
f = 0;
|
||||
} else if p < 0.7 {
|
||||
fragment_no = u8::min(fragment_no + 1, 63);
|
||||
c = counter;
|
||||
f = fragment_no;
|
||||
} else if p < 0.8 {
|
||||
counter = counter.wrapping_add(1);
|
||||
fragment_no = 0;
|
||||
c = counter;
|
||||
f = fragment_no;
|
||||
} else if p < 0.9 {
|
||||
if history.len() > 0 {
|
||||
let idx = xorshift64(&mut rng) as usize%history.len();
|
||||
let (c, f) = history[idx];
|
||||
assert!(!w.message_received(c, f));
|
||||
let c = history[idx];
|
||||
assert!(!w.message_authenticated(c));
|
||||
}
|
||||
continue;
|
||||
} else if p < 0.9995 {
|
||||
} else {
|
||||
c = xorshift64(&mut rng);
|
||||
f = (xorshift64(&mut rng)%64) as u8;
|
||||
if w.message_received(c, f) {
|
||||
w.purge(c, f);
|
||||
}
|
||||
w.message_received(c);
|
||||
continue;
|
||||
} else {
|
||||
//simulate rekeying
|
||||
counter = xorshift64(&mut rng);
|
||||
fragment_no = 0;
|
||||
w = CounterWindow::new(counter.wrapping_sub(1));
|
||||
history = Vec::<(u32, u8)>::new();
|
||||
c = counter;
|
||||
f = fragment_no;
|
||||
}
|
||||
if history.contains(&(c, f)) {
|
||||
assert!(!w.message_received(c, f));
|
||||
if history.contains(&c) {
|
||||
assert!(!w.message_authenticated(c));
|
||||
} else {
|
||||
assert!(w.message_received(c, f));
|
||||
history.push((c, f));
|
||||
assert!(w.message_authenticated(c));
|
||||
history.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -487,7 +487,7 @@ impl<Application: ApplicationLayer> ReceiveContext<Application> {
|
|||
{
|
||||
if let Some(session) = app.lookup_session(local_session_id) {
|
||||
if verify_header_check_code(incoming_packet, &session.header_check_cipher) {
|
||||
if session.receive_window.message_received(counter, fragment_no) {
|
||||
if session.receive_window.message_received(counter) {
|
||||
let canonical_header = CanonicalHeader::make(local_session_id, packet_type, counter);
|
||||
if fragment_count > 1 {
|
||||
if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count {
|
||||
|
@ -664,31 +664,33 @@ impl<Application: ApplicationLayer> ReceiveContext<Application> {
|
|||
session_key.return_receive_cipher(c);
|
||||
|
||||
if aead_authentication_ok {
|
||||
// Select this key as the new default if it's newer than the current key.
|
||||
if p > 0
|
||||
&& state.session_keys[state.cur_session_key_idx]
|
||||
.as_ref()
|
||||
.map_or(true, |old| old.creation_counter < session_key.creation_counter)
|
||||
{
|
||||
drop(state);
|
||||
let mut state = session.state.write().unwrap();
|
||||
state.cur_session_key_idx = key_idx;
|
||||
for i in 0..KEY_HISTORY_SIZE {
|
||||
if i != key_idx {
|
||||
if let Some(old_key) = state.session_keys[key_idx].as_ref() {
|
||||
// Release pooled cipher memory from old keys.
|
||||
old_key.receive_cipher_pool.lock().unwrap().clear();
|
||||
old_key.send_cipher_pool.lock().unwrap().clear();
|
||||
if session.receive_window.message_authenticated(counter) {
|
||||
// Select this key as the new default if it's newer than the current key.
|
||||
if p > 0
|
||||
&& state.session_keys[state.cur_session_key_idx]
|
||||
.as_ref()
|
||||
.map_or(true, |old| old.creation_counter < session_key.creation_counter)
|
||||
{
|
||||
drop(state);
|
||||
let mut state = session.state.write().unwrap();
|
||||
state.cur_session_key_idx = key_idx;
|
||||
for i in 0..KEY_HISTORY_SIZE {
|
||||
if i != key_idx {
|
||||
if let Some(old_key) = state.session_keys[key_idx].as_ref() {
|
||||
// Release pooled cipher memory from old keys.
|
||||
old_key.receive_cipher_pool.lock().unwrap().clear();
|
||||
old_key.send_cipher_pool.lock().unwrap().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if packet_type == PACKET_TYPE_DATA {
|
||||
return Ok(ReceiveResult::OkData(&mut data_buf[..data_len]));
|
||||
} else {
|
||||
unlikely_branch();
|
||||
return Ok(ReceiveResult::Ok);
|
||||
if packet_type == PACKET_TYPE_DATA {
|
||||
return Ok(ReceiveResult::OkData(&mut data_buf[..data_len]));
|
||||
} else {
|
||||
unlikely_branch();
|
||||
return Ok(ReceiveResult::Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1158,7 +1160,7 @@ impl<Application: ApplicationLayer> ReceiveContext<Application> {
|
|||
last_ratchet_count + 1,
|
||||
hybrid_kk.is_some(),
|
||||
);
|
||||
session.receive_window.init(counter);
|
||||
session.receive_window.init_authenticated(counter);
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// packet encoding for post-noise session start ack
|
||||
|
|
Loading…
Add table
Reference in a new issue