mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 17:45:44 +00:00
* perf(core): make data-plane idle check constant time Avoid scanning every DashMap shard for each peer packet when no data-plane flows are active. Publish the flow count before insertion and release it after removal so an Acquire load is a safe O(1) idle signal. Reject count overflow and underflow instead of silently saturating. * test(perf): add repeatable two-node netns benchmark Create isolated underlay namespaces, pin both EasyTier cores and iperf3 endpoints, and measure a single TCP flow in both directions over either UDP or TCP peer transport. Keep every iperf3 JSON result and emit directional medians while cleaning up processes and namespaces on every exit path. * perf(tcp): preserve native owned stream halves Let each VirtualTcpSocket adapter consume itself into independent read and write halves. Portable adapters retain the generic shared split as a default. Use lock-free Tokio owned halves for native TCP and Unix streams so tunnel I/O no longer takes the generic split mutex on every poll. Cover full-duplex traffic and write-half shutdown. * perf(packet): preserve ownership across the Host seam Introduce an opaque, move-only HostPacket that retains core packet storage while exposing only the raw IP payload. Clear private headers before handing storage back to a native TUN adapter. Use an ownership-preserving bounded channel for native ingress and egress. Keep explicit copy adapters for Vec and WASI boundaries, and verify allocation identity, backpressure, shutdown, and end-to-end delivery. * perf(udp): preserve packet ownership through sessions Carry EasyTier tunnel packets through UDP session queues as owned values. Reuse the existing tunnel header for session framing instead of copying payloads into a second packet and rebuilding them on receive. Keep completion delivery for the public datagram socket API while removing the unused completion channel from streaming tunnel sends. Avoid the unconditional receive-side clone before QUIC routing is known. * perf(peer): publish packet filters as immutable snapshots Replace per-packet async and synchronous registry locks with ArcSwap snapshots. Permanent filters now need no activity checks, while managed registrations retain explicit acquire/release visibility. Closing a managed registration marks it inactive before atomically removing it. Existing snapshots keep in-flight filters alive, and registration mutations prune inactive entries while preserving newest-first order. * perf(instance): give native hosts direct packet egress Let the core create one bounded HostPacket channel and transfer its receiver directly to a PacketEgressHost during startup. Native TUN runtimes now consume that receiver without the intermediate PacketSink channel and forwarding task. Keep PacketSinkEgress as the compatibility adapter for callback and test hosts, and make receiver installation one-shot across desktop, mobile, and disabled runtimes. * perf(crypto): restore accelerated native AEAD backends Move Ring and OpenSSL implementations behind the core Encryptor seam. Portable builds continue selecting only supported backends. Restore historical precedence: OpenSSL, Ring, then RustCrypto. Keep backend availability consistent across secure transports and cover fixed-nonce wire compatibility between implementations. * perf(udp): receive native datagrams into owned buffers Extend the portable UDP socket seam with an owned-datagram receive path. Keep a compatible default for portable hosts. Native Unix sockets write recvmsg output directly into the final BytesMut allocation. This removes the per-packet stack-to-heap copy introduced by the portable socket boundary without exposing native socket resources to core. * perf(data-plane): remove portable hot-path overhead Restore native throughput lost while generalizing the host and UDP session layers. Read packet policy once per send, update traffic counters through registry guards, and preserve packet ownership while UDP dispatch borrows stable session state. Move UDP shutdown monitoring into a control task so forwarding avoids a select future per packet. Bound native datagram storage to 8 KiB, reject oversized sends, and drop truncated Unix receives. Keep accelerated AEAD selection warning-free when portable crypto features are also built. Cover session bounds, truncation, and idle shutdown with regression tests. * fix(udp): preserve portable datagram receive semantics Keep the public portable receive capacity at the theoretical UDP maximum instead of silently shrinking it to the native fast-path limit. Apply the 8 KiB session boundary after a complete portable receive, so Windows cannot turn an oversized datagram into a fatal listener error and other adapters cannot dispatch a truncated prefix. Cover dropping an oversized packet while the same portable socket continues to deliver the following valid datagram. * fix(ci): align feature gating with backend selection Compile the Ring implementation in production only when OpenSSL is not selected, while retaining it for cross-backend unit tests. Remove stale test imports and assert UDP dispatch results so the strict workspace Clippy job passes without suppressing diagnostics.
429 lines
12 KiB
Rust
429 lines
12 KiB
Rust
//! Shared data-plane flow registration and ownership.
|
|
|
|
use std::{
|
|
net::{IpAddr, SocketAddr},
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicUsize, Ordering},
|
|
},
|
|
};
|
|
|
|
use atomic_shim::AtomicU64;
|
|
use dashmap::{DashMap, mapref::entry::Entry};
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
#[repr(u8)]
|
|
pub(crate) enum FlowKind {
|
|
Udp = 1,
|
|
Tcp = 2,
|
|
TcpListen = 3,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub(crate) struct FlowKey {
|
|
pub src: SocketAddr,
|
|
pub dst: SocketAddr,
|
|
pub kind: FlowKind,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) struct FlowCountChange {
|
|
pub previous: usize,
|
|
pub current: usize,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) struct FlowInsert {
|
|
pub replaced: bool,
|
|
pub count: FlowCountChange,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) struct FlowRemoval {
|
|
pub removed: bool,
|
|
pub count: FlowCountChange,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) struct FlowRetain {
|
|
pub removed: usize,
|
|
pub count: FlowCountChange,
|
|
}
|
|
|
|
pub(crate) struct FlowTable<V> {
|
|
entries: DashMap<FlowKey, RegisteredFlow<V>>,
|
|
count: AtomicUsize,
|
|
next_registration: AtomicU64,
|
|
}
|
|
|
|
struct RegisteredFlow<V> {
|
|
registration: u64,
|
|
value: V,
|
|
}
|
|
|
|
pub(crate) struct FlowLease<V> {
|
|
table: Arc<FlowTable<V>>,
|
|
entry: FlowKey,
|
|
registration: u64,
|
|
active: bool,
|
|
}
|
|
|
|
impl<V> FlowLease<V> {
|
|
pub fn register(table: Arc<FlowTable<V>>, entry: FlowKey, value: V) -> (Self, FlowInsert) {
|
|
let (registration, insert) = table.insert_registered(entry.clone(), value);
|
|
(
|
|
Self {
|
|
table,
|
|
entry,
|
|
registration,
|
|
active: true,
|
|
},
|
|
insert,
|
|
)
|
|
}
|
|
|
|
pub fn try_register(table: Arc<FlowTable<V>>, entry: FlowKey, value: V) -> Option<Self> {
|
|
let registration = table.try_insert_registered(entry.clone(), value)?;
|
|
Some(Self {
|
|
table,
|
|
entry,
|
|
registration,
|
|
active: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<V> Drop for FlowLease<V> {
|
|
fn drop(&mut self) {
|
|
if self.active {
|
|
self.table
|
|
.remove_registration(&self.entry, self.registration);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<V> Default for FlowTable<V> {
|
|
fn default() -> Self {
|
|
Self {
|
|
entries: DashMap::new(),
|
|
count: AtomicUsize::new(0),
|
|
next_registration: AtomicU64::new(1),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<V> FlowTable<V> {
|
|
pub fn count(&self) -> usize {
|
|
self.count.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Returns whether no flow is visible or being published.
|
|
///
|
|
/// New entries reserve their count before they become visible, while
|
|
/// removals release their count after the entry is gone. Consequently a
|
|
/// zero observed here is a safe fast-path signal without inspecting every
|
|
/// DashMap shard.
|
|
pub fn is_idle(&self) -> bool {
|
|
self.count.load(Ordering::Acquire) == 0
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.entries.len()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
|
|
pub fn contains_key(&self, entry: &FlowKey) -> bool {
|
|
self.entries.contains_key(entry)
|
|
}
|
|
|
|
pub fn contains_destination_ip(&self, destination: IpAddr) -> bool {
|
|
self.entries
|
|
.iter()
|
|
.any(|entry| entry.key().dst.ip() == destination)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn with_entry<R>(&self, entry: &FlowKey, f: impl FnOnce(&V) -> R) -> Option<R> {
|
|
self.entries.get(entry).map(|value| f(&value.value().value))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn insert(&self, entry: FlowKey, value: V) -> FlowInsert {
|
|
self.insert_registered(entry, value).1
|
|
}
|
|
|
|
fn insert_registered(&self, entry: FlowKey, value: V) -> (u64, FlowInsert) {
|
|
let registration = self.next_registration();
|
|
match self.entries.entry(entry) {
|
|
Entry::Occupied(mut occupied) => {
|
|
occupied.insert(RegisteredFlow {
|
|
registration,
|
|
value,
|
|
});
|
|
let count = self.count();
|
|
(
|
|
registration,
|
|
FlowInsert {
|
|
replaced: true,
|
|
count: FlowCountChange {
|
|
previous: count,
|
|
current: count,
|
|
},
|
|
},
|
|
)
|
|
}
|
|
Entry::Vacant(vacant) => {
|
|
// Reserve the count while holding the shard lock so retain cannot
|
|
// observe the entry before its count is accounted for.
|
|
let count = self.increment_count();
|
|
vacant.insert(RegisteredFlow {
|
|
registration,
|
|
value,
|
|
});
|
|
(
|
|
registration,
|
|
FlowInsert {
|
|
replaced: false,
|
|
count,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn try_insert(&self, entry: FlowKey, value: V) -> bool {
|
|
self.try_insert_registered(entry, value).is_some()
|
|
}
|
|
|
|
fn try_insert_registered(&self, entry: FlowKey, value: V) -> Option<u64> {
|
|
match self.entries.entry(entry) {
|
|
Entry::Occupied(_) => None,
|
|
Entry::Vacant(vacant) => {
|
|
let registration = self.next_registration();
|
|
self.increment_count();
|
|
vacant.insert(RegisteredFlow {
|
|
registration,
|
|
value,
|
|
});
|
|
Some(registration)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn remove(&self, entry: &FlowKey) -> FlowRemoval {
|
|
let removed = self.entries.remove(entry).is_some();
|
|
let count = if removed {
|
|
self.decrement_count_by(1)
|
|
} else {
|
|
let count = self.count();
|
|
FlowCountChange {
|
|
previous: count,
|
|
current: count,
|
|
}
|
|
};
|
|
FlowRemoval { removed, count }
|
|
}
|
|
|
|
pub fn retain(&self, mut f: impl FnMut(&FlowKey, &mut V) -> bool) -> FlowRetain {
|
|
let mut removed = 0;
|
|
self.entries.retain(|entry, value| {
|
|
let keep = f(entry, &mut value.value);
|
|
if !keep {
|
|
removed += 1;
|
|
}
|
|
keep
|
|
});
|
|
FlowRetain {
|
|
removed,
|
|
count: self.decrement_count_by(removed),
|
|
}
|
|
}
|
|
|
|
pub fn clear(&self) -> FlowRetain {
|
|
self.retain(|_, _| false)
|
|
}
|
|
|
|
fn increment_count(&self) -> FlowCountChange {
|
|
let previous = self
|
|
.count
|
|
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
|
|
count.checked_add(1)
|
|
})
|
|
.expect("flow count overflow");
|
|
FlowCountChange {
|
|
previous,
|
|
current: previous + 1,
|
|
}
|
|
}
|
|
|
|
fn next_registration(&self) -> u64 {
|
|
self.next_registration.fetch_add(1, Ordering::Relaxed)
|
|
}
|
|
|
|
fn remove_registration(&self, entry: &FlowKey, registration: u64) -> FlowRemoval {
|
|
let removed = self
|
|
.entries
|
|
.remove_if(entry, |_, flow| flow.registration == registration)
|
|
.is_some();
|
|
let count = if removed {
|
|
self.decrement_count_by(1)
|
|
} else {
|
|
let count = self.count();
|
|
FlowCountChange {
|
|
previous: count,
|
|
current: count,
|
|
}
|
|
};
|
|
FlowRemoval { removed, count }
|
|
}
|
|
|
|
fn decrement_count_by(&self, delta: usize) -> FlowCountChange {
|
|
if delta == 0 {
|
|
let count = self.count();
|
|
return FlowCountChange {
|
|
previous: count,
|
|
current: count,
|
|
};
|
|
}
|
|
|
|
let previous = self
|
|
.count
|
|
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
|
|
count.checked_sub(delta)
|
|
})
|
|
.expect("flow count underflow");
|
|
FlowCountChange {
|
|
previous,
|
|
current: previous - delta,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{FlowKey, FlowKind, FlowLease, FlowTable};
|
|
use std::{
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
sync::Arc,
|
|
};
|
|
|
|
impl<V> FlowLease<V> {
|
|
fn remove(mut self) -> super::FlowRemoval {
|
|
self.active = false;
|
|
self.table
|
|
.remove_registration(&self.entry, self.registration)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn entry_kind_values_preserve_native_table_identity() {
|
|
assert_eq!(FlowKind::Udp as u8, 1);
|
|
assert_eq!(FlowKind::Tcp as u8, 2);
|
|
assert_eq!(FlowKind::TcpListen as u8, 3);
|
|
}
|
|
|
|
fn table_entry(port: u16) -> FlowKey {
|
|
FlowKey {
|
|
src: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 42, 0, 2)), port),
|
|
dst: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 42, 0, 1)), 22),
|
|
kind: FlowKind::Tcp,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn flow_table_tracks_insert_replace_and_remove() {
|
|
let table = FlowTable::default();
|
|
let entry = table_entry(40000);
|
|
|
|
let inserted = table.insert(entry.clone(), "first");
|
|
assert!(!inserted.replaced);
|
|
assert_eq!(inserted.count.previous, 0);
|
|
assert_eq!(inserted.count.current, 1);
|
|
assert!(!table.is_idle());
|
|
assert_eq!(table.with_entry(&entry, |value| *value), Some("first"));
|
|
|
|
let replaced = table.insert(entry.clone(), "second");
|
|
assert!(replaced.replaced);
|
|
assert_eq!(replaced.count.previous, 1);
|
|
assert_eq!(replaced.count.current, 1);
|
|
assert_eq!(table.with_entry(&entry, |value| *value), Some("second"));
|
|
|
|
let removed = table.remove(&entry);
|
|
assert!(removed.removed);
|
|
assert_eq!(removed.count.previous, 1);
|
|
assert_eq!(removed.count.current, 0);
|
|
assert!(table.is_idle());
|
|
|
|
let missing = table.remove(&entry);
|
|
assert!(!missing.removed);
|
|
assert_eq!(missing.count.previous, 0);
|
|
assert_eq!(missing.count.current, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn flow_table_try_insert_and_retain_keep_count_consistent() {
|
|
let table = FlowTable::default();
|
|
let first = table_entry(40000);
|
|
let second = table_entry(40001);
|
|
|
|
assert!(table.try_insert(first.clone(), 1));
|
|
assert!(!table.try_insert(first.clone(), 2));
|
|
assert!(table.try_insert(second.clone(), 3));
|
|
assert_eq!(table.count(), 2);
|
|
assert!(table.contains_destination_ip(first.dst.ip()));
|
|
|
|
let retained = table.retain(|entry, _| entry == &second);
|
|
assert_eq!(retained.removed, 1);
|
|
assert_eq!(retained.count.previous, 2);
|
|
assert_eq!(retained.count.current, 1);
|
|
assert!(!table.contains_key(&first));
|
|
assert!(table.contains_key(&second));
|
|
|
|
let cleared = table.clear();
|
|
assert_eq!(cleared.removed, 1);
|
|
assert_eq!(cleared.count.current, 0);
|
|
assert!(table.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn entry_guard_owns_registration_lifetime() {
|
|
let table = Arc::new(FlowTable::default());
|
|
let entry = table_entry(40000);
|
|
|
|
let (guard, insert) = FlowLease::register(table.clone(), entry.clone(), "first");
|
|
assert!(!insert.replaced);
|
|
assert!(table.contains_key(&entry));
|
|
assert!(FlowLease::try_register(table.clone(), entry.clone(), "second").is_none());
|
|
assert_eq!(table.with_entry(&entry, |value| *value), Some("first"));
|
|
|
|
drop(guard);
|
|
assert!(!table.contains_key(&entry));
|
|
|
|
let guard = FlowLease::try_register(table.clone(), entry.clone(), "third").unwrap();
|
|
let removal = guard.remove();
|
|
assert!(removal.removed);
|
|
assert_eq!(table.count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn replaced_lease_cannot_remove_new_registration() {
|
|
let table = Arc::new(FlowTable::default());
|
|
let entry = table_entry(40000);
|
|
let (old, _) = FlowLease::register(table.clone(), entry.clone(), "old");
|
|
let (new, replaced) = FlowLease::register(table.clone(), entry.clone(), "new");
|
|
assert!(replaced.replaced);
|
|
|
|
drop(old);
|
|
assert_eq!(table.with_entry(&entry, |value| *value), Some("new"));
|
|
|
|
drop(new);
|
|
assert!(!table.contains_key(&entry));
|
|
}
|
|
}
|