mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 10:14:35 +00:00
multi_fix: harden peer/session handling, tighten foreign-network trust, and improve web client metadata (#1999)
* machine-id should be scoped unbder same user-id * feat: report device os metadata to console * fix sync root key cause packet loss * fix tun packet not invalid * fix faketcp cause lat jitter * fix some packet not decrypt * fix peer info patch, improve performance of update self info * fix foreign credential identity mismatch handling
This commit is contained in:
@@ -212,7 +212,12 @@ impl PeerSessionStore {
|
||||
.clone();
|
||||
session.check_encrypt_algo_same(&send_algorithm, &recv_algorithm)?;
|
||||
session.check_or_set_peer_static_pubkey(peer_static_pubkey)?;
|
||||
session.sync_root_key(root_key, b_session_generation, initial_epoch);
|
||||
session.sync_root_key(
|
||||
root_key,
|
||||
b_session_generation,
|
||||
initial_epoch,
|
||||
matches!(action, PeerSessionAction::Sync),
|
||||
);
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
@@ -352,6 +357,33 @@ impl EpochRxSlot {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct SyncRxGrace {
|
||||
slots: [[EpochRxSlot; 2]; 2],
|
||||
expires_at_ms: u64,
|
||||
valid: bool,
|
||||
}
|
||||
|
||||
impl SyncRxGrace {
|
||||
fn clear(&mut self) {
|
||||
self.slots = [[EpochRxSlot::default(), EpochRxSlot::default()]; 2];
|
||||
self.expires_at_ms = 0;
|
||||
self.valid = false;
|
||||
}
|
||||
|
||||
fn refresh(&mut self, slots: [[EpochRxSlot; 2]; 2], expires_at_ms: u64) {
|
||||
self.slots = slots;
|
||||
self.expires_at_ms = expires_at_ms;
|
||||
self.valid = true;
|
||||
}
|
||||
|
||||
fn maybe_expire(&mut self, now_ms: u64) {
|
||||
if self.valid && now_ms >= self.expires_at_ms {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PeerSession {
|
||||
peer_id: PeerId,
|
||||
root_key: RwLock<[u8; 32]>,
|
||||
@@ -365,6 +397,8 @@ pub struct PeerSession {
|
||||
|
||||
rx_slots: Mutex<[[EpochRxSlot; 2]; 2]>,
|
||||
key_cache: Mutex<[[EpochKeySlot; 2]; 2]>,
|
||||
sync_rx_grace: Mutex<SyncRxGrace>,
|
||||
sync_rx_grace_expires_at_ms: AtomicU64,
|
||||
|
||||
send_cipher_algorithm: String,
|
||||
recv_cipher_algorithm: String,
|
||||
@@ -389,6 +423,11 @@ impl std::fmt::Debug for PeerSession {
|
||||
.field("send_packets_since_epoch", &self.send_packets_since_epoch)
|
||||
.field("rx_slots", &self.rx_slots)
|
||||
.field("key_cache", &self.key_cache)
|
||||
.field("sync_rx_grace", &self.sync_rx_grace)
|
||||
.field(
|
||||
"sync_rx_grace_expires_at_ms",
|
||||
&self.sync_rx_grace_expires_at_ms,
|
||||
)
|
||||
.field("send_cipher_algorithm", &self.send_cipher_algorithm)
|
||||
.field("recv_cipher_algorithm", &self.recv_cipher_algorithm)
|
||||
.finish()
|
||||
@@ -405,6 +444,10 @@ impl PeerSession {
|
||||
/// traffic may want to increase this value; low-latency or tightly
|
||||
/// resource-constrained deployments may lower it.
|
||||
const EVICT_IDLE_AFTER_MS: u64 = 30_000;
|
||||
/// Keep the pre-sync receive windows alive briefly so in-flight packets
|
||||
/// from the previous epochs are still accepted after a shared session is
|
||||
/// synced in place by another connection.
|
||||
const SYNC_RX_GRACE_AFTER_MS: u64 = 5_000;
|
||||
|
||||
/// Maximum number of packets to send in a single epoch before forcing
|
||||
/// a key/epoch rotation.
|
||||
@@ -458,6 +501,8 @@ impl PeerSession {
|
||||
send_packets_since_epoch: AtomicU64::new(0),
|
||||
rx_slots: Mutex::new(rx_slots),
|
||||
key_cache: Mutex::new(key_cache),
|
||||
sync_rx_grace: Mutex::new(SyncRxGrace::default()),
|
||||
sync_rx_grace_expires_at_ms: AtomicU64::new(0),
|
||||
send_cipher_algorithm,
|
||||
recv_cipher_algorithm,
|
||||
invalidated: AtomicBool::new(false),
|
||||
@@ -540,7 +585,15 @@ impl PeerSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync_root_key(&self, root_key: [u8; 32], session_generation: u32, initial_epoch: u32) {
|
||||
pub fn sync_root_key(
|
||||
&self,
|
||||
root_key: [u8; 32],
|
||||
session_generation: u32,
|
||||
initial_epoch: u32,
|
||||
preserve_rx_grace: bool,
|
||||
) {
|
||||
let old_root_key = self.root_key();
|
||||
let can_preserve_rx_grace = preserve_rx_grace && old_root_key == root_key;
|
||||
{
|
||||
let mut g = self.root_key.write().unwrap();
|
||||
*g = root_key;
|
||||
@@ -557,6 +610,16 @@ impl PeerSession {
|
||||
|
||||
{
|
||||
let mut rx = self.rx_slots.lock().unwrap();
|
||||
let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap();
|
||||
if can_preserve_rx_grace {
|
||||
let expires_at_ms = now_ms().saturating_add(Self::SYNC_RX_GRACE_AFTER_MS);
|
||||
sync_rx_grace.refresh(*rx, expires_at_ms);
|
||||
self.sync_rx_grace_expires_at_ms
|
||||
.store(expires_at_ms, Ordering::Relaxed);
|
||||
} else {
|
||||
sync_rx_grace.clear();
|
||||
self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed);
|
||||
}
|
||||
for dir in 0..2 {
|
||||
rx[dir][0].clear();
|
||||
rx[dir][1].clear();
|
||||
@@ -598,21 +661,17 @@ impl PeerSession {
|
||||
key
|
||||
}
|
||||
|
||||
fn get_encryptor(&self, epoch: u32, dir: usize, is_send: bool) -> Option<Arc<dyn Encryptor>> {
|
||||
let generation = self.session_generation();
|
||||
let rx = self.rx_slots.lock().unwrap();
|
||||
let send_epoch = self.send_epoch.load(Ordering::Relaxed);
|
||||
let allowed = epoch == send_epoch
|
||||
|| rx[dir][0].valid && rx[dir][0].epoch == epoch
|
||||
|| rx[dir][1].valid && rx[dir][1].epoch == epoch;
|
||||
if !allowed {
|
||||
return None;
|
||||
}
|
||||
|
||||
fn get_or_create_encryptor(
|
||||
&self,
|
||||
epoch: u32,
|
||||
dir: usize,
|
||||
generation: u32,
|
||||
is_send: bool,
|
||||
) -> Arc<dyn Encryptor> {
|
||||
let mut guard = self.key_cache.lock().unwrap();
|
||||
for slot in guard[dir].iter_mut() {
|
||||
if slot.valid && slot.epoch == epoch && slot.generation == generation {
|
||||
return Some(slot.get_encryptor(is_send));
|
||||
return slot.get_encryptor(is_send);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +694,7 @@ impl PeerSession {
|
||||
guard[dir][1] = slot;
|
||||
}
|
||||
|
||||
Some(ret)
|
||||
ret
|
||||
}
|
||||
|
||||
fn maybe_rotate_epoch(&self, now_ms: u64) {
|
||||
@@ -698,9 +757,38 @@ impl PeerSession {
|
||||
}
|
||||
}
|
||||
|
||||
fn epoch_in_slots(slots: &[EpochRxSlot; 2], epoch: u32) -> bool {
|
||||
slots[0].valid && slots[0].epoch == epoch || slots[1].valid && slots[1].epoch == epoch
|
||||
}
|
||||
|
||||
fn sync_rx_grace_active(&self, now_ms: u64) -> bool {
|
||||
let expires_at_ms = self.sync_rx_grace_expires_at_ms.load(Ordering::Relaxed);
|
||||
if expires_at_ms == 0 {
|
||||
return false;
|
||||
}
|
||||
if now_ms < expires_at_ms {
|
||||
return true;
|
||||
}
|
||||
self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed);
|
||||
false
|
||||
}
|
||||
|
||||
fn check_replay(&self, epoch: u32, seq: u64, dir: usize, now_ms: u64) -> bool {
|
||||
let mut rx = self.rx_slots.lock().unwrap();
|
||||
Self::evict_old_rx_slots(&mut rx, now_ms);
|
||||
let mut sync_rx_grace = if self.sync_rx_grace_active(now_ms) {
|
||||
let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap();
|
||||
sync_rx_grace.maybe_expire(now_ms);
|
||||
if sync_rx_grace.valid {
|
||||
Self::evict_old_rx_slots(&mut sync_rx_grace.slots, now_ms);
|
||||
Some(sync_rx_grace)
|
||||
} else {
|
||||
self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let send_epoch = self.send_epoch.load(Ordering::Relaxed);
|
||||
{
|
||||
let mut key_cache = self.key_cache.lock().unwrap();
|
||||
@@ -712,7 +800,10 @@ impl PeerSession {
|
||||
let e = key_cache[d][s].epoch;
|
||||
let allowed = e == send_epoch
|
||||
|| rx[d][0].valid && rx[d][0].epoch == e
|
||||
|| rx[d][1].valid && rx[d][1].epoch == e;
|
||||
|| rx[d][1].valid && rx[d][1].epoch == e
|
||||
|| sync_rx_grace
|
||||
.as_ref()
|
||||
.is_some_and(|g| Self::epoch_in_slots(&g.slots[d], e));
|
||||
if !allowed {
|
||||
key_cache[d][s].valid = false;
|
||||
}
|
||||
@@ -720,6 +811,18 @@ impl PeerSession {
|
||||
}
|
||||
}
|
||||
|
||||
if sync_rx_grace
|
||||
.as_ref()
|
||||
.is_some_and(|g| Self::epoch_in_slots(&g.slots[dir], epoch))
|
||||
{
|
||||
for slot in sync_rx_grace.as_mut().unwrap().slots[dir].iter_mut() {
|
||||
if slot.valid && slot.epoch == epoch {
|
||||
slot.last_rx_ms = now_ms;
|
||||
return slot.window.accept(seq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !rx[dir][0].valid {
|
||||
rx[dir][0] = EpochRxSlot {
|
||||
epoch,
|
||||
@@ -777,9 +880,7 @@ impl PeerSession {
|
||||
}
|
||||
let dir = Self::dir_for_sender(sender_peer_id, receiver_peer_id);
|
||||
let (epoch, _seq, nonce_bytes) = self.next_nonce(dir);
|
||||
let encryptor = self
|
||||
.get_encryptor(epoch, dir, true)
|
||||
.ok_or_else(|| anyhow!("no key for epoch"))?;
|
||||
let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), true);
|
||||
if let Err(e) = encryptor.encrypt_with_nonce(pkt, Some(nonce_bytes.as_slice())) {
|
||||
tracing::warn!(
|
||||
peer_id = ?self.peer_id,
|
||||
@@ -816,9 +917,7 @@ impl PeerSession {
|
||||
));
|
||||
}
|
||||
|
||||
let encryptor = self
|
||||
.get_encryptor(epoch, dir, false)
|
||||
.ok_or_else(|| anyhow!("no key for epoch"))?;
|
||||
let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), false);
|
||||
if let Err(e) = encryptor.decrypt(ciphertext_with_tail) {
|
||||
let count = self.decrypt_fail_count.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if count >= Self::DECRYPT_FAIL_THRESHOLD {
|
||||
@@ -974,7 +1073,7 @@ mod tests {
|
||||
assert!(s.check_replay(0, 1, 0, now));
|
||||
|
||||
// Sync with initial_epoch=2 (simulating a Sync action)
|
||||
s.sync_root_key(root_key, 2, 2);
|
||||
s.sync_root_key(root_key, 2, 2, true);
|
||||
|
||||
// Remote peer is still sending at epoch 0 — should be accepted
|
||||
// (rx_slots were cleared, so the first packet establishes the epoch)
|
||||
@@ -983,4 +1082,85 @@ mod tests {
|
||||
"packets at old epoch should be accepted after sync"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_root_key_keeps_previous_epochs_during_grace_window() {
|
||||
let peer_id: PeerId = 10;
|
||||
let root_key = PeerSession::new_root_key();
|
||||
let s = PeerSession::new(
|
||||
peer_id,
|
||||
root_key,
|
||||
1,
|
||||
0,
|
||||
"aes-256-gcm".to_string(),
|
||||
"aes-256-gcm".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
let now = now_ms();
|
||||
assert!(s.check_replay(0, 0, 0, now));
|
||||
assert!(s.check_replay(1, 0, 0, now + 1));
|
||||
|
||||
s.sync_root_key(root_key, 2, 2, true);
|
||||
|
||||
// The first packet after sync may already use the new epoch.
|
||||
assert!(s.check_replay(2, 0, 0, now + 2));
|
||||
// Older in-flight packets from pre-sync epochs should still be accepted
|
||||
// during the grace period, regardless of arrival order.
|
||||
assert!(s.check_replay(1, 1, 0, now + 3));
|
||||
assert!(s.check_replay(0, 1, 0, now + 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_root_key_expires_previous_epochs_after_grace_window() {
|
||||
let peer_id: PeerId = 10;
|
||||
let root_key = PeerSession::new_root_key();
|
||||
let s = PeerSession::new(
|
||||
peer_id,
|
||||
root_key,
|
||||
1,
|
||||
0,
|
||||
"aes-256-gcm".to_string(),
|
||||
"aes-256-gcm".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
let now = now_ms();
|
||||
assert!(s.check_replay(0, 0, 0, now));
|
||||
assert!(s.check_replay(1, 0, 0, now + 1));
|
||||
|
||||
s.sync_root_key(root_key, 2, 2, true);
|
||||
assert!(s.check_replay(2, 0, 0, now + 2));
|
||||
|
||||
assert!(
|
||||
!s.check_replay(0, 1, 0, now + PeerSession::SYNC_RX_GRACE_AFTER_MS + 3),
|
||||
"old epochs should stop being accepted once the sync grace window expires"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_root_key_does_not_preserve_previous_epochs_when_root_key_changes() {
|
||||
let peer_id: PeerId = 10;
|
||||
let root_key = PeerSession::new_root_key();
|
||||
let s = PeerSession::new(
|
||||
peer_id,
|
||||
root_key,
|
||||
1,
|
||||
0,
|
||||
"aes-256-gcm".to_string(),
|
||||
"aes-256-gcm".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
let now = now_ms();
|
||||
assert!(s.check_replay(0, 0, 0, now));
|
||||
assert!(s.check_replay(1, 0, 0, now + 1));
|
||||
|
||||
s.sync_root_key(PeerSession::new_root_key(), 2, 2, true);
|
||||
assert!(s.check_replay(2, 0, 0, now + 2));
|
||||
assert!(
|
||||
!s.check_replay(1, 1, 0, now + 3),
|
||||
"old epochs should not be preserved when sync replaces the root key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user