use super::*;
use crate::transport::PacketBuffer;
#[cfg(unix)]
use crossbeam_channel::{
Receiver as CrossbeamReceiver, Sender as CrossbeamSender, TryRecvError, TrySendError, bounded,
};
#[derive(Debug)]
pub struct ExternalPacketIo {
pub outbound_tx: crate::upper::tun::TunOutboundTx,
pub inbound_rx: tokio::sync::mpsc::Receiver<NodeDeliveredPacket>,
}
#[derive(Debug)]
pub(crate) struct EndpointDataIo {
pub(crate) priority_command_tx: tokio::sync::mpsc::Sender<NodeEndpointCommand>,
pub(crate) command_tx: tokio::sync::mpsc::Sender<NodeEndpointCommand>,
pub(crate) event_rx: EndpointEventReceiver,
pub(crate) event_tx: EndpointEventSender,
#[cfg(unix)]
pub(crate) bulk_send_runtime: EndpointBulkSendRuntime,
}
#[cfg(unix)]
#[derive(Clone)]
pub(crate) struct EndpointBulkSendRuntime {
leases: Arc<std::sync::RwLock<std::collections::HashMap<NodeAddr, EndpointBulkSendLease>>>,
feedback_tx: tokio::sync::mpsc::Sender<EndpointBulkSendFeedback>,
committed_dispatch: EndpointCommittedBulkDispatch,
generation: Arc<std::sync::atomic::AtomicU64>,
}
#[cfg(unix)]
impl std::fmt::Debug for EndpointBulkSendRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EndpointBulkSendRuntime")
.field("leases", &self.lease_count())
.finish_non_exhaustive()
}
}
#[cfg(unix)]
#[derive(Clone)]
pub(crate) struct EndpointBulkSendLease {
pub(in crate::node) source_addr: NodeAddr,
pub(in crate::node) dest_addr: NodeAddr,
pub(in crate::node) next_hop_addr: NodeAddr,
pub(in crate::node) path_mtu: u16,
pub(in crate::node) default_ttl: u8,
pub(in crate::node) scheduling_weight: u8,
pub(in crate::node) direct_path_blocks_direct_payload: bool,
pub(in crate::node) fsp: EndpointBulkSendFspLease,
pub(in crate::node) fmp: EndpointBulkSendFmpLease,
pub(in crate::node) send_target: crate::node::encrypt_worker::SelectedSendTarget,
pub(in crate::node) workers: crate::node::encrypt_worker::EncryptWorkerPool,
expires_at: crate::time::Instant,
generation: u64,
}
#[cfg(unix)]
#[derive(Clone)]
pub(crate) struct EndpointBulkSendFspLease {
pub(in crate::node) cipher: ring::aead::LessSafeKey,
pub(in crate::node) counter_authority: crate::noise::SendCounterAuthority,
pub(in crate::node) session_start_ms: u64,
pub(in crate::node) current_k_bit: bool,
pub(in crate::node) spin_bit: bool,
}
#[cfg(unix)]
#[derive(Clone)]
pub(crate) struct EndpointBulkSendFmpLease {
pub(in crate::node) cipher: ring::aead::LessSafeKey,
pub(in crate::node) counter_authority: crate::noise::SendCounterAuthority,
pub(in crate::node) their_index: crate::utils::index::SessionIndex,
pub(in crate::node) session_start: std::time::Instant,
pub(in crate::node) base_flags: u8,
}
#[cfg_attr(not(unix), allow(dead_code))]
pub(crate) struct EndpointBulkSendFeedback {
pub(in crate::node) records: Vec<EndpointBulkSendFeedbackRecord>,
}
#[cfg_attr(not(unix), allow(dead_code))]
#[derive(Clone, Copy)]
pub(in crate::node) enum EndpointBulkSendSessionBookkeeping {
Fsp {
path_mtu: u16,
bookkeeping: FspSendBookkeepingInput,
},
}
#[cfg_attr(not(unix), allow(dead_code))]
#[derive(Clone, Copy)]
pub(crate) struct EndpointBulkSendFeedbackRecord {
pub(in crate::node) dest_addr: NodeAddr,
pub(in crate::node) next_hop_addr: NodeAddr,
pub(in crate::node) fmp_counter: u64,
pub(in crate::node) fmp_timestamp_ms: u32,
pub(in crate::node) fmp_wire_capacity: usize,
pub(in crate::node) originated_bytes: usize,
pub(in crate::node) session_bookkeeping: EndpointBulkSendSessionBookkeeping,
}
#[cfg(unix)]
#[derive(Clone)]
struct EndpointCommittedBulkDispatch {
tx: CrossbeamSender<EndpointCommittedBulkBatch>,
}
#[cfg(unix)]
struct EndpointCommittedBulkBatch {
workers: crate::node::encrypt_worker::EncryptWorkerPool,
jobs: Vec<crate::node::encrypt_worker::FmpSendJob>,
ready: Arc<EndpointCommittedBulkReady>,
}
#[cfg(unix)]
pub(in crate::node) struct EndpointCommittedBulkHandle {
ready: Option<Arc<EndpointCommittedBulkReady>>,
}
#[cfg(unix)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum EndpointCommittedBulkState {
Pending,
Committed,
Canceled,
}
#[cfg(unix)]
struct EndpointCommittedBulkReady {
state: std::sync::Mutex<EndpointCommittedBulkState>,
changed: Condvar,
}
#[cfg(unix)]
impl EndpointCommittedBulkDispatch {
fn channel(capacity: usize) -> Self {
let (tx, rx) = bounded(capacity.max(1));
std::thread::Builder::new()
.name("fips-endpoint-bulk-commit".to_string())
.spawn(move || run_endpoint_committed_bulk_dispatch(rx))
.expect("failed to spawn FIPS endpoint committed bulk dispatcher");
Self { tx }
}
fn try_stage(
&self,
workers: crate::node::encrypt_worker::EncryptWorkerPool,
jobs: Vec<crate::node::encrypt_worker::FmpSendJob>,
) -> Option<EndpointCommittedBulkHandle> {
if jobs.is_empty() {
return Some(EndpointCommittedBulkHandle { ready: None });
}
let ready = Arc::new(EndpointCommittedBulkReady::new());
let batch = EndpointCommittedBulkBatch {
workers,
jobs,
ready: Arc::clone(&ready),
};
match self.tx.try_send(batch) {
Ok(()) => Some(EndpointCommittedBulkHandle { ready: Some(ready) }),
Err(TrySendError::Full(batch)) | Err(TrySendError::Disconnected(batch)) => {
batch.ready.cancel();
None
}
}
}
}
#[cfg(unix)]
impl EndpointCommittedBulkBatch {
fn packet_count(&self) -> usize {
self.jobs.len()
}
fn can_merge_after(&self, other: &Self) -> bool {
crate::node::encrypt_worker::fmp_send_job_batches_share_bulk_target(&self.jobs, &other.jobs)
}
fn append_committed(&mut self, mut other: Self) {
self.jobs.append(&mut other.jobs);
}
}
#[cfg(unix)]
impl EndpointCommittedBulkHandle {
pub(in crate::node) fn commit(mut self) {
if let Some(ready) = self.ready.take() {
ready.commit();
}
}
pub(in crate::node) fn cancel(mut self) {
if let Some(ready) = self.ready.take() {
ready.cancel();
}
}
}
#[cfg(unix)]
impl Drop for EndpointCommittedBulkHandle {
fn drop(&mut self) {
if let Some(ready) = self.ready.take() {
ready.cancel();
}
}
}
#[cfg(unix)]
impl EndpointCommittedBulkReady {
fn new() -> Self {
Self {
state: std::sync::Mutex::new(EndpointCommittedBulkState::Pending),
changed: Condvar::new(),
}
}
fn commit(&self) {
self.complete(EndpointCommittedBulkState::Committed);
}
fn cancel(&self) {
self.complete(EndpointCommittedBulkState::Canceled);
}
fn complete(&self, next: EndpointCommittedBulkState) {
let Ok(mut state) = self.state.lock() else {
return;
};
if *state == EndpointCommittedBulkState::Pending {
*state = next;
self.changed.notify_one();
}
}
fn wait(&self) -> EndpointCommittedBulkState {
let Ok(mut state) = self.state.lock() else {
return EndpointCommittedBulkState::Canceled;
};
while *state == EndpointCommittedBulkState::Pending {
match self.changed.wait(state) {
Ok(next) => state = next,
Err(_) => return EndpointCommittedBulkState::Canceled,
}
}
*state
}
fn try_state(&self) -> EndpointCommittedBulkState {
self.state
.lock()
.map(|state| *state)
.unwrap_or(EndpointCommittedBulkState::Canceled)
}
}
#[cfg(unix)]
fn run_endpoint_committed_bulk_dispatch(rx: CrossbeamReceiver<EndpointCommittedBulkBatch>) {
let mut pending: Option<EndpointCommittedBulkBatch> = None;
loop {
let Some(mut batch) = pending.take().or_else(|| rx.recv().ok()) else {
break;
};
if batch.ready.wait() != EndpointCommittedBulkState::Committed {
continue;
}
let mut merged_batches = 0usize;
let mut merged_packets = 0usize;
let max_batches = endpoint_committed_bulk_coalesce_batches();
let max_packets = endpoint_committed_bulk_coalesce_packets();
while merged_batches + 1 < max_batches && batch.packet_count() < max_packets {
match rx.try_recv() {
Ok(next) => match next.ready.try_state() {
EndpointCommittedBulkState::Committed => {
if !batch.can_merge_after(&next) {
pending = Some(next);
break;
}
if batch.packet_count().saturating_add(next.packet_count()) > max_packets {
pending = Some(next);
break;
}
merged_batches = merged_batches.saturating_add(1);
merged_packets = merged_packets.saturating_add(next.packet_count());
batch.append_committed(next);
}
EndpointCommittedBulkState::Canceled => {}
EndpointCommittedBulkState::Pending => {
pending = Some(next);
break;
}
},
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => break,
}
}
crate::perf_profile::record_endpoint_committed_bulk_dispatch(
batch.packet_count(),
merged_batches,
merged_packets,
);
let EndpointCommittedBulkBatch { workers, jobs, .. } = batch;
let _all_enqueued = workers.dispatch_bulk_batch_blocking(jobs);
}
}
#[cfg(unix)]
fn endpoint_committed_bulk_coalesce_batches() -> usize {
static VALUE: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*VALUE.get_or_init(|| {
std::env::var("FIPS_ENDPOINT_COMMITTED_BULK_COALESCE_BATCHES")
.ok()
.and_then(|raw| raw.trim().parse::<usize>().ok())
.unwrap_or(1)
.clamp(1, 16)
})
}
#[cfg(unix)]
fn endpoint_committed_bulk_coalesce_packets() -> usize {
static VALUE: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*VALUE.get_or_init(|| {
std::env::var("FIPS_ENDPOINT_COMMITTED_BULK_COALESCE_PACKETS")
.ok()
.and_then(|raw| raw.trim().parse::<usize>().ok())
.unwrap_or(128)
.clamp(1, 1024)
})
}
#[cfg(unix)]
impl EndpointBulkSendRuntime {
pub(in crate::node) fn channel(
capacity: usize,
) -> (Self, tokio::sync::mpsc::Receiver<EndpointBulkSendFeedback>) {
let feedback_capacity = endpoint_data_command_capacity(capacity).max(1);
let (feedback_tx, feedback_rx) = tokio::sync::mpsc::channel(feedback_capacity);
let committed_dispatch = EndpointCommittedBulkDispatch::channel(capacity);
(
Self {
leases: Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
feedback_tx,
committed_dispatch,
generation: Arc::new(std::sync::atomic::AtomicU64::new(1)),
},
feedback_rx,
)
}
pub(in crate::node) fn publish(&self, mut lease: EndpointBulkSendLease) {
lease.generation = self.generation.load(Relaxed);
if let Ok(mut leases) = self.leases.write() {
leases.insert(lease.dest_addr, lease);
}
}
pub(in crate::node) fn invalidate(&self, dest_addr: &NodeAddr) {
self.generation
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if let Ok(mut leases) = self.leases.write() {
leases.remove(dest_addr);
}
}
pub(in crate::node) fn lease(&self, dest_addr: &NodeAddr) -> Option<EndpointBulkSendLease> {
let now = crate::time::instant_now();
let generation = self.generation.load(Relaxed);
let mut expired = false;
let lease = self.leases.read().ok().and_then(|leases| {
leases.get(dest_addr).and_then(|lease| {
if lease.generation != generation || lease.expires_at <= now {
expired = true;
None
} else {
Some(lease.clone())
}
})
});
if expired && let Ok(mut leases) = self.leases.write() {
leases.remove(dest_addr);
}
lease
}
pub(in crate::node) fn try_feedback(
&self,
records: Vec<EndpointBulkSendFeedbackRecord>,
) -> bool {
if records.is_empty() {
return true;
}
self.feedback_tx
.try_send(EndpointBulkSendFeedback { records })
.is_ok()
}
pub(in crate::node) fn try_stage_committed_bulk_dispatch(
&self,
workers: crate::node::encrypt_worker::EncryptWorkerPool,
jobs: Vec<crate::node::encrypt_worker::FmpSendJob>,
) -> Option<EndpointCommittedBulkHandle> {
self.committed_dispatch.try_stage(workers, jobs)
}
fn lease_count(&self) -> usize {
self.leases.read().map(|leases| leases.len()).unwrap_or(0)
}
}
#[cfg(unix)]
impl EndpointBulkSendLease {
pub(in crate::node) fn new(
source_addr: NodeAddr,
dest_addr: NodeAddr,
next_hop_addr: NodeAddr,
path_mtu: u16,
default_ttl: u8,
scheduling_weight: u8,
direct_path_blocks_direct_payload: bool,
fsp: EndpointBulkSendFspLease,
fmp: EndpointBulkSendFmpLease,
send_target: crate::node::encrypt_worker::SelectedSendTarget,
workers: crate::node::encrypt_worker::EncryptWorkerPool,
ttl: std::time::Duration,
) -> Self {
Self {
source_addr,
dest_addr,
next_hop_addr,
path_mtu,
default_ttl,
scheduling_weight,
direct_path_blocks_direct_payload,
fsp,
fmp,
send_target,
workers,
expires_at: crate::time::instant_now() + ttl,
generation: 0,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct EndpointEventSender {
priority: tokio::sync::mpsc::UnboundedSender<NodeEndpointEvent>,
bulk: tokio::sync::mpsc::Sender<NodeEndpointEvent>,
queued_messages: Arc<AtomicUsize>,
bulk_queued_messages: Arc<AtomicUsize>,
ready: Arc<EndpointEventReady>,
bulk_message_cap: usize,
}
#[derive(Debug)]
pub(crate) struct EndpointEventReceiver {
priority: tokio::sync::mpsc::UnboundedReceiver<NodeEndpointEvent>,
bulk: tokio::sync::mpsc::Receiver<NodeEndpointEvent>,
queued_messages: Arc<AtomicUsize>,
bulk_queued_messages: Arc<AtomicUsize>,
ready: Arc<EndpointEventReady>,
priority_closed: bool,
bulk_closed: bool,
}
#[derive(Debug, Default)]
struct EndpointEventReady {
sequence: StdMutex<u64>,
changed: Condvar,
}
impl EndpointEventReady {
fn notify(&self) {
if let Ok(mut sequence) = self.sequence.lock() {
*sequence = sequence.wrapping_add(1);
self.changed.notify_one();
}
}
fn snapshot(&self) -> u64 {
self.sequence.lock().map(|sequence| *sequence).unwrap_or(0)
}
fn wait_for_change(&self, observed: &mut u64) {
let Ok(mut sequence) = self.sequence.lock() else {
return;
};
while *sequence == *observed {
match self.changed.wait(sequence) {
Ok(next) => sequence = next,
Err(_) => return,
}
}
*observed = *sequence;
}
}
#[derive(Clone, Copy)]
enum EndpointEventLane {
Priority,
Bulk,
}
fn endpoint_event_lane_for_len(len: usize) -> EndpointEventLane {
if len <= ENDPOINT_EVENT_PRIORITY_MAX_LEN {
EndpointEventLane::Priority
} else {
EndpointEventLane::Bulk
}
}
fn endpoint_event_bulk_capacity(requested: usize) -> usize {
requested.max(1)
}
fn try_reserve_endpoint_event_bulk_messages(
counter: &AtomicUsize,
capacity: usize,
count: usize,
) -> Option<usize> {
if count == 0 {
return Some(counter.load(Relaxed));
}
counter
.fetch_update(Relaxed, Relaxed, |current| {
current.checked_add(count).filter(|next| *next <= capacity)
})
.ok()
}
#[derive(Debug, Default)]
pub(in crate::node) struct EndpointEventRuntime {
sender: Option<EndpointEventSender>,
batch_depth: usize,
batch: Vec<EndpointDataDelivery>,
}
impl EndpointEventSender {
pub(in crate::node) fn channel(capacity: usize) -> (Self, EndpointEventReceiver) {
let (priority_tx, priority_rx) = tokio::sync::mpsc::unbounded_channel();
let bulk_message_cap = endpoint_event_bulk_capacity(capacity);
let (bulk_tx, bulk_rx) = tokio::sync::mpsc::channel(bulk_message_cap);
let queued_messages = Arc::new(AtomicUsize::new(0));
let bulk_queued_messages = Arc::new(AtomicUsize::new(0));
let ready = Arc::new(EndpointEventReady::default());
(
Self {
priority: priority_tx,
bulk: bulk_tx,
queued_messages: Arc::clone(&queued_messages),
bulk_queued_messages: Arc::clone(&bulk_queued_messages),
ready: Arc::clone(&ready),
bulk_message_cap,
},
EndpointEventReceiver {
priority: priority_rx,
bulk: bulk_rx,
queued_messages,
bulk_queued_messages,
ready,
priority_closed: false,
bulk_closed: false,
},
)
}
pub(in crate::node) fn same_channels(&self, other: &Self) -> bool {
self.priority.same_channel(&other.priority)
&& self.bulk.same_channel(&other.bulk)
&& Arc::ptr_eq(&self.queued_messages, &other.queued_messages)
&& Arc::ptr_eq(&self.bulk_queued_messages, &other.bulk_queued_messages)
&& Arc::ptr_eq(&self.ready, &other.ready)
&& self.bulk_message_cap == other.bulk_message_cap
}
#[allow(clippy::result_large_err)]
pub(crate) fn send(
&self,
event: NodeEndpointEvent,
) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
match event {
NodeEndpointEvent::Data {
source_peer,
payload,
queued_at,
} => {
let lane = endpoint_event_lane_for_len(payload.len());
self.send_to_lane(
NodeEndpointEvent::Data {
source_peer,
payload,
queued_at,
},
lane,
)
}
NodeEndpointEvent::DataBatch {
messages,
queued_at,
} => self.send_data_batch(messages, queued_at),
}
}
#[allow(clippy::result_large_err)]
fn send_data_batch(
&self,
messages: Vec<EndpointDataDelivery>,
queued_at: Option<crate::perf_profile::TraceStamp>,
) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
if messages.is_empty() {
return Ok(());
}
let message_count = messages.len();
let priority_count = messages
.iter()
.filter(|message| message.is_priority_sized())
.count();
if priority_count == 0 || priority_count == message_count {
let lane = if priority_count == 0 {
EndpointEventLane::Bulk
} else {
EndpointEventLane::Priority
};
let event = NodeEndpointEvent::from_delivery_messages(messages, queued_at)
.expect("non-empty endpoint event batch should produce event");
return self.send_to_lane(event, lane);
}
let mut priority_messages = Vec::with_capacity(priority_count);
let mut bulk_messages = Vec::with_capacity(message_count - priority_count);
for message in messages {
if message.is_priority_sized() {
priority_messages.push(message);
} else {
bulk_messages.push(message);
}
}
if let Some(event) = NodeEndpointEvent::from_delivery_messages(priority_messages, queued_at)
{
self.send_to_lane(event, EndpointEventLane::Priority)?;
}
if let Some(event) = NodeEndpointEvent::from_delivery_messages(bulk_messages, queued_at) {
self.send_to_lane(event, EndpointEventLane::Bulk)?;
}
Ok(())
}
#[allow(clippy::result_large_err)]
fn send_to_lane(
&self,
event: NodeEndpointEvent,
lane: EndpointEventLane,
) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
if matches!(lane, EndpointEventLane::Bulk) {
return self.send_bulk_to_lane(event, true);
}
let count = event.message_count();
let previous = self.queued_messages.fetch_add(count, Relaxed);
let queued = previous.saturating_add(count);
match self.priority.send(event) {
Ok(()) => {
self.note_send_success(previous, queued);
Ok(())
}
Err(error) => {
self.note_send_rejected(count);
Err(error)
}
}
}
#[allow(clippy::result_large_err)]
fn send_bulk_to_lane(
&self,
event: NodeEndpointEvent,
split_on_pressure: bool,
) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
let count = event.message_count();
let Some(previous_bulk) = try_reserve_endpoint_event_bulk_messages(
&self.bulk_queued_messages,
self.bulk_message_cap,
count,
) else {
if split_on_pressure && count > 1 {
return self.split_and_send_bulk_event(event);
}
crate::perf_profile::record_event_count(
crate::perf_profile::Event::EndpointEventBulkDropped,
count as u64,
);
return Ok(());
};
let queued_bulk = previous_bulk.saturating_add(count);
if previous_bulk < ENDPOINT_EVENT_BACKLOG_HIGH_WATER
&& queued_bulk >= ENDPOINT_EVENT_BACKLOG_HIGH_WATER
{
crate::perf_profile::record_event(
crate::perf_profile::Event::EndpointEventBulkBacklogHigh,
);
}
let previous = self.queued_messages.fetch_add(count, Relaxed);
let queued = previous.saturating_add(count);
match self.bulk.try_send(event) {
Ok(()) => {
self.note_send_success(previous, queued);
Ok(())
}
Err(tokio::sync::mpsc::error::TrySendError::Full(_event)) => {
self.note_bulk_send_rejected(count);
crate::perf_profile::record_event_count(
crate::perf_profile::Event::EndpointEventBulkDropped,
count as u64,
);
Ok(())
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(event)) => {
self.note_bulk_send_rejected(count);
Err(tokio::sync::mpsc::error::SendError(event))
}
}
}
#[allow(clippy::result_large_err)]
fn split_and_send_bulk_event(
&self,
event: NodeEndpointEvent,
) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
let (mut messages, queued_at) = match event {
NodeEndpointEvent::DataBatch {
messages,
queued_at,
} => (messages, queued_at),
event => {
let count = event.message_count();
crate::perf_profile::record_event_count(
crate::perf_profile::Event::EndpointEventBulkDropped,
count as u64,
);
return Ok(());
}
};
if messages.len() <= 1 {
let event = NodeEndpointEvent::from_delivery_messages(messages, queued_at)
.expect("non-empty split endpoint batch should produce an event");
return self.send_bulk_to_lane(event, false);
}
let right = messages.split_off(messages.len() / 2);
if let Some(left) = NodeEndpointEvent::from_delivery_messages(messages, queued_at) {
self.send_bulk_to_lane(left, true)?;
}
if let Some(right) = NodeEndpointEvent::from_delivery_messages(right, queued_at) {
self.send_bulk_to_lane(right, true)?;
}
Ok(())
}
fn note_send_success(&self, previous: usize, queued: usize) {
if previous < ENDPOINT_EVENT_BACKLOG_HIGH_WATER
&& queued >= ENDPOINT_EVENT_BACKLOG_HIGH_WATER
{
crate::perf_profile::record_event(crate::perf_profile::Event::EndpointEventBacklogHigh);
}
self.ready.notify();
}
fn note_send_rejected(&self, count: usize) {
release_endpoint_event_messages(&self.queued_messages, count);
self.ready.notify();
}
fn note_bulk_send_rejected(&self, count: usize) {
release_endpoint_event_messages(&self.queued_messages, count);
release_endpoint_event_messages(&self.bulk_queued_messages, count);
self.ready.notify();
}
#[cfg(test)]
pub(crate) fn queued_messages(&self) -> usize {
self.queued_messages.load(Relaxed)
}
#[cfg(test)]
pub(crate) fn bulk_queued_messages(&self) -> usize {
self.bulk_queued_messages.load(Relaxed)
}
}
impl Drop for EndpointEventSender {
fn drop(&mut self) {
self.ready.notify();
}
}
impl EndpointEventRuntime {
pub(in crate::node) fn attach(&mut self, sender: EndpointEventSender) {
self.sender = Some(sender);
self.batch_depth = 0;
self.batch.clear();
}
pub(in crate::node) fn is_attached(&self) -> bool {
self.sender.is_some()
}
pub(in crate::node) fn sender(&self) -> Option<EndpointEventSender> {
self.sender.clone()
}
pub(in crate::node) fn begin_batch(&mut self) {
if self.is_attached() {
self.batch_depth = self.batch_depth.saturating_add(1);
}
}
pub(in crate::node) fn finish_batch(&mut self) {
if self.batch_depth == 0 {
return;
}
self.batch_depth -= 1;
if self.batch_depth == 0 {
self.flush_batch();
}
}
#[allow(clippy::result_large_err)]
pub(in crate::node) fn deliver_endpoint_data(
&mut self,
message: EndpointDataDelivery,
) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
if self.batch_depth > 0 {
self.batch.push(message);
return Ok(());
}
self.send(NodeEndpointEvent::Data {
source_peer: message.source_peer,
payload: message.payload,
queued_at: crate::perf_profile::stamp(),
})
}
fn flush_batch(&mut self) {
let count = self.batch.len();
if count == 0 {
return;
}
let queued_at = crate::perf_profile::stamp();
let event = if count == 1 {
let message = self.batch.pop().expect("batch should contain message");
NodeEndpointEvent::Data {
source_peer: message.source_peer,
payload: message.payload,
queued_at,
}
} else {
NodeEndpointEvent::DataBatch {
messages: std::mem::take(&mut self.batch),
queued_at,
}
};
if let Err(error) = self.send(event) {
debug!(
error = %error,
messages = count,
"Failed to deliver endpoint data event batch"
);
}
}
#[allow(clippy::result_large_err)]
fn send(
&self,
event: NodeEndpointEvent,
) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
let Some(sender) = &self.sender else {
return Ok(());
};
let _t_deliver =
crate::perf_profile::Timer::start(crate::perf_profile::Stage::EndpointDeliver);
sender.send(event)
}
}
impl EndpointEventReceiver {
pub(crate) async fn recv(&mut self) -> Option<NodeEndpointEvent> {
loop {
match self.try_recv() {
Ok(event) => return Some(event),
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return None,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
}
tokio::select! {
biased;
event = self.priority.recv(), if !self.priority_closed => {
match event {
Some(event) => {
self.note_dequeued(&event);
return Some(event);
}
None => self.priority_closed = true,
}
}
event = self.bulk.recv(), if !self.bulk_closed => {
match event {
Some(event) => {
self.note_dequeued(&event);
return Some(event);
}
None => self.bulk_closed = true,
}
}
}
}
}
pub(crate) fn blocking_recv(&mut self) -> Option<NodeEndpointEvent> {
let mut observed = self.ready.snapshot();
loop {
match self.try_recv() {
Ok(event) => return Some(event),
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return None,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
self.ready.wait_for_change(&mut observed);
}
}
}
}
pub(crate) fn try_recv(
&mut self,
) -> Result<NodeEndpointEvent, tokio::sync::mpsc::error::TryRecvError> {
match self.try_recv_priority() {
Ok(event) => return Ok(event),
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {}
}
match self.bulk.try_recv() {
Ok(event) => {
self.note_dequeued(&event);
Ok(event)
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
if self.priority_closed && self.bulk_closed {
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
} else {
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
}
}
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
self.bulk_closed = true;
if self.priority_closed {
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
} else {
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
}
}
}
}
pub(crate) fn try_recv_priority(
&mut self,
) -> Result<NodeEndpointEvent, tokio::sync::mpsc::error::TryRecvError> {
let event = match self.priority.try_recv() {
Ok(event) => event,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
return Err(tokio::sync::mpsc::error::TryRecvError::Empty);
}
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
self.priority_closed = true;
return Err(tokio::sync::mpsc::error::TryRecvError::Disconnected);
}
};
self.note_dequeued(&event);
Ok(event)
}
fn note_dequeued(&self, event: &NodeEndpointEvent) {
event.record_dequeue_wait();
let counts = event.dequeue_counts();
release_endpoint_event_messages(&self.queued_messages, counts.total);
release_endpoint_event_messages(&self.bulk_queued_messages, counts.bulk);
}
}
pub(in crate::node) fn release_endpoint_event_messages(counter: &AtomicUsize, count: usize) {
if count == 0 {
return;
}
let previous = counter.fetch_sub(count, Relaxed);
debug_assert!(
previous >= count,
"endpoint event queued message accounting underflow"
);
}
pub(crate) fn endpoint_data_command_capacity(requested: usize) -> usize {
if let Ok(raw) = std::env::var("FIPS_ENDPOINT_DATA_QUEUE_CAP")
&& let Ok(value) = raw.trim().parse::<usize>()
&& value > 0
{
return value;
}
requested.max(1).max(32_768)
}
const ENDPOINT_SEND_BATCH_DRAIN_QUANTUM: usize = 8;
fn endpoint_send_batch_drain_cost(packet_count: usize) -> usize {
packet_count
.max(1)
.saturating_add(ENDPOINT_SEND_BATCH_DRAIN_QUANTUM - 1)
/ ENDPOINT_SEND_BATCH_DRAIN_QUANTUM
}
#[derive(Debug)]
pub(crate) enum NodeEndpointCommand {
Send {
command: EndpointSendCommand,
response_tx: tokio::sync::oneshot::Sender<Result<(), NodeError>>,
},
SendOneway { command: EndpointSendCommand },
SendBatchOneway {
command: EndpointSendBatchCommand,
lane: EndpointCommandLane,
},
PeerSnapshot {
response_tx: tokio::sync::oneshot::Sender<Vec<NodeEndpointPeer>>,
},
RelaySnapshot {
response_tx: tokio::sync::oneshot::Sender<Vec<NodeEndpointRelayStatus>>,
},
UpdateRelays {
advert_relays: Vec<String>,
dm_relays: Vec<String>,
response_tx: tokio::sync::oneshot::Sender<Result<(), NodeError>>,
},
UpdatePeers {
peers: Vec<crate::config::PeerConfig>,
response_tx: tokio::sync::oneshot::Sender<Result<UpdatePeersOutcome, NodeError>>,
},
RefreshPeerPaths {
npubs: Vec<String>,
response_tx: tokio::sync::oneshot::Sender<Result<usize, NodeError>>,
},
}
#[derive(Debug)]
pub(crate) struct EndpointSendCommand {
send: EndpointDataSend,
queued_at: Option<crate::perf_profile::TraceStamp>,
}
impl EndpointSendCommand {
pub(crate) fn new(
remote: PeerIdentity,
payload: Vec<u8>,
queued_at: Option<crate::perf_profile::TraceStamp>,
) -> Self {
Self {
send: EndpointDataSend::new(remote, EndpointDataPayload::new(payload)),
queued_at,
}
}
pub(crate) fn lane(&self) -> EndpointCommandLane {
self.send.payload().lane()
}
pub(crate) fn drop_on_backpressure(&self) -> bool {
self.send.payload().drop_on_backpressure()
}
pub(crate) fn into_parts(self) -> (EndpointDataSend, Option<crate::perf_profile::TraceStamp>) {
(self.send, self.queued_at)
}
}
#[derive(Debug)]
pub(crate) struct EndpointSendBatchCommand {
remote: PeerIdentity,
payloads: Vec<EndpointDataPayload>,
queued_at: Option<crate::perf_profile::TraceStamp>,
}
impl EndpointSendBatchCommand {
pub(crate) fn new(
remote: PeerIdentity,
payloads: Vec<EndpointDataPayload>,
queued_at: Option<crate::perf_profile::TraceStamp>,
) -> Option<Self> {
if payloads.is_empty() {
return None;
}
Some(Self {
remote,
payloads,
queued_at,
})
}
pub(crate) fn lane(&self) -> EndpointCommandLane {
self.payloads[0].lane()
}
pub(crate) fn len(&self) -> usize {
self.payloads.len()
}
pub(crate) fn can_coalesce_with(&self, other: &Self, max_payloads: usize) -> bool {
self.remote == other.remote
&& self.lane() == other.lane()
&& self.len().saturating_add(other.len()) <= max_payloads
}
pub(crate) fn remote(&self) -> PeerIdentity {
self.remote
}
pub(crate) fn drop_on_backpressure(&self) -> bool {
self.payloads
.iter()
.all(EndpointDataPayload::drop_on_backpressure)
}
pub(crate) fn into_parts(
self,
) -> (
PeerIdentity,
Vec<EndpointDataPayload>,
Option<crate::perf_profile::TraceStamp>,
) {
(self.remote, self.payloads, self.queued_at)
}
}
impl NodeEndpointCommand {
pub(crate) fn send(
remote: PeerIdentity,
payload: Vec<u8>,
queued_at: Option<crate::perf_profile::TraceStamp>,
response_tx: tokio::sync::oneshot::Sender<Result<(), NodeError>>,
) -> Self {
Self::Send {
command: EndpointSendCommand::new(remote, payload, queued_at),
response_tx,
}
}
pub(crate) fn send_oneway(
remote: PeerIdentity,
payload: Vec<u8>,
queued_at: Option<crate::perf_profile::TraceStamp>,
) -> Self {
Self::SendOneway {
command: EndpointSendCommand::new(remote, payload, queued_at),
}
}
pub(crate) fn send_batch_oneway(
remote: PeerIdentity,
payloads: Vec<EndpointDataPayload>,
queued_at: Option<crate::perf_profile::TraceStamp>,
lane: EndpointCommandLane,
) -> Option<Self> {
debug_assert!(payloads.iter().all(|payload| payload.lane() == lane));
let command = EndpointSendBatchCommand::new(remote, payloads, queued_at)?;
debug_assert_eq!(command.lane(), lane);
Some(Self::SendBatchOneway { command, lane })
}
pub(crate) fn lane(&self) -> EndpointCommandLane {
match self {
Self::Send { command, .. } | Self::SendOneway { command } => command.lane(),
Self::SendBatchOneway { lane, .. } => *lane,
Self::PeerSnapshot { .. }
| Self::RelaySnapshot { .. }
| Self::UpdateRelays { .. }
| Self::UpdatePeers { .. }
| Self::RefreshPeerPaths { .. } => EndpointCommandLane::Priority,
}
}
pub(crate) fn drop_on_backpressure(&self) -> bool {
match self {
Self::SendOneway { command } => {
command.lane() == EndpointCommandLane::Bulk && command.drop_on_backpressure()
}
Self::SendBatchOneway { command, lane } => {
*lane == EndpointCommandLane::Bulk && command.drop_on_backpressure()
}
Self::Send { .. }
| Self::PeerSnapshot { .. }
| Self::RelaySnapshot { .. }
| Self::UpdateRelays { .. }
| Self::UpdatePeers { .. }
| Self::RefreshPeerPaths { .. } => false,
}
}
pub(crate) fn drain_cost(&self) -> usize {
match self {
Self::SendBatchOneway { command, .. } => endpoint_send_batch_drain_cost(command.len()),
Self::Send { .. }
| Self::SendOneway { .. }
| Self::PeerSnapshot { .. }
| Self::RelaySnapshot { .. }
| Self::UpdateRelays { .. }
| Self::UpdatePeers { .. }
| Self::RefreshPeerPaths { .. } => 1,
}
}
pub(crate) fn packet_count(&self) -> usize {
match self {
Self::SendBatchOneway { command, .. } => command.len(),
Self::Send { .. }
| Self::SendOneway { .. }
| Self::PeerSnapshot { .. }
| Self::RelaySnapshot { .. }
| Self::UpdateRelays { .. }
| Self::UpdatePeers { .. }
| Self::RefreshPeerPaths { .. } => 1,
}
}
pub(crate) fn into_send_batch_oneway(
self,
) -> Result<(EndpointSendBatchCommand, EndpointCommandLane), Self> {
match self {
Self::SendBatchOneway { command, lane } => Ok((command, lane)),
other => Err(other),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct UpdatePeersOutcome {
pub(crate) added: usize,
pub(crate) removed: usize,
pub(crate) updated: usize,
pub(crate) unchanged: usize,
}
#[derive(Debug)]
pub(crate) struct EndpointDataDelivery {
pub(crate) source_peer: PeerIdentity,
pub(crate) payload: PacketBuffer,
}
impl EndpointDataDelivery {
pub(crate) fn new(source_peer: PeerIdentity, payload: impl Into<PacketBuffer>) -> Self {
Self {
source_peer,
payload: payload.into(),
}
}
fn is_priority_sized(&self) -> bool {
matches!(
endpoint_event_lane_for_len(self.payload.len()),
EndpointEventLane::Priority
)
}
}
#[derive(Debug)]
pub(crate) enum NodeEndpointEvent {
Data {
source_peer: PeerIdentity,
payload: PacketBuffer,
queued_at: Option<crate::perf_profile::TraceStamp>,
},
DataBatch {
messages: Vec<EndpointDataDelivery>,
queued_at: Option<crate::perf_profile::TraceStamp>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::node) struct EndpointEventDequeueCounts {
pub(in crate::node) total: usize,
pub(in crate::node) priority: usize,
pub(in crate::node) bulk: usize,
}
impl NodeEndpointEvent {
fn message_count(&self) -> usize {
match self {
NodeEndpointEvent::Data { .. } => 1,
NodeEndpointEvent::DataBatch { messages, .. } => messages.len(),
}
}
pub(in crate::node) fn dequeue_counts(&self) -> EndpointEventDequeueCounts {
match self {
NodeEndpointEvent::Data { payload, .. } => {
let priority = usize::from(payload.len() <= ENDPOINT_EVENT_PRIORITY_MAX_LEN);
EndpointEventDequeueCounts {
total: 1,
priority,
bulk: 1 - priority,
}
}
NodeEndpointEvent::DataBatch { messages, .. } => {
let priority = messages
.iter()
.filter(|message| message.is_priority_sized())
.count();
EndpointEventDequeueCounts {
total: messages.len(),
priority,
bulk: messages.len().saturating_sub(priority),
}
}
}
}
fn queued_at(&self) -> Option<crate::perf_profile::TraceStamp> {
match self {
NodeEndpointEvent::Data { queued_at, .. }
| NodeEndpointEvent::DataBatch { queued_at, .. } => *queued_at,
}
}
fn record_dequeue_wait(&self) {
let queued_at = self.queued_at();
if queued_at.is_none() {
return;
}
let counts = self.dequeue_counts();
crate::perf_profile::record_since_split_count(
crate::perf_profile::Stage::EndpointEventWait,
crate::perf_profile::Stage::EndpointPriorityEventWait,
crate::perf_profile::Stage::EndpointBulkEventWait,
queued_at,
counts.total as u64,
counts.priority as u64,
counts.bulk as u64,
);
}
fn from_delivery_messages(
mut messages: Vec<EndpointDataDelivery>,
queued_at: Option<crate::perf_profile::TraceStamp>,
) -> Option<Self> {
match messages.len() {
0 => None,
1 => {
let message = messages.pop().expect("one endpoint message should exist");
Some(NodeEndpointEvent::Data {
source_peer: message.source_peer,
payload: message.payload,
queued_at,
})
}
_ => Some(NodeEndpointEvent::DataBatch {
messages,
queued_at,
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NodeEndpointPeer {
pub(crate) npub: String,
pub(crate) node_addr: NodeAddr,
pub(crate) connected: bool,
pub(crate) transport_addr: Option<String>,
pub(crate) transport_type: Option<String>,
pub(crate) link_id: u64,
pub(crate) srtt_ms: Option<u64>,
pub(crate) srtt_age_ms: Option<u64>,
pub(crate) packets_sent: u64,
pub(crate) packets_recv: u64,
pub(crate) bytes_sent: u64,
pub(crate) bytes_recv: u64,
pub(crate) rekey_in_progress: bool,
pub(crate) rekey_draining: bool,
pub(crate) current_k_bit: Option<bool>,
pub(crate) direct_probe_pending: bool,
pub(crate) direct_probe_after_ms: Option<u64>,
pub(crate) direct_probe_retry_count: u32,
pub(crate) direct_probe_auto_reconnect: bool,
pub(crate) direct_probe_expires_at_ms: Option<u64>,
pub(crate) nostr_traversal_consecutive_failures: u32,
pub(crate) nostr_traversal_in_cooldown: bool,
pub(crate) nostr_traversal_cooldown_until_ms: Option<u64>,
pub(crate) nostr_traversal_last_observed_skew_ms: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NodeEndpointRelayStatus {
pub(crate) url: String,
pub(crate) status: String,
}
#[cfg(all(test, unix))]
mod endpoint_committed_bulk_tests {
use super::*;
use crate::node::encrypt_worker::DEFAULT_SEND_WEIGHT;
use crate::transport::udp::socket::UdpRawSocket;
use ring::aead::{LessSafeKey, UnboundKey};
#[test]
fn committed_bulk_ready_waits_for_commit_or_cancel() {
let ready = Arc::new(EndpointCommittedBulkReady::new());
assert_eq!(ready.try_state(), EndpointCommittedBulkState::Pending);
let thread_ready = Arc::clone(&ready);
let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
let thread_done = Arc::clone(&done);
let handle = std::thread::spawn(move || {
assert_eq!(thread_ready.wait(), EndpointCommittedBulkState::Committed);
thread_done.store(true, Relaxed);
});
std::thread::sleep(std::time::Duration::from_millis(20));
assert!(
!done.load(Relaxed),
"staged bulk container must stay locked until feedback commits"
);
ready.commit();
handle.join().expect("commit waiter should finish");
assert!(done.load(Relaxed));
let ready = EndpointCommittedBulkReady::new();
ready.cancel();
assert_eq!(ready.try_state(), EndpointCommittedBulkState::Canceled);
assert_eq!(ready.wait(), EndpointCommittedBulkState::Canceled);
}
fn test_cipher() -> LessSafeKey {
let unbound =
UnboundKey::new(&ring::aead::CHACHA20_POLY1305, &[0u8; 32]).expect("build key");
LessSafeKey::new(unbound)
}
fn with_test_socket(
test: impl FnOnce(crate::transport::udp::socket::AsyncUdpSocket, LessSafeKey),
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.build()
.expect("tokio runtime");
rt.block_on(async {
let raw = UdpRawSocket::open("127.0.0.1:0".parse().unwrap(), 1 << 20, 1 << 20)
.expect("open UDP socket");
test(raw.into_async().expect("async UDP socket"), test_cipher());
});
}
fn bulk_job(
socket: crate::transport::udp::socket::AsyncUdpSocket,
cipher: &LessSafeKey,
dest_addr: &str,
bulk_endpoint_data: bool,
) -> crate::node::encrypt_worker::FmpSendJob {
let mut wire_buf = Vec::with_capacity(crate::node::wire::ESTABLISHED_HEADER_SIZE + 32);
wire_buf.extend_from_slice(&[0u8; crate::node::wire::ESTABLISHED_HEADER_SIZE]);
crate::node::encrypt_worker::FmpSendJob {
cipher: cipher.clone(),
counter: 0,
wire_buf,
fsp_seal: None,
send_target: crate::node::encrypt_worker::SelectedSendTarget::new(
socket,
#[cfg(any(target_os = "linux", target_os = "macos"))]
None,
dest_addr.parse().expect("socket addr"),
),
endpoint_flow_dispatch_key: None,
bulk_endpoint_data,
drop_on_backpressure: bulk_endpoint_data,
scheduling_weight: DEFAULT_SEND_WEIGHT,
queued_at: None,
}
}
#[test]
fn committed_bulk_batch_merge_requires_same_bulk_target() {
with_test_socket(|socket, cipher| {
let workers = crate::node::encrypt_worker::EncryptWorkerPool::spawn(1);
let first = EndpointCommittedBulkBatch {
workers: workers.clone(),
jobs: vec![bulk_job(socket.clone(), &cipher, "127.0.0.1:10031", true)],
ready: Arc::new(EndpointCommittedBulkReady::new()),
};
let same = EndpointCommittedBulkBatch {
workers: workers.clone(),
jobs: vec![bulk_job(socket.clone(), &cipher, "127.0.0.1:10031", true)],
ready: Arc::new(EndpointCommittedBulkReady::new()),
};
let other_target = EndpointCommittedBulkBatch {
workers: workers.clone(),
jobs: vec![bulk_job(socket.clone(), &cipher, "127.0.0.1:10032", true)],
ready: Arc::new(EndpointCommittedBulkReady::new()),
};
let priority_like = EndpointCommittedBulkBatch {
workers,
jobs: vec![bulk_job(socket, &cipher, "127.0.0.1:10031", false)],
ready: Arc::new(EndpointCommittedBulkReady::new()),
};
assert!(first.can_merge_after(&same));
assert!(!first.can_merge_after(&other_target));
assert!(!first.can_merge_after(&priority_like));
});
}
}