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;
|
pub(crate) const KEY_HISTORY_SIZE: usize = 3;
|
||||||
|
|
||||||
/// Maximum difference between out-of-order incoming packet counters, and size of deduplication buffer.
|
/// 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;
|
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
|
// 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::{
|
use std::{sync::{
|
||||||
atomic::{AtomicU64, Ordering},
|
atomic::{AtomicU64, Ordering, AtomicU32, AtomicI32, AtomicBool}
|
||||||
Mutex, RwLock,
|
}, mem};
|
||||||
};
|
|
||||||
|
|
||||||
use zerotier_crypto::random;
|
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.
|
/// Outgoing packet counter with strictly ordered atomic semantics.
|
||||||
///
|
///
|
||||||
|
@ -20,7 +19,7 @@ impl Counter {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
// Using a random value has no security implication. Zero would be fine. This just
|
// Using a random value has no security implication. Zero would be fine. This just
|
||||||
// helps randomize packet contents a bit.
|
// 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.
|
/// Get the value most recently used to send a packet.
|
||||||
|
@ -59,132 +58,51 @@ impl CounterValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Incoming packet deduplication and replay protection window.
|
/// Incoming packet deduplication and replay protection window.
|
||||||
pub(crate) struct CounterWindowAlt(RwLock<(u32, [u32; COUNTER_MAX_DELTA as usize])>);
|
pub(crate) struct CounterWindow(AtomicBool, AtomicBool, [AtomicU32; COUNTER_MAX_ALLOWED_OOO]);
|
||||||
|
|
||||||
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]>>);
|
|
||||||
|
|
||||||
impl CounterWindow {
|
impl CounterWindow {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn new(initial: u32) -> Self {
|
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
|
Self(AtomicBool::new(true), AtomicBool::new(false), std::array::from_fn(|_| AtomicU32::new(initial)))
|
||||||
let initial_nonce = (initial as u64).wrapping_shl(32);
|
|
||||||
Self(Mutex::new(Some([initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize])))
|
|
||||||
}
|
}
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn new_uninit() -> Self {
|
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)]
|
#[inline(always)]
|
||||||
pub fn init(&self, initial: u32) {
|
pub fn init_authenticated(&self, received_counter_value: u32) {
|
||||||
let initial_nonce = (initial as u64).wrapping_shl(6);
|
self.1.store((u32::MAX/4 < received_counter_value) & (received_counter_value <= u32::MAX/4*3), Ordering::SeqCst);
|
||||||
let mut data = self.0.lock().unwrap();
|
for i in 1..COUNTER_MAX_ALLOWED_OOO {
|
||||||
*data = Some([initial_nonce; COUNTER_MAX_ALLOWED_OOO as usize]);
|
self.2[i].store(received_counter_value, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
self.0.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn message_received(&self, received_counter_value: u32, received_fragment_no: u8) -> bool {
|
pub fn message_received(&self, received_counter_value: u32) -> bool {
|
||||||
let fragment_nonce = (received_counter_value as u64).wrapping_shl(32) | (received_fragment_no as u64);
|
if self.0.load(Ordering::SeqCst) {
|
||||||
//everything past this point must be atomic, i.e. these instructions must be run mutually exclusive to completion;
|
let idx = (received_counter_value % COUNTER_MAX_ALLOWED_OOO as u32) as usize;
|
||||||
//atomic instructions are only ever atomic within themselves;
|
let pre = self.2[idx].load(Ordering::SeqCst);
|
||||||
//sequentially consistent atomics do not guarantee that the thread is not preempted between individual atomic instructions
|
if self.1.load(Ordering::Relaxed) {
|
||||||
if let Some(history) = self.0.lock().unwrap().as_mut() {
|
return pre < received_counter_value;
|
||||||
let mut is_in = false;
|
} else {
|
||||||
let mut idx = 0;
|
return (pre as i32) < (received_counter_value as i32);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !is_in & (smallest != fragment_nonce) {
|
|
||||||
history[idx] = fragment_nonce;
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn purge(&self, inauthentic_counter_value: u32, inauthentic_fragment_no: u8) {
|
pub fn message_authenticated(&self, received_counter_value: u32) -> bool {
|
||||||
let inauthentic_nonce = (inauthentic_counter_value as u64).wrapping_shl(32) | (inauthentic_fragment_no as u64);
|
//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
|
||||||
//everything past this point must be atomic, i.e. these instructions must be run mutually exclusive to completion;
|
//eventually the counter of that message will be too OOO to be accepted anymore so it can't be used to DOS
|
||||||
//atomic instructions are only ever atomic within themselves;
|
let idx = (received_counter_value % COUNTER_MAX_ALLOWED_OOO as u32) as usize;
|
||||||
//sequentially consistent atomics do not guarantee that the thread is not preempted between individual atomic instructions
|
if self.1.swap((u32::MAX/4 < received_counter_value) & (received_counter_value <= u32::MAX/4*3), Ordering::SeqCst) {
|
||||||
if let Some(history) = self.0.lock().unwrap().as_mut() {
|
return self.2[idx].fetch_max(received_counter_value, Ordering::SeqCst) < received_counter_value;
|
||||||
let mut idx = 0;
|
} else {
|
||||||
let mut smallest = history[0];
|
let pre_as_signed: &AtomicI32 = unsafe {mem::transmute(&self.2[idx])};
|
||||||
for i in 0..history.len() {
|
return pre_as_signed.fetch_max(received_counter_value as i32, Ordering::SeqCst) < received_counter_value as i32;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,56 +229,36 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn counter_window() {
|
fn counter_window() {
|
||||||
let mut rng = 84632;
|
let mut rng = 84632;
|
||||||
let mut counter = u32::MAX - 16;
|
let mut counter = 1u32;
|
||||||
let mut fragment_no: u8 = 0;
|
let mut history = Vec::new();
|
||||||
let mut history = Vec::<(u32, u8)>::new();
|
|
||||||
|
|
||||||
let mut w = CounterWindow::new(counter.wrapping_sub(1));
|
let mut w = CounterWindow::new(counter);
|
||||||
for i in 0..1000000 {
|
for i in 0..1000000 {
|
||||||
let p = xorshift64(&mut rng) as f32/(u32::MAX as f32 + 1.0);
|
let p = xorshift64(&mut rng) as f32/(u32::MAX as f32 + 1.0);
|
||||||
let c;
|
let c;
|
||||||
let f;
|
|
||||||
if p < 0.5 {
|
if p < 0.5 {
|
||||||
let r = xorshift64(&mut rng);
|
let r = xorshift64(&mut rng);
|
||||||
c = counter.wrapping_add(r%(COUNTER_MAX_ALLOWED_OOO - 2) as u32 + 1);
|
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 {
|
} else if p < 0.8 {
|
||||||
counter = counter.wrapping_add(1);
|
counter = counter.wrapping_add(1);
|
||||||
fragment_no = 0;
|
|
||||||
c = counter;
|
c = counter;
|
||||||
f = fragment_no;
|
|
||||||
} else if p < 0.9 {
|
} else if p < 0.9 {
|
||||||
if history.len() > 0 {
|
if history.len() > 0 {
|
||||||
let idx = xorshift64(&mut rng) as usize%history.len();
|
let idx = xorshift64(&mut rng) as usize%history.len();
|
||||||
let (c, f) = history[idx];
|
let c = history[idx];
|
||||||
assert!(!w.message_received(c, f));
|
assert!(!w.message_authenticated(c));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
} else if p < 0.9995 {
|
} else {
|
||||||
c = xorshift64(&mut rng);
|
c = xorshift64(&mut rng);
|
||||||
f = (xorshift64(&mut rng)%64) as u8;
|
w.message_received(c);
|
||||||
if w.message_received(c, f) {
|
|
||||||
w.purge(c, f);
|
|
||||||
}
|
|
||||||
continue;
|
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)) {
|
if history.contains(&c) {
|
||||||
assert!(!w.message_received(c, f));
|
assert!(!w.message_authenticated(c));
|
||||||
} else {
|
} else {
|
||||||
assert!(w.message_received(c, f));
|
assert!(w.message_authenticated(c));
|
||||||
history.push((c, f));
|
history.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -487,7 +487,7 @@ impl<Application: ApplicationLayer> ReceiveContext<Application> {
|
||||||
{
|
{
|
||||||
if let Some(session) = app.lookup_session(local_session_id) {
|
if let Some(session) = app.lookup_session(local_session_id) {
|
||||||
if verify_header_check_code(incoming_packet, &session.header_check_cipher) {
|
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);
|
let canonical_header = CanonicalHeader::make(local_session_id, packet_type, counter);
|
||||||
if fragment_count > 1 {
|
if fragment_count > 1 {
|
||||||
if fragment_count <= (MAX_FRAGMENTS as u8) && fragment_no < fragment_count {
|
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);
|
session_key.return_receive_cipher(c);
|
||||||
|
|
||||||
if aead_authentication_ok {
|
if aead_authentication_ok {
|
||||||
// Select this key as the new default if it's newer than the current key.
|
if session.receive_window.message_authenticated(counter) {
|
||||||
if p > 0
|
// Select this key as the new default if it's newer than the current key.
|
||||||
&& state.session_keys[state.cur_session_key_idx]
|
if p > 0
|
||||||
.as_ref()
|
&& state.session_keys[state.cur_session_key_idx]
|
||||||
.map_or(true, |old| old.creation_counter < session_key.creation_counter)
|
.as_ref()
|
||||||
{
|
.map_or(true, |old| old.creation_counter < session_key.creation_counter)
|
||||||
drop(state);
|
{
|
||||||
let mut state = session.state.write().unwrap();
|
drop(state);
|
||||||
state.cur_session_key_idx = key_idx;
|
let mut state = session.state.write().unwrap();
|
||||||
for i in 0..KEY_HISTORY_SIZE {
|
state.cur_session_key_idx = key_idx;
|
||||||
if i != key_idx {
|
for i in 0..KEY_HISTORY_SIZE {
|
||||||
if let Some(old_key) = state.session_keys[key_idx].as_ref() {
|
if i != key_idx {
|
||||||
// Release pooled cipher memory from old keys.
|
if let Some(old_key) = state.session_keys[key_idx].as_ref() {
|
||||||
old_key.receive_cipher_pool.lock().unwrap().clear();
|
// Release pooled cipher memory from old keys.
|
||||||
old_key.send_cipher_pool.lock().unwrap().clear();
|
old_key.receive_cipher_pool.lock().unwrap().clear();
|
||||||
|
old_key.send_cipher_pool.lock().unwrap().clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if packet_type == PACKET_TYPE_DATA {
|
if packet_type == PACKET_TYPE_DATA {
|
||||||
return Ok(ReceiveResult::OkData(&mut data_buf[..data_len]));
|
return Ok(ReceiveResult::OkData(&mut data_buf[..data_len]));
|
||||||
} else {
|
} else {
|
||||||
unlikely_branch();
|
unlikely_branch();
|
||||||
return Ok(ReceiveResult::Ok);
|
return Ok(ReceiveResult::Ok);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1158,7 +1160,7 @@ impl<Application: ApplicationLayer> ReceiveContext<Application> {
|
||||||
last_ratchet_count + 1,
|
last_ratchet_count + 1,
|
||||||
hybrid_kk.is_some(),
|
hybrid_kk.is_some(),
|
||||||
);
|
);
|
||||||
session.receive_window.init(counter);
|
session.receive_window.init_authenticated(counter);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
// packet encoding for post-noise session start ack
|
// packet encoding for post-noise session start ack
|
||||||
|
|
Loading…
Add table
Reference in a new issue