Skip to main content

fips_core/node/
endpoint_event.rs

1use super::*;
2
3/// App-owned packet channels for embedding FIPS without a system TUN.
4#[derive(Debug)]
5pub struct ExternalPacketIo {
6    /// Send outbound IPv6 packets into the node.
7    pub outbound_tx: crate::upper::tun::TunOutboundTx,
8    /// Receive inbound IPv6 packets delivered by FIPS sessions.
9    pub inbound_rx: tokio::sync::mpsc::Receiver<NodeDeliveredPacket>,
10}
11
12/// App-owned endpoint data channels for embedding FIPS without a daemon.
13#[derive(Debug)]
14pub(crate) struct EndpointDataIo {
15    /// Send latency-sensitive endpoint data and management commands into the
16    /// node RX loop ahead of queued bulk endpoint data.
17    pub(crate) priority_command_tx: tokio::sync::mpsc::Sender<NodeEndpointCommand>,
18    /// Send endpoint data commands into the node RX loop.
19    ///
20    /// Bounded with a generous default so normal sender bursts do not
21    /// stall on semaphore acquisition. macOS pacing happens at the UDP
22    /// egress thread where the real Wi-Fi/interface bottleneck is visible;
23    /// constraining this app queue instead caused the inner TCP flow to
24    /// collapse under iperf. `FIPS_ENDPOINT_DATA_QUEUE_CAP` overrides the
25    /// default for benches.
26    pub(crate) command_tx: tokio::sync::mpsc::Sender<NodeEndpointCommand>,
27    /// Receive endpoint data delivered by FIPS sessions.
28    ///
29    /// Priority endpoint events use an unbounded lane so small control-shaped
30    /// packets keep a wait-free push from the rx loop. Bulk endpoint messages
31    /// are bounded by the endpoint-data capacity and drop on pressure, with
32    /// drops visible through `endpoint_event_bulk_dropped`. Backpressure is
33    /// still visible through `endpoint_event_wait` latency and
34    /// `endpoint_event_backlog_high` when the consumer falls materially behind.
35    pub(crate) event_rx: EndpointEventReceiver,
36    /// Clone of the event_tx exposed for in-process loopback (e.g.
37    /// `FipsEndpoint::send` to self_npub). Lets the endpoint inject an
38    /// event into the same queue without going through the encrypt /
39    /// decrypt path, while keeping every consumer reading from a single
40    /// channel.
41    pub(crate) event_tx: EndpointEventSender,
42}
43
44/// Observable owner for endpoint events delivered to embedded applications.
45#[derive(Debug, Clone)]
46pub(crate) struct EndpointEventSender {
47    priority: tokio::sync::mpsc::UnboundedSender<NodeEndpointEvent>,
48    bulk: tokio::sync::mpsc::Sender<NodeEndpointEvent>,
49    queued_messages: Arc<AtomicUsize>,
50    bulk_queued_messages: Arc<AtomicUsize>,
51    ready: Arc<EndpointEventReady>,
52    bulk_message_cap: usize,
53}
54
55#[derive(Debug)]
56pub(crate) struct EndpointEventReceiver {
57    priority: tokio::sync::mpsc::UnboundedReceiver<NodeEndpointEvent>,
58    bulk: tokio::sync::mpsc::Receiver<NodeEndpointEvent>,
59    queued_messages: Arc<AtomicUsize>,
60    bulk_queued_messages: Arc<AtomicUsize>,
61    ready: Arc<EndpointEventReady>,
62    priority_closed: bool,
63    bulk_closed: bool,
64}
65
66#[derive(Debug, Default)]
67struct EndpointEventReady {
68    sequence: StdMutex<u64>,
69    changed: Condvar,
70}
71
72impl EndpointEventReady {
73    fn notify(&self) {
74        if let Ok(mut sequence) = self.sequence.lock() {
75            *sequence = sequence.wrapping_add(1);
76            self.changed.notify_one();
77        }
78    }
79
80    fn snapshot(&self) -> u64 {
81        self.sequence.lock().map(|sequence| *sequence).unwrap_or(0)
82    }
83
84    fn wait_for_change(&self, observed: &mut u64) {
85        let Ok(mut sequence) = self.sequence.lock() else {
86            return;
87        };
88        while *sequence == *observed {
89            match self.changed.wait(sequence) {
90                Ok(next) => sequence = next,
91                Err(_) => return,
92            }
93        }
94        *observed = *sequence;
95    }
96}
97
98#[derive(Clone, Copy)]
99enum EndpointEventLane {
100    Priority,
101    Bulk,
102}
103
104fn endpoint_event_lane_for_len(len: usize) -> EndpointEventLane {
105    if len <= ENDPOINT_EVENT_PRIORITY_MAX_LEN {
106        EndpointEventLane::Priority
107    } else {
108        EndpointEventLane::Bulk
109    }
110}
111
112fn endpoint_event_bulk_capacity(requested: usize) -> usize {
113    requested.max(1)
114}
115
116fn try_reserve_endpoint_event_bulk_messages(
117    counter: &AtomicUsize,
118    capacity: usize,
119    count: usize,
120) -> bool {
121    if count == 0 {
122        return true;
123    }
124
125    counter
126        .fetch_update(Relaxed, Relaxed, |current| {
127            current.checked_add(count).filter(|next| *next <= capacity)
128        })
129        .is_ok()
130}
131
132/// Delivery-side owner for endpoint data emitted by session receive handling.
133///
134/// The rx loop currently owns this runtime, but keeping sender, batching, and
135/// backlog accounting behind one value makes the future peer/shard receive
136/// runtime move explicit instead of threading endpoint-event fields through
137/// `Node` packet handlers.
138#[derive(Debug, Default)]
139pub(in crate::node) struct EndpointEventRuntime {
140    sender: Option<EndpointEventSender>,
141    batch_depth: usize,
142    batch: Vec<EndpointDataDelivery>,
143}
144
145impl EndpointEventSender {
146    pub(in crate::node) fn channel(capacity: usize) -> (Self, EndpointEventReceiver) {
147        let (priority_tx, priority_rx) = tokio::sync::mpsc::unbounded_channel();
148        let bulk_message_cap = endpoint_event_bulk_capacity(capacity);
149        let (bulk_tx, bulk_rx) = tokio::sync::mpsc::channel(bulk_message_cap);
150        let queued_messages = Arc::new(AtomicUsize::new(0));
151        let bulk_queued_messages = Arc::new(AtomicUsize::new(0));
152        let ready = Arc::new(EndpointEventReady::default());
153        (
154            Self {
155                priority: priority_tx,
156                bulk: bulk_tx,
157                queued_messages: Arc::clone(&queued_messages),
158                bulk_queued_messages: Arc::clone(&bulk_queued_messages),
159                ready: Arc::clone(&ready),
160                bulk_message_cap,
161            },
162            EndpointEventReceiver {
163                priority: priority_rx,
164                bulk: bulk_rx,
165                queued_messages,
166                bulk_queued_messages,
167                ready,
168                priority_closed: false,
169                bulk_closed: false,
170            },
171        )
172    }
173
174    pub(in crate::node) fn same_channels(&self, other: &Self) -> bool {
175        self.priority.same_channel(&other.priority)
176            && self.bulk.same_channel(&other.bulk)
177            && Arc::ptr_eq(&self.queued_messages, &other.queued_messages)
178            && Arc::ptr_eq(&self.bulk_queued_messages, &other.bulk_queued_messages)
179            && Arc::ptr_eq(&self.ready, &other.ready)
180            && self.bulk_message_cap == other.bulk_message_cap
181    }
182
183    #[allow(clippy::result_large_err)]
184    pub(crate) fn send(
185        &self,
186        event: NodeEndpointEvent,
187    ) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
188        match event {
189            NodeEndpointEvent::Data {
190                source_peer,
191                payload,
192                queued_at,
193            } => {
194                let lane = endpoint_event_lane_for_len(payload.len());
195                self.send_to_lane(
196                    NodeEndpointEvent::Data {
197                        source_peer,
198                        payload,
199                        queued_at,
200                    },
201                    lane,
202                )
203            }
204            NodeEndpointEvent::DataBatch {
205                messages,
206                queued_at,
207            } => self.send_data_batch(messages, queued_at),
208        }
209    }
210
211    #[allow(clippy::result_large_err)]
212    fn send_data_batch(
213        &self,
214        messages: Vec<EndpointDataDelivery>,
215        queued_at: Option<crate::perf_profile::TraceStamp>,
216    ) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
217        if messages.is_empty() {
218            return Ok(());
219        }
220
221        let message_count = messages.len();
222        let priority_count = messages
223            .iter()
224            .filter(|message| message.is_priority_sized())
225            .count();
226        if priority_count == 0 || priority_count == message_count {
227            let lane = if priority_count == 0 {
228                EndpointEventLane::Bulk
229            } else {
230                EndpointEventLane::Priority
231            };
232            let event = NodeEndpointEvent::from_delivery_messages(messages, queued_at)
233                .expect("non-empty endpoint event batch should produce event");
234            return self.send_to_lane(event, lane);
235        }
236
237        let mut priority_messages = Vec::with_capacity(priority_count);
238        let mut bulk_messages = Vec::with_capacity(message_count - priority_count);
239        for message in messages {
240            if message.is_priority_sized() {
241                priority_messages.push(message);
242            } else {
243                bulk_messages.push(message);
244            }
245        }
246
247        if let Some(event) = NodeEndpointEvent::from_delivery_messages(priority_messages, queued_at)
248        {
249            self.send_to_lane(event, EndpointEventLane::Priority)?;
250        }
251        if let Some(event) = NodeEndpointEvent::from_delivery_messages(bulk_messages, queued_at) {
252            self.send_to_lane(event, EndpointEventLane::Bulk)?;
253        }
254        Ok(())
255    }
256
257    #[allow(clippy::result_large_err)]
258    fn send_to_lane(
259        &self,
260        event: NodeEndpointEvent,
261        lane: EndpointEventLane,
262    ) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
263        let count = event.message_count();
264        let bulk_reserved = if matches!(lane, EndpointEventLane::Bulk) {
265            try_reserve_endpoint_event_bulk_messages(
266                &self.bulk_queued_messages,
267                self.bulk_message_cap,
268                count,
269            )
270        } else {
271            false
272        };
273        if matches!(lane, EndpointEventLane::Bulk) && !bulk_reserved {
274            crate::perf_profile::record_event_count(
275                crate::perf_profile::Event::EndpointEventBulkDropped,
276                count as u64,
277            );
278            return Ok(());
279        }
280
281        let previous = self.queued_messages.fetch_add(count, Relaxed);
282        let queued = previous.saturating_add(count);
283        match lane {
284            EndpointEventLane::Priority => match self.priority.send(event) {
285                Ok(()) => {
286                    self.note_send_success(previous, queued);
287                    Ok(())
288                }
289                Err(error) => {
290                    self.note_send_rejected(count);
291                    Err(error)
292                }
293            },
294            EndpointEventLane::Bulk => match self.bulk.try_send(event) {
295                Ok(()) => {
296                    self.note_send_success(previous, queued);
297                    Ok(())
298                }
299                Err(tokio::sync::mpsc::error::TrySendError::Full(_event)) => {
300                    self.note_bulk_send_rejected(count);
301                    crate::perf_profile::record_event_count(
302                        crate::perf_profile::Event::EndpointEventBulkDropped,
303                        count as u64,
304                    );
305                    Ok(())
306                }
307                Err(tokio::sync::mpsc::error::TrySendError::Closed(event)) => {
308                    self.note_bulk_send_rejected(count);
309                    Err(tokio::sync::mpsc::error::SendError(event))
310                }
311            },
312        }
313    }
314
315    fn note_send_success(&self, previous: usize, queued: usize) {
316        if previous < ENDPOINT_EVENT_BACKLOG_HIGH_WATER
317            && queued >= ENDPOINT_EVENT_BACKLOG_HIGH_WATER
318        {
319            crate::perf_profile::record_event(crate::perf_profile::Event::EndpointEventBacklogHigh);
320        }
321        self.ready.notify();
322    }
323
324    fn note_send_rejected(&self, count: usize) {
325        release_endpoint_event_messages(&self.queued_messages, count);
326        self.ready.notify();
327    }
328
329    fn note_bulk_send_rejected(&self, count: usize) {
330        release_endpoint_event_messages(&self.queued_messages, count);
331        release_endpoint_event_messages(&self.bulk_queued_messages, count);
332        self.ready.notify();
333    }
334
335    #[cfg(test)]
336    pub(crate) fn queued_messages(&self) -> usize {
337        self.queued_messages.load(Relaxed)
338    }
339
340    #[cfg(test)]
341    pub(crate) fn bulk_queued_messages(&self) -> usize {
342        self.bulk_queued_messages.load(Relaxed)
343    }
344}
345
346impl Drop for EndpointEventSender {
347    fn drop(&mut self) {
348        self.ready.notify();
349    }
350}
351
352impl EndpointEventRuntime {
353    pub(in crate::node) fn attach(&mut self, sender: EndpointEventSender) {
354        self.sender = Some(sender);
355        self.batch_depth = 0;
356        self.batch.clear();
357    }
358
359    pub(in crate::node) fn is_attached(&self) -> bool {
360        self.sender.is_some()
361    }
362
363    pub(in crate::node) fn sender(&self) -> Option<EndpointEventSender> {
364        self.sender.clone()
365    }
366
367    pub(in crate::node) fn begin_batch(&mut self) {
368        if self.is_attached() {
369            self.batch_depth = self.batch_depth.saturating_add(1);
370        }
371    }
372
373    pub(in crate::node) fn finish_batch(&mut self) {
374        if self.batch_depth == 0 {
375            return;
376        }
377        self.batch_depth -= 1;
378        if self.batch_depth == 0 {
379            self.flush_batch();
380        }
381    }
382
383    #[allow(clippy::result_large_err)]
384    pub(in crate::node) fn deliver_endpoint_data(
385        &mut self,
386        message: EndpointDataDelivery,
387    ) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
388        if self.batch_depth > 0 {
389            self.batch.push(message);
390            return Ok(());
391        }
392
393        self.send(NodeEndpointEvent::Data {
394            source_peer: message.source_peer,
395            payload: message.payload,
396            queued_at: crate::perf_profile::stamp(),
397        })
398    }
399
400    fn flush_batch(&mut self) {
401        let count = self.batch.len();
402        if count == 0 {
403            return;
404        }
405
406        let queued_at = crate::perf_profile::stamp();
407        let event = if count == 1 {
408            let message = self.batch.pop().expect("batch should contain message");
409            NodeEndpointEvent::Data {
410                source_peer: message.source_peer,
411                payload: message.payload,
412                queued_at,
413            }
414        } else {
415            NodeEndpointEvent::DataBatch {
416                messages: std::mem::take(&mut self.batch),
417                queued_at,
418            }
419        };
420
421        if let Err(error) = self.send(event) {
422            debug!(
423                error = %error,
424                messages = count,
425                "Failed to deliver endpoint data event batch"
426            );
427        }
428    }
429
430    #[allow(clippy::result_large_err)]
431    fn send(
432        &self,
433        event: NodeEndpointEvent,
434    ) -> Result<(), tokio::sync::mpsc::error::SendError<NodeEndpointEvent>> {
435        let Some(sender) = &self.sender else {
436            return Ok(());
437        };
438        let _t_deliver =
439            crate::perf_profile::Timer::start(crate::perf_profile::Stage::EndpointDeliver);
440        sender.send(event)
441    }
442}
443
444impl EndpointEventReceiver {
445    pub(crate) async fn recv(&mut self) -> Option<NodeEndpointEvent> {
446        loop {
447            match self.try_recv() {
448                Ok(event) => return Some(event),
449                Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return None,
450                Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
451            }
452
453            tokio::select! {
454                biased;
455                event = self.priority.recv(), if !self.priority_closed => {
456                    match event {
457                        Some(event) => {
458                            self.note_dequeued(&event);
459                            return Some(event);
460                        }
461                        None => self.priority_closed = true,
462                    }
463                }
464                event = self.bulk.recv(), if !self.bulk_closed => {
465                    match event {
466                        Some(event) => {
467                            self.note_dequeued(&event);
468                            return Some(event);
469                        }
470                        None => self.bulk_closed = true,
471                    }
472                }
473            }
474        }
475    }
476
477    pub(crate) fn blocking_recv(&mut self) -> Option<NodeEndpointEvent> {
478        let mut observed = self.ready.snapshot();
479        loop {
480            match self.try_recv() {
481                Ok(event) => return Some(event),
482                Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return None,
483                Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
484                    self.ready.wait_for_change(&mut observed);
485                }
486            }
487        }
488    }
489
490    pub(crate) fn try_recv(
491        &mut self,
492    ) -> Result<NodeEndpointEvent, tokio::sync::mpsc::error::TryRecvError> {
493        match self.try_recv_priority() {
494            Ok(event) => return Ok(event),
495            Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
496            Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {}
497        }
498
499        match self.bulk.try_recv() {
500            Ok(event) => {
501                self.note_dequeued(&event);
502                Ok(event)
503            }
504            Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
505                if self.priority_closed && self.bulk_closed {
506                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
507                } else {
508                    Err(tokio::sync::mpsc::error::TryRecvError::Empty)
509                }
510            }
511            Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
512                self.bulk_closed = true;
513                if self.priority_closed {
514                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
515                } else {
516                    Err(tokio::sync::mpsc::error::TryRecvError::Empty)
517                }
518            }
519        }
520    }
521
522    pub(crate) fn try_recv_priority(
523        &mut self,
524    ) -> Result<NodeEndpointEvent, tokio::sync::mpsc::error::TryRecvError> {
525        let event = match self.priority.try_recv() {
526            Ok(event) => event,
527            Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
528                return Err(tokio::sync::mpsc::error::TryRecvError::Empty);
529            }
530            Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
531                self.priority_closed = true;
532                return Err(tokio::sync::mpsc::error::TryRecvError::Disconnected);
533            }
534        };
535        self.note_dequeued(&event);
536        Ok(event)
537    }
538
539    fn note_dequeued(&self, event: &NodeEndpointEvent) {
540        event.record_dequeue_wait();
541        let counts = event.dequeue_counts();
542        release_endpoint_event_messages(&self.queued_messages, counts.total);
543        release_endpoint_event_messages(&self.bulk_queued_messages, counts.bulk);
544    }
545}
546
547pub(in crate::node) fn release_endpoint_event_messages(counter: &AtomicUsize, count: usize) {
548    if count == 0 {
549        return;
550    }
551
552    let previous = counter.fetch_sub(count, Relaxed);
553    debug_assert!(
554        previous >= count,
555        "endpoint event queued message accounting underflow"
556    );
557}
558
559pub(crate) fn endpoint_data_command_capacity(requested: usize) -> usize {
560    if let Ok(raw) = std::env::var("FIPS_ENDPOINT_DATA_QUEUE_CAP")
561        && let Ok(value) = raw.trim().parse::<usize>()
562        && value > 0
563    {
564        return value;
565    }
566
567    requested.max(1).max(32_768)
568}
569
570/// Commands accepted by the node endpoint data service.
571#[derive(Debug)]
572pub(crate) enum NodeEndpointCommand {
573    /// Send with an explicit response channel — used by callers that
574    /// care whether the local-stack handoff succeeded (e.g.
575    /// `blocking_send` waits for the runtime to accept the send).
576    Send {
577        command: EndpointSendCommand,
578        response_tx: tokio::sync::oneshot::Sender<Result<(), NodeError>>,
579    },
580    /// **Fire-and-forget** variant of `Send` — no oneshot allocation,
581    /// no per-packet result channel. Used by the data-plane fast path
582    /// (`FipsEndpoint::send`) where the caller already discards the
583    /// result. Saves one oneshot::channel() allocation per outbound
584    /// packet on the application's send hot path.
585    SendOneway { command: EndpointSendCommand },
586    /// Fire-and-forget batch of endpoint payloads that already share the same
587    /// peer and command lane. This keeps bursty embedded dataplanes from
588    /// paying one mpsc send/wake per packet while preserving the priority/bulk
589    /// split without repeating the resolved peer identity in every payload.
590    SendBatchOneway {
591        command: EndpointSendBatchCommand,
592        lane: EndpointCommandLane,
593    },
594    PeerSnapshot {
595        response_tx: tokio::sync::oneshot::Sender<Vec<NodeEndpointPeer>>,
596    },
597    RelaySnapshot {
598        response_tx: tokio::sync::oneshot::Sender<Vec<NodeEndpointRelayStatus>>,
599    },
600    UpdateRelays {
601        advert_relays: Vec<String>,
602        dm_relays: Vec<String>,
603        response_tx: tokio::sync::oneshot::Sender<Result<(), NodeError>>,
604    },
605    /// Replace the runtime peer list. Newly added auto-connect peers get
606    /// `initiate_peer_connection` immediately; removed peers are dropped
607    /// from the retry queue (the regular liveness timeout reaps any active
608    /// session). Existing entries are kept and their `addresses` field is
609    /// refreshed so the next retry sees the latest hints.
610    UpdatePeers {
611        peers: Vec<crate::config::PeerConfig>,
612        response_tx: tokio::sync::oneshot::Sender<Result<UpdatePeersOutcome, NodeError>>,
613    },
614}
615
616/// Message payload for outbound endpoint data handed from an embedded
617/// application into the node rx loop.
618#[derive(Debug)]
619pub(crate) struct EndpointSendCommand {
620    send: EndpointDataSend,
621    queued_at: Option<crate::perf_profile::TraceStamp>,
622}
623
624impl EndpointSendCommand {
625    pub(crate) fn new(
626        remote: PeerIdentity,
627        payload: Vec<u8>,
628        queued_at: Option<crate::perf_profile::TraceStamp>,
629    ) -> Self {
630        Self {
631            send: EndpointDataSend::new(remote, EndpointDataPayload::new(payload)),
632            queued_at,
633        }
634    }
635
636    pub(crate) fn lane(&self) -> EndpointCommandLane {
637        self.send.payload().lane()
638    }
639
640    pub(crate) fn drop_on_backpressure(&self) -> bool {
641        self.send.payload().drop_on_backpressure()
642    }
643
644    pub(crate) fn set_queued_at(&mut self, queued_at: Option<crate::perf_profile::TraceStamp>) {
645        self.queued_at = queued_at;
646    }
647
648    pub(crate) fn into_parts(self) -> (EndpointDataSend, Option<crate::perf_profile::TraceStamp>) {
649        (self.send, self.queued_at)
650    }
651}
652
653/// Batch of endpoint payloads to one resolved peer.
654#[derive(Debug)]
655pub(crate) struct EndpointSendBatchCommand {
656    remote: PeerIdentity,
657    payloads: Vec<EndpointDataPayload>,
658    queued_at: Option<crate::perf_profile::TraceStamp>,
659}
660
661impl EndpointSendBatchCommand {
662    pub(crate) fn new(
663        remote: PeerIdentity,
664        payloads: Vec<EndpointDataPayload>,
665        queued_at: Option<crate::perf_profile::TraceStamp>,
666    ) -> Option<Self> {
667        if payloads.is_empty() {
668            return None;
669        }
670        Some(Self {
671            remote,
672            payloads,
673            queued_at,
674        })
675    }
676
677    pub(crate) fn lane(&self) -> EndpointCommandLane {
678        self.payloads[0].lane()
679    }
680
681    pub(crate) fn len(&self) -> usize {
682        self.payloads.len()
683    }
684
685    pub(crate) fn drop_on_backpressure(&self) -> bool {
686        self.payloads
687            .iter()
688            .all(EndpointDataPayload::drop_on_backpressure)
689    }
690
691    pub(crate) fn set_queued_at(&mut self, queued_at: Option<crate::perf_profile::TraceStamp>) {
692        self.queued_at = queued_at;
693    }
694
695    pub(crate) fn into_parts(
696        self,
697    ) -> (
698        PeerIdentity,
699        Vec<EndpointDataPayload>,
700        Option<crate::perf_profile::TraceStamp>,
701    ) {
702        (self.remote, self.payloads, self.queued_at)
703    }
704}
705
706impl NodeEndpointCommand {
707    pub(crate) fn send(
708        remote: PeerIdentity,
709        payload: Vec<u8>,
710        queued_at: Option<crate::perf_profile::TraceStamp>,
711        response_tx: tokio::sync::oneshot::Sender<Result<(), NodeError>>,
712    ) -> Self {
713        Self::Send {
714            command: EndpointSendCommand::new(remote, payload, queued_at),
715            response_tx,
716        }
717    }
718
719    pub(crate) fn send_oneway(
720        remote: PeerIdentity,
721        payload: Vec<u8>,
722        queued_at: Option<crate::perf_profile::TraceStamp>,
723    ) -> Self {
724        Self::SendOneway {
725            command: EndpointSendCommand::new(remote, payload, queued_at),
726        }
727    }
728
729    pub(crate) fn send_batch_oneway(
730        remote: PeerIdentity,
731        payloads: Vec<EndpointDataPayload>,
732        queued_at: Option<crate::perf_profile::TraceStamp>,
733        lane: EndpointCommandLane,
734    ) -> Option<Self> {
735        debug_assert!(payloads.iter().all(|payload| payload.lane() == lane));
736        let command = EndpointSendBatchCommand::new(remote, payloads, queued_at)?;
737        debug_assert_eq!(command.lane(), lane);
738        Some(Self::SendBatchOneway { command, lane })
739    }
740
741    pub(crate) fn lane(&self) -> EndpointCommandLane {
742        match self {
743            Self::Send { command, .. } | Self::SendOneway { command } => command.lane(),
744            Self::SendBatchOneway { lane, .. } => *lane,
745            Self::PeerSnapshot { .. }
746            | Self::RelaySnapshot { .. }
747            | Self::UpdateRelays { .. }
748            | Self::UpdatePeers { .. } => EndpointCommandLane::Priority,
749        }
750    }
751
752    pub(crate) fn drop_on_backpressure(&self) -> bool {
753        match self {
754            Self::SendOneway { command } => {
755                command.lane() == EndpointCommandLane::Bulk && command.drop_on_backpressure()
756            }
757            Self::SendBatchOneway { command, lane } => {
758                *lane == EndpointCommandLane::Bulk && command.drop_on_backpressure()
759            }
760            Self::Send { .. }
761            | Self::PeerSnapshot { .. }
762            | Self::RelaySnapshot { .. }
763            | Self::UpdateRelays { .. }
764            | Self::UpdatePeers { .. } => false,
765        }
766    }
767
768    pub(crate) fn drain_cost(&self) -> usize {
769        match self {
770            Self::SendBatchOneway { command, .. } => command.len().max(1),
771            Self::Send { .. }
772            | Self::SendOneway { .. }
773            | Self::PeerSnapshot { .. }
774            | Self::RelaySnapshot { .. }
775            | Self::UpdateRelays { .. }
776            | Self::UpdatePeers { .. } => 1,
777        }
778    }
779
780    pub(crate) fn set_queued_at(&mut self, queued_at: Option<crate::perf_profile::TraceStamp>) {
781        match self {
782            Self::Send { command, .. } | Self::SendOneway { command } => {
783                command.set_queued_at(queued_at);
784            }
785            Self::SendBatchOneway { command, .. } => {
786                command.set_queued_at(queued_at);
787            }
788            Self::PeerSnapshot { .. }
789            | Self::RelaySnapshot { .. }
790            | Self::UpdateRelays { .. }
791            | Self::UpdatePeers { .. } => {}
792        }
793    }
794}
795
796/// Reports what changed in response to `UpdatePeers`.
797#[derive(Debug, Clone, Default, PartialEq, Eq)]
798pub(crate) struct UpdatePeersOutcome {
799    pub(crate) added: usize,
800    pub(crate) removed: usize,
801    pub(crate) updated: usize,
802    pub(crate) unchanged: usize,
803}
804
805/// Authenticated endpoint data emitted by the session receive path.
806///
807/// Keeping source identity and payload together makes the delivery-side
808/// ownership boundary explicit for the current rx loop and for a future
809/// peer/session runtime that can move endpoint-data delivery off the bounce path.
810#[derive(Debug)]
811pub(crate) struct EndpointDataDelivery {
812    pub(crate) source_peer: PeerIdentity,
813    pub(crate) payload: Vec<u8>,
814}
815
816impl EndpointDataDelivery {
817    pub(crate) fn new(source_peer: PeerIdentity, payload: Vec<u8>) -> Self {
818        Self {
819            source_peer,
820            payload,
821        }
822    }
823
824    fn is_priority_sized(&self) -> bool {
825        matches!(
826            endpoint_event_lane_for_len(self.payload.len()),
827            EndpointEventLane::Priority
828        )
829    }
830}
831
832/// Endpoint data events emitted by the node session receive path.
833#[derive(Debug)]
834pub(crate) enum NodeEndpointEvent {
835    Data {
836        source_peer: PeerIdentity,
837        payload: Vec<u8>,
838        queued_at: Option<crate::perf_profile::TraceStamp>,
839    },
840    DataBatch {
841        messages: Vec<EndpointDataDelivery>,
842        queued_at: Option<crate::perf_profile::TraceStamp>,
843    },
844}
845
846#[derive(Debug, Clone, Copy, PartialEq, Eq)]
847pub(in crate::node) struct EndpointEventDequeueCounts {
848    pub(in crate::node) total: usize,
849    pub(in crate::node) priority: usize,
850    pub(in crate::node) bulk: usize,
851}
852
853impl NodeEndpointEvent {
854    fn message_count(&self) -> usize {
855        match self {
856            NodeEndpointEvent::Data { .. } => 1,
857            NodeEndpointEvent::DataBatch { messages, .. } => messages.len(),
858        }
859    }
860
861    pub(in crate::node) fn dequeue_counts(&self) -> EndpointEventDequeueCounts {
862        match self {
863            NodeEndpointEvent::Data { payload, .. } => {
864                let priority = usize::from(payload.len() <= ENDPOINT_EVENT_PRIORITY_MAX_LEN);
865                EndpointEventDequeueCounts {
866                    total: 1,
867                    priority,
868                    bulk: 1 - priority,
869                }
870            }
871            NodeEndpointEvent::DataBatch { messages, .. } => {
872                let priority = messages
873                    .iter()
874                    .filter(|message| message.is_priority_sized())
875                    .count();
876                EndpointEventDequeueCounts {
877                    total: messages.len(),
878                    priority,
879                    bulk: messages.len().saturating_sub(priority),
880                }
881            }
882        }
883    }
884
885    fn queued_at(&self) -> Option<crate::perf_profile::TraceStamp> {
886        match self {
887            NodeEndpointEvent::Data { queued_at, .. }
888            | NodeEndpointEvent::DataBatch { queued_at, .. } => *queued_at,
889        }
890    }
891
892    fn record_dequeue_wait(&self) {
893        let queued_at = self.queued_at();
894        if queued_at.is_none() {
895            return;
896        }
897        let counts = self.dequeue_counts();
898        crate::perf_profile::record_since_split_count(
899            crate::perf_profile::Stage::EndpointEventWait,
900            crate::perf_profile::Stage::EndpointPriorityEventWait,
901            crate::perf_profile::Stage::EndpointBulkEventWait,
902            queued_at,
903            counts.total as u64,
904            counts.priority as u64,
905            counts.bulk as u64,
906        );
907    }
908
909    fn from_delivery_messages(
910        mut messages: Vec<EndpointDataDelivery>,
911        queued_at: Option<crate::perf_profile::TraceStamp>,
912    ) -> Option<Self> {
913        match messages.len() {
914            0 => None,
915            1 => {
916                let message = messages.pop().expect("one endpoint message should exist");
917                Some(NodeEndpointEvent::Data {
918                    source_peer: message.source_peer,
919                    payload: message.payload,
920                    queued_at,
921                })
922            }
923            _ => Some(NodeEndpointEvent::DataBatch {
924                messages,
925                queued_at,
926            }),
927        }
928    }
929}
930
931/// Authenticated peer state exposed to embedded endpoint callers.
932#[derive(Debug, Clone, PartialEq, Eq)]
933pub(crate) struct NodeEndpointPeer {
934    pub(crate) npub: String,
935    pub(crate) node_addr: NodeAddr,
936    pub(crate) connected: bool,
937    pub(crate) transport_addr: Option<String>,
938    pub(crate) transport_type: Option<String>,
939    pub(crate) link_id: u64,
940    pub(crate) srtt_ms: Option<u64>,
941    pub(crate) srtt_age_ms: Option<u64>,
942    pub(crate) packets_sent: u64,
943    pub(crate) packets_recv: u64,
944    pub(crate) bytes_sent: u64,
945    pub(crate) bytes_recv: u64,
946    pub(crate) rekey_in_progress: bool,
947    pub(crate) rekey_draining: bool,
948    pub(crate) current_k_bit: Option<bool>,
949    pub(crate) direct_probe_pending: bool,
950    pub(crate) direct_probe_after_ms: Option<u64>,
951    pub(crate) direct_probe_retry_count: u32,
952    pub(crate) direct_probe_auto_reconnect: bool,
953    pub(crate) direct_probe_expires_at_ms: Option<u64>,
954    pub(crate) nostr_traversal_consecutive_failures: u32,
955    pub(crate) nostr_traversal_in_cooldown: bool,
956    pub(crate) nostr_traversal_cooldown_until_ms: Option<u64>,
957    pub(crate) nostr_traversal_last_observed_skew_ms: Option<i64>,
958}
959
960/// Live Nostr relay state exposed to embedded endpoint callers.
961#[derive(Debug, Clone, PartialEq, Eq)]
962pub(crate) struct NodeEndpointRelayStatus {
963    pub(crate) url: String,
964    pub(crate) status: String,
965}