Skip to main content

fips_core/node/handlers/
rx_loop.rs

1//! RX event loop and packet dispatch.
2
3use crate::control::queries;
4use crate::control::{ControlMessage, ControlSenders, ControlSocket, commands};
5use crate::discovery::is_punch_packet;
6use crate::node::decrypt_worker::{
7    DecryptFailureReport, DecryptFallback, DecryptJobBatcher, DecryptWorkerEvent,
8    DecryptWorkerFallbackReceivers,
9};
10use crate::node::handlers::encrypted::EncryptedFrameFastPath;
11use crate::node::wire::{
12    COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
13};
14use crate::node::{
15    AuthenticatedFmpPlaintext, EndpointSendBatchCommand, Node, NodeEndpointCommand, NodeError,
16};
17use crate::transport::PacketRx;
18use crate::transport::ReceivedPacket;
19use crate::upper::tun::TunOutboundRx;
20use std::time::{Duration, Instant};
21use tokio::sync::mpsc::Receiver;
22use tracing::{debug, info, trace, warn};
23
24mod budget;
25mod drain;
26
27#[cfg(test)]
28mod tests;
29
30use budget::*;
31use drain::*;
32
33impl Node {
34    /// Run the receive event loop.
35    ///
36    /// Processes packets from all transports, dispatching based on
37    /// the phase field in the 4-byte common prefix:
38    /// - Phase 0x0: Encrypted frame (session data)
39    /// - Phase 0x1: Handshake message 1 (initiator -> responder)
40    /// - Phase 0x2: Handshake message 2 (responder -> initiator)
41    ///
42    /// Also processes outbound IPv6 packets from the TUN reader for session
43    /// encapsulation and routing through the mesh.
44    ///
45    /// Also processes DNS-resolved identities for identity cache population.
46    ///
47    /// Also runs a periodic tick (1s) to clean up stale handshake connections
48    /// that never received a response. This prevents resource leaks when peers
49    /// are unreachable.
50    ///
51    /// This method takes ownership of the packet_rx channel and runs
52    /// until the channel is closed (typically when stop() is called).
53    pub async fn run_rx_loop(&mut self) -> Result<(), NodeError> {
54        let mut packet_rx = self.packet_rx.take().ok_or(NodeError::NotStarted)?;
55
56        // Take the TUN outbound receiver, or create a dummy channel that never
57        // produces messages (when TUN is disabled). Holding the sender prevents
58        // the channel from closing.
59        let (mut tun_outbound_rx, _tun_guard) = match self.tun_outbound_rx.take() {
60            Some(rx) => (rx, None),
61            None => {
62                let (tx, rx) = tokio::sync::mpsc::channel(1);
63                (rx, Some(tx))
64            }
65        };
66
67        // Take the DNS identity receiver, or create a dummy channel (when DNS
68        // is disabled). Same pattern as TUN outbound.
69        let (mut dns_identity_rx, _dns_guard) = match self.dns_identity_rx.take() {
70            Some(rx) => (rx, None),
71            None => {
72                let (tx, rx) = tokio::sync::mpsc::channel(1);
73                (rx, Some(tx))
74            }
75        };
76
77        // Take the endpoint-data command receiver, or create a dummy channel
78        // when the embedded endpoint API is not in use.
79        let (mut endpoint_priority_command_rx, _endpoint_priority_command_guard) =
80            match self.endpoint_priority_command_rx.take() {
81                Some(rx) => (rx, None),
82                None => {
83                    let (tx, rx) = tokio::sync::mpsc::channel(1);
84                    (rx, Some(tx))
85                }
86            };
87        let (mut endpoint_command_rx, _endpoint_command_guard) =
88            match self.endpoint_command_rx.take() {
89                Some(rx) => (rx, None),
90                None => {
91                    let (tx, rx) = tokio::sync::mpsc::channel(1);
92                    (rx, Some(tx))
93                }
94            };
95        let (mut endpoint_bulk_feedback_rx, _endpoint_bulk_feedback_guard) =
96            match self.endpoint_bulk_feedback_rx.take() {
97                Some(rx) => (rx, None),
98                None => {
99                    let (tx, rx) = tokio::sync::mpsc::channel(1);
100                    (rx, Some(tx))
101                }
102            };
103
104        // Take the decrypt worker fallback receiver if a worker pool
105        // is in use. The worker pushes non-fast-path packets (anything
106        // that's not bulk EndpointData) here for the legacy dispatch.
107        let (mut decrypt_fallback_rx, _decrypt_fallback_guard) =
108            match self.decrypt_fallback_rx.take() {
109                Some(rx) => (rx, None),
110                None => {
111                    let (tx, rx) = crate::node::decrypt_worker::decrypt_worker_fallback_channels();
112                    (rx, Some(tx))
113                }
114            };
115
116        let mut tick =
117            tokio::time::interval(Duration::from_secs(self.config.node.tick_interval_secs));
118        tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
119        let mut maintenance_state = RxLoopMaintenanceState::default();
120
121        // Set up control socket channels. Read-only queries are separated
122        // from mutating commands so operator status reads can get reserved
123        // progress while a command awaits slower discovery/transport work.
124        let (control_query_tx, mut control_query_rx) =
125            tokio::sync::mpsc::channel::<ControlMessage>(32);
126        let (control_command_tx, mut control_command_rx) =
127            tokio::sync::mpsc::channel::<ControlMessage>(32);
128
129        if self.config.node.control.enabled {
130            let config = self.config.node.control.clone();
131            let senders = ControlSenders::new(control_query_tx.clone(), control_command_tx.clone());
132            tokio::spawn(async move {
133                match ControlSocket::bind(&config) {
134                    Ok(socket) => {
135                        socket.accept_loop_split(senders).await;
136                    }
137                    Err(e) => {
138                        warn!(error = %e, "Failed to bind control socket");
139                    }
140                }
141            });
142        }
143        // Drop unused sender to avoid keeping channel open if control is disabled
144        drop(control_query_tx);
145        drop(control_command_tx);
146
147        info!("RX event loop started");
148        // Optional perf profiler (FIPS_PERF=1). No-op otherwise.
149        crate::perf_profile::maybe_spawn_reporter();
150        // Tokio intervals tick immediately on first poll. Consume that startup
151        // tick so the reserved-progress branch below represents a due periodic
152        // maintenance turn, not an eager pre-data maintenance pass.
153        tick.tick().await;
154
155        loop {
156            tokio::select! {
157                biased;
158                // Priority decrypt-worker fallback drains first. The
159                // previous packet-first ordering could hold small ACK,
160                // heartbeat, and failure-report plaintexts behind a hot
161                // raw-packet drain long enough to collapse TCP. Bulk
162                // fallback is intentionally below `packet_rx`: bulk
163                // plaintext must keep making bounded progress, but it
164                // should not stop fresh transport priority packets from
165                // being dequeued. `drain_packet_rx` interleaves fallback
166                // turns every few dozen packets to keep that progress
167                // reserve while avoiding a bulk-fallback convoy.
168                Some(event) = decrypt_fallback_rx.priority.recv() => {
169                    let fallback_drained = self.drain_decrypt_priority_fallback(
170                        &mut decrypt_fallback_rx.priority,
171                        Some(event),
172                        PACKET_DRAIN_BUDGET,
173                    ).await;
174                    let side_drained = self.drain_rx_loop_side_queues(
175                        &mut control_query_rx,
176                        &mut tun_outbound_rx,
177                        &mut endpoint_priority_command_rx,
178                        &mut endpoint_command_rx,
179                        SIDE_QUEUE_INTERLEAVE_BUDGET,
180                    ).await;
181                    if fallback_drained > 0 || side_drained.has_data_drained() {
182                        maintenance_state.record_data_activity(Instant::now());
183                    }
184                }
185                // Timer-driven liveness is a reserved-progress branch. It
186                // performs bounded pre/post data drains and timeboxes slow
187                // discovery/status work, so hot packet or bulk-fallback
188                // queues cannot indefinitely postpone heartbeat, rekey, MMP,
189                // route aging, or path maintenance.
190                _ = tick.tick() => {
191                    let drained = self.drain_rx_loop_data_queues(
192                        &mut packet_rx,
193                        &mut decrypt_fallback_rx,
194                        &mut tun_outbound_rx,
195                        &mut endpoint_priority_command_rx,
196                        &mut endpoint_command_rx,
197                        NON_PACKET_DRAIN_BUDGET,
198                    ).await;
199                    if drained.has_drained() {
200                        maintenance_state.record_data_activity(Instant::now());
201                        debug!(
202                            drained = drained.total(),
203                            drained_packets = drained.packets,
204                            drained_tun = drained.tun,
205                            drained_endpoint = drained.endpoint,
206                            "Drained queued packets before rx-loop maintenance"
207                        );
208                    }
209                    let maintenance_plan = maintenance_state.plan_maintenance(
210                        drained,
211                        Instant::now(),
212                        RX_LOOP_RECENT_DATA_ACTIVITY_WINDOW,
213                        RX_LOOP_SLOW_MAINTENANCE_IDLE_TIMEOUT,
214                        RX_LOOP_SLOW_MAINTENANCE_BUSY_TIMEOUT,
215                    );
216
217                    let slow_timed_out = self.run_rx_loop_maintenance_tick(
218                        maintenance_plan,
219                    ).await;
220                    maintenance_state.record_maintenance_result(
221                        maintenance_plan.data_pressure(),
222                        slow_timed_out,
223                    );
224
225                    let post_drained = self.drain_rx_loop_data_queues(
226                        &mut packet_rx,
227                        &mut decrypt_fallback_rx,
228                        &mut tun_outbound_rx,
229                        &mut endpoint_priority_command_rx,
230                        &mut endpoint_command_rx,
231                        PACKET_DRAIN_BUDGET,
232                    ).await;
233                    if post_drained.has_drained() {
234                        maintenance_state.record_data_activity(Instant::now());
235                        debug!(
236                            drained = post_drained.total(),
237                            drained_packets = post_drained.packets,
238                            drained_tun = post_drained.tun,
239                            drained_endpoint = post_drained.endpoint,
240                            "Drained queued packets after rx-loop maintenance"
241                        );
242                    }
243                }
244                Some(event) = decrypt_fallback_rx.authenticated_bulk.recv(),
245                    if authenticated_bulk_preempts_packet_rx(packet_rx.priority_ready_packets()) =>
246                {
247                    let fallback_drained = self.drain_decrypt_fallback(
248                        &mut decrypt_fallback_rx,
249                        None,
250                        Some(event),
251                        None,
252                        NON_PACKET_DRAIN_BUDGET,
253                    ).await;
254                    let side_drained = self.drain_rx_loop_side_queues(
255                        &mut control_query_rx,
256                        &mut tun_outbound_rx,
257                        &mut endpoint_priority_command_rx,
258                        &mut endpoint_command_rx,
259                        SIDE_QUEUE_INTERLEAVE_BUDGET,
260                    ).await;
261                    if fallback_drained > 0 || side_drained.has_data_drained() {
262                        maintenance_state.record_data_activity(Instant::now());
263                    }
264                }
265                Some(message) = control_query_rx.recv() => {
266                    self.drain_control_queries(
267                        &mut control_query_rx,
268                        Some(message),
269                        NON_PACKET_DRAIN_BUDGET,
270                    ).await;
271                }
272                Some(feedback) = endpoint_bulk_feedback_rx.recv() => {
273                    let drained = self.drain_endpoint_bulk_send_feedback(
274                        &mut endpoint_bulk_feedback_rx,
275                        Some(feedback),
276                        NON_PACKET_DRAIN_BUDGET,
277                    );
278                    if drained > 0 {
279                        maintenance_state.record_data_activity(Instant::now());
280                    }
281                }
282                packet = packet_rx.recv() => {
283                    match packet {
284                        Some(p) => {
285                            let drained = self.drain_packet_rx(
286                                &mut packet_rx,
287                                &mut decrypt_fallback_rx,
288                                Some(RxLoopSideQueues {
289                                    control_query_rx: &mut control_query_rx,
290                                    tun_outbound_rx: &mut tun_outbound_rx,
291                                    endpoint_priority_command_rx: &mut endpoint_priority_command_rx,
292                                    endpoint_command_rx: &mut endpoint_command_rx,
293                                }),
294                                Some(p),
295                                PACKET_DRAIN_BUDGET,
296                            ).await;
297                            if drained > 0 {
298                                maintenance_state.record_data_activity(Instant::now());
299                            }
300                        }
301                        None => break, // channel closed
302                    }
303                }
304                Some(command) = endpoint_priority_command_rx.recv() => {
305                    let drained = self.drain_endpoint_commands(
306                        &mut endpoint_priority_command_rx,
307                        &mut endpoint_command_rx,
308                        Some(command),
309                        None,
310                        NON_PACKET_DRAIN_BUDGET,
311                    ).await;
312                    if drained > 0 {
313                        maintenance_state.record_data_activity(Instant::now());
314                    }
315                }
316                Some(event) = decrypt_fallback_rx.bulk.recv() => {
317                    let fallback_plan = fallback_drain_plan();
318                    let fallback_drained = self.drain_decrypt_fallback(
319                        &mut decrypt_fallback_rx,
320                        None,
321                        None,
322                        Some(event),
323                        fallback_plan.trailing_budget,
324                    ).await;
325                    let side_drained = self.drain_rx_loop_side_queues(
326                        &mut control_query_rx,
327                        &mut tun_outbound_rx,
328                        &mut endpoint_priority_command_rx,
329                        &mut endpoint_command_rx,
330                        SIDE_QUEUE_INTERLEAVE_BUDGET,
331                    ).await;
332                    if fallback_drained > 0 || side_drained.has_data_drained() {
333                        maintenance_state.record_data_activity(Instant::now());
334                    }
335                }
336                Some(ipv6_packet) = tun_outbound_rx.recv() => {
337                    let drained = self.drain_tun_outbound(
338                        &mut tun_outbound_rx,
339                        Some(ipv6_packet),
340                        NON_PACKET_DRAIN_BUDGET,
341                    ).await;
342                    if drained > 0 {
343                        maintenance_state.record_data_activity(Instant::now());
344                    }
345                }
346                Some(identity) = dns_identity_rx.recv() => {
347                    debug!(
348                        node_addr = %identity.node_addr,
349                        "Registering identity from DNS resolution"
350                    );
351                    self.register_identity(identity.node_addr, identity.pubkey);
352                }
353                Some(command) = endpoint_command_rx.recv() => {
354                    let drained = self.drain_endpoint_commands(
355                        &mut endpoint_priority_command_rx,
356                        &mut endpoint_command_rx,
357                        None,
358                        Some(command),
359                        NON_PACKET_DRAIN_BUDGET,
360                    ).await;
361                    if drained > 0 {
362                        maintenance_state.record_data_activity(Instant::now());
363                    }
364                }
365                Some((request, response_tx)) = control_command_rx.recv() => {
366                    let response = commands::dispatch(
367                        self,
368                        &request.command,
369                        request.params.as_ref(),
370                    ).await;
371                    let _ = response_tx.send(response);
372                }
373            }
374        }
375
376        info!("RX event loop stopped (channel closed)");
377        Ok(())
378    }
379
380    async fn drain_rx_loop_data_queues(
381        &mut self,
382        packet_rx: &mut PacketRx,
383        decrypt_fallback_rx: &mut DecryptWorkerFallbackReceivers,
384        tun_outbound_rx: &mut TunOutboundRx,
385        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
386        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
387        budget: usize,
388    ) -> RxLoopDataDrainStats {
389        let drained_packets = self
390            .drain_packet_rx(packet_rx, decrypt_fallback_rx, None, None, budget)
391            .await;
392        let non_packet_budget = non_packet_drain_budget(budget);
393        let drained_tun = self
394            .drain_tun_outbound(tun_outbound_rx, None, non_packet_budget)
395            .await;
396        let drained_endpoint = self
397            .drain_endpoint_commands(
398                endpoint_priority_command_rx,
399                endpoint_command_rx,
400                None,
401                None,
402                non_packet_budget,
403            )
404            .await;
405        RxLoopDataDrainStats::new(drained_packets, drained_tun, drained_endpoint)
406    }
407
408    async fn drain_packet_rx(
409        &mut self,
410        packet_rx: &mut PacketRx,
411        decrypt_fallback_rx: &mut DecryptWorkerFallbackReceivers,
412        mut side_queues: Option<RxLoopSideQueues<'_>>,
413        first_packet: Option<ReceivedPacket>,
414        budget: usize,
415    ) -> usize {
416        // Drain remaining ready inbound packets in a tight loop before
417        // yielding back to select! Every yield is a scheduler hop, and at
418        // line rate transports typically have several packets available per
419        // wake. Caps at a batch boundary so other branches eventually get a
420        // turn even under sustained load.
421        self.begin_endpoint_event_batch();
422        let side_queue_interleave_every = if side_queues.is_some() {
423            SIDE_QUEUE_INTERLEAVE_EVERY
424        } else {
425            0
426        };
427        let fallback_plan = fallback_drain_plan();
428        let mut drain = PacketDrainCursor::new(
429            first_packet,
430            budget,
431            fallback_plan.interleave_every,
432            side_queue_interleave_every,
433        );
434        let mut decrypt_jobs = DecryptJobBatcher::new();
435        while let Some(action) = drain.next(packet_rx) {
436            match action {
437                PacketDrainAction::Packet(packet) => {
438                    let action = self.begin_process_packet(packet);
439                    match action {
440                        PacketProcessAction::DecryptJob { job } => {
441                            if let Some(workers) = self.decrypt_workers.as_ref() {
442                                decrypt_jobs.push(workers, job);
443                            }
444                        }
445                        PacketProcessAction::Done => {}
446                        action => {
447                            self.flush_decrypt_job_batcher(&mut decrypt_jobs);
448                            self.finish_packet_process(action).await;
449                        }
450                    }
451                }
452                PacketDrainAction::InterleaveFallback => {
453                    self.flush_decrypt_job_batcher(&mut decrypt_jobs);
454                    let drained = if decrypt_fallback_has_ready(decrypt_fallback_rx) {
455                        self.drain_decrypt_fallback(
456                            decrypt_fallback_rx,
457                            None,
458                            None,
459                            None,
460                            fallback_plan.interleave_budget,
461                        )
462                        .await
463                    } else {
464                        0
465                    };
466                    if drained == 0 {
467                        drain.refund_empty_interleave_turn();
468                    }
469                }
470                PacketDrainAction::InterleaveSideQueues => {
471                    self.flush_decrypt_job_batcher(&mut decrypt_jobs);
472                    let drained = if let Some(side_queues) = side_queues.as_mut() {
473                        if rx_loop_side_queues_have_ready(side_queues) {
474                            self.drain_rx_loop_side_queues(
475                                side_queues.control_query_rx,
476                                side_queues.tun_outbound_rx,
477                                side_queues.endpoint_priority_command_rx,
478                                side_queues.endpoint_command_rx,
479                                SIDE_QUEUE_INTERLEAVE_BUDGET,
480                            )
481                            .await
482                        } else {
483                            RxLoopDataDrainStats::default()
484                        }
485                    } else {
486                        RxLoopDataDrainStats::default()
487                    };
488                    if !drained.has_drained() {
489                        drain.refund_empty_interleave_turn();
490                    }
491                }
492            }
493        }
494
495        self.flush_decrypt_job_batcher(&mut decrypt_jobs);
496        let drained = drain.drained();
497        if drained > 0 {
498            // One trailing fallback slice so the last bounced packets of the
499            // burst aren't held up by the post-burst send flush. Keep it a
500            // non-packet turn: bulk fallback should not convoy ahead of fresh
501            // transport receive work after every hot packet drain.
502            self.drain_decrypt_fallback(
503                decrypt_fallback_rx,
504                None,
505                None,
506                None,
507                fallback_plan.trailing_budget.min(budget),
508            )
509            .await;
510            self.finish_endpoint_event_batch();
511        } else {
512            self.finish_endpoint_event_batch();
513        }
514        drained
515    }
516
517    async fn drain_rx_loop_side_queues(
518        &mut self,
519        control_query_rx: &mut Receiver<ControlMessage>,
520        tun_outbound_rx: &mut TunOutboundRx,
521        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
522        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
523        budget: usize,
524    ) -> RxLoopDataDrainStats {
525        let control_budget = budget.min(CONTROL_QUERY_INTERLEAVE_BUDGET);
526        let drained_control = self
527            .drain_control_queries(control_query_rx, None, control_budget)
528            .await;
529        let remaining_budget = budget.saturating_sub(drained_control);
530        let (endpoint_budget, tun_budget) = split_side_queue_budget(remaining_budget);
531        let mut drained_endpoint = self
532            .drain_endpoint_commands(
533                endpoint_priority_command_rx,
534                endpoint_command_rx,
535                None,
536                None,
537                endpoint_budget,
538            )
539            .await;
540        let mut drained_tun = self
541            .drain_tun_outbound(tun_outbound_rx, None, tun_budget)
542            .await;
543
544        let endpoint_remainder = remaining_side_queue_budget(endpoint_budget, drained_endpoint);
545        let tun_remainder = remaining_side_queue_budget(tun_budget, drained_tun);
546        if endpoint_remainder > 0 && !tun_outbound_rx.is_empty() {
547            drained_tun += self
548                .drain_tun_outbound(tun_outbound_rx, None, endpoint_remainder)
549                .await;
550        }
551        if tun_remainder > 0
552            && (!endpoint_priority_command_rx.is_empty() || !endpoint_command_rx.is_empty())
553        {
554            drained_endpoint += self
555                .drain_endpoint_commands(
556                    endpoint_priority_command_rx,
557                    endpoint_command_rx,
558                    None,
559                    None,
560                    tun_remainder,
561                )
562                .await;
563        }
564
565        RxLoopDataDrainStats::with_control(0, drained_tun, drained_endpoint, drained_control)
566    }
567
568    async fn drain_control_queries(
569        &mut self,
570        control_query_rx: &mut Receiver<ControlMessage>,
571        first_message: Option<ControlMessage>,
572        budget: usize,
573    ) -> usize {
574        let mut drain = SingleLaneDrainCursor::new(first_message, budget);
575        while let Some((request, response_tx)) = drain.next(control_query_rx) {
576            let response = queries::dispatch(self, &request.command, request.params.as_ref());
577            let _ = response_tx.send(response);
578        }
579
580        drain.drained()
581    }
582
583    async fn drain_tun_outbound(
584        &mut self,
585        tun_outbound_rx: &mut TunOutboundRx,
586        first_packet: Option<Vec<u8>>,
587        budget: usize,
588    ) -> usize {
589        let mut drain = SingleLaneDrainCursor::new(first_packet, budget);
590        while let Some(packet) = drain.next(tun_outbound_rx) {
591            self.handle_tun_outbound(packet).await;
592        }
593
594        drain.drained()
595    }
596
597    async fn drain_endpoint_commands(
598        &mut self,
599        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
600        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
601        first_priority_command: Option<NodeEndpointCommand>,
602        first_bulk_command: Option<NodeEndpointCommand>,
603        budget: usize,
604    ) -> usize {
605        let mut drain =
606            PriorityBulkDrainCursor::new(first_priority_command, first_bulk_command, budget);
607        while let Some(command) = drain.next(endpoint_priority_command_rx, endpoint_command_rx) {
608            let drain_cost = command.drain_cost();
609            match command.into_send_batch_oneway() {
610                Ok((batch, _lane)) => {
611                    let mut batch_commands = vec![batch];
612                    self.coalesce_endpoint_send_batch_commands(
613                        &mut drain,
614                        endpoint_priority_command_rx,
615                        endpoint_command_rx,
616                        &mut batch_commands,
617                    );
618                    self.handle_endpoint_send_batch_commands(batch_commands)
619                        .await;
620                }
621                Err(command) => {
622                    self.handle_endpoint_data_command(command).await;
623                }
624            }
625            drain.charge_extra(drain_cost.saturating_sub(1));
626        }
627
628        drain.drained()
629    }
630
631    fn drain_endpoint_bulk_send_feedback(
632        &mut self,
633        endpoint_bulk_feedback_rx: &mut Receiver<crate::node::EndpointBulkSendFeedback>,
634        first_feedback: Option<crate::node::EndpointBulkSendFeedback>,
635        budget: usize,
636    ) -> usize {
637        let mut drain = SingleLaneDrainCursor::new(first_feedback, budget);
638        while let Some(feedback) = drain.next(endpoint_bulk_feedback_rx) {
639            self.apply_endpoint_bulk_send_feedback(feedback);
640        }
641
642        drain.drained()
643    }
644
645    fn coalesce_endpoint_send_batch_commands(
646        &mut self,
647        drain: &mut PriorityBulkDrainCursor<NodeEndpointCommand>,
648        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
649        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
650        batch_commands: &mut Vec<EndpointSendBatchCommand>,
651    ) {
652        let mut payloads = batch_commands
653            .iter()
654            .fold(0usize, |total, command| total.saturating_add(command.len()));
655        while payloads < ENDPOINT_COMMAND_COALESCE_MAX_PACKETS {
656            let Some(command) =
657                drain.next_bulk_if_no_priority(endpoint_priority_command_rx, endpoint_command_rx)
658            else {
659                break;
660            };
661            let drain_cost = command.drain_cost();
662            match command.into_send_batch_oneway() {
663                Ok((batch, _lane))
664                    if batch_commands.last().is_some_and(|last| {
665                        last.can_coalesce_with(&batch, ENDPOINT_COMMAND_COALESCE_MAX_PACKETS)
666                    }) =>
667                {
668                    payloads = payloads.saturating_add(batch.len());
669                    batch_commands.push(batch);
670                    drain.charge_extra(drain_cost.saturating_sub(1));
671                }
672                Ok((batch, lane)) => {
673                    drain.defer_bulk(NodeEndpointCommand::SendBatchOneway {
674                        command: batch,
675                        lane,
676                    });
677                    break;
678                }
679                Err(command) => {
680                    drain.defer_bulk(command);
681                    break;
682                }
683            }
684        }
685    }
686
687    async fn run_rx_loop_maintenance_tick(&mut self, plan: RxLoopMaintenancePlan) -> bool {
688        self.check_timeouts();
689        let now_ms = Self::now_ms();
690        // Link/session liveness must run before slower retry/discovery work:
691        // under bulk send pressure a late heartbeat or MMP report is
692        // indistinguishable from a dead direct path on the remote peer.
693        self.check_link_heartbeats().await;
694        self.reload_peer_acl();
695        self.resend_pending_handshakes(now_ms).await;
696        self.resend_pending_rekeys(now_ms).await;
697        self.resend_pending_session_handshakes(now_ms).await;
698        self.resend_pending_session_msg3(now_ms).await;
699        self.purge_idle_sessions(now_ms);
700        self.purge_learned_routes(now_ms);
701        self.check_mmp_reports().await;
702        self.check_session_mmp_reports().await;
703        self.check_rekey().await;
704        self.check_session_rekey().await;
705        self.check_pending_lookups(now_ms).await;
706        self.poll_pending_connects().await;
707        self.process_pending_retries(now_ms).await;
708        self.poll_transport_discovery().await;
709        self.activate_connected_udp_sessions().await;
710        self.sample_transport_congestion();
711
712        let Some(slow_timeout) = plan.slow_timeout() else {
713            crate::perf_profile::record_event(
714                crate::perf_profile::Event::RxLoopSlowMaintenanceSkipped,
715            );
716            return false;
717        };
718
719        if tokio::time::timeout(slow_timeout, self.run_rx_loop_slow_maintenance_tick())
720            .await
721            .is_err()
722        {
723            crate::perf_profile::record_event(
724                crate::perf_profile::Event::RxLoopSlowMaintenanceTimeout,
725            );
726            self.mark_rx_loop_maintenance_timeout();
727            warn!(
728                timeout_ms = slow_timeout.as_millis() as u64,
729                data_pressure = plan.data_pressure(),
730                "RX loop slow maintenance timed out; continuing packet processing"
731            );
732            return true;
733        }
734        false
735    }
736
737    async fn run_rx_loop_slow_maintenance_tick(&mut self) {
738        if let Some(delay) = rx_loop_slow_maintenance_fault_delay() {
739            tokio::time::sleep(delay).await;
740        }
741
742        // Discovery and graph/stat maintenance can involve relay work or
743        // larger scans. Keep it bounded after direct-path liveness and session
744        // upkeep so a slow Nostr/LAN tick degrades discovery freshness, not
745        // packet flow.
746        self.poll_nostr_discovery().await;
747        self.poll_lan_discovery().await;
748        self.poll_local_instance_discovery().await;
749        self.check_tree_state().await;
750        self.check_bloom_state().await;
751        self.compute_mesh_size();
752        self.record_stats_history();
753    }
754
755    /// Hand a decrypt-worker fallback to the canonical post-FMP-decrypt
756    /// processor as one authenticated receive envelope. The envelope keeps the
757    /// worker-captured source peer, FMP flags, packet facts, and plaintext slice
758    /// together so peer bookkeeping and link dispatch cannot drift apart.
759    async fn process_decrypt_worker_event(&mut self, event: DecryptWorkerEvent) {
760        event.record_queue_wait();
761        match event {
762            DecryptWorkerEvent::Plaintext(fallback) => {
763                self.process_decrypt_fallback(fallback).await;
764            }
765            DecryptWorkerEvent::PlaintextBatch(fallbacks) => {
766                for fallback in fallbacks {
767                    self.process_decrypt_fallback(fallback).await;
768                }
769            }
770            DecryptWorkerEvent::AuthenticatedFmpReceive(receive) => {
771                self.process_authenticated_fmp_receive_from_worker(receive);
772            }
773            DecryptWorkerEvent::AuthenticatedSession(session) => {
774                self.process_authenticated_session_from_worker(session)
775                    .await;
776            }
777            DecryptWorkerEvent::AuthenticatedSessionBatch(sessions) => {
778                self.process_authenticated_session_batch_from_worker(sessions)
779                    .await;
780            }
781            DecryptWorkerEvent::DirectSessionCommit(commit) => {
782                self.process_direct_session_commit_from_worker(commit).await;
783            }
784            DecryptWorkerEvent::DirectSessionCommitBatch(commits) => {
785                self.process_direct_session_commit_batch_from_worker(commits)
786                    .await;
787            }
788            DecryptWorkerEvent::DirectSessionData(direct) => {
789                self.process_direct_session_data_from_worker(direct).await;
790            }
791            DecryptWorkerEvent::DirectSessionDataBatch(directs) => {
792                self.process_direct_session_data_batch_from_worker(directs)
793                    .await;
794            }
795            DecryptWorkerEvent::FspDecryptFailure(report) => {
796                self.process_fsp_decrypt_failure_from_worker(report).await;
797            }
798            DecryptWorkerEvent::DecryptFailure(report) => {
799                self.process_decrypt_failure_report(report).await;
800            }
801        }
802    }
803
804    async fn process_decrypt_fallback(&mut self, fallback: DecryptFallback) {
805        let plaintext = &fallback.packet_data[fallback.fmp_plaintext_offset
806            ..fallback.fmp_plaintext_offset + fallback.fmp_plaintext_len];
807        self.process_authentic_fmp_plaintext(AuthenticatedFmpPlaintext::new(
808            fallback.source_peer,
809            fallback.transport_id,
810            &fallback.remote_addr,
811            fallback.timestamp_ms,
812            fallback.packet_len,
813            fallback.fmp_counter,
814            fallback.fmp_flags,
815            plaintext,
816        ))
817        .await;
818    }
819
820    async fn process_decrypt_failure_report(&mut self, report: DecryptFailureReport) {
821        debug!(
822            peer = %self.peer_display_name(report.source_peer.node_addr()),
823            counter = report.fmp_counter,
824            replay_highest = report.fmp_replay_highest,
825            "Worker FMP AEAD decryption failed"
826        );
827        self.handle_decrypt_failure_report(&report).await;
828    }
829
830    /// Drain only the priority decrypt-worker fallback lane.
831    ///
832    /// This is the top-level reserved-progress arm: priority plaintext and
833    /// decrypt failures get first service, but bulk fallback stays behind
834    /// `packet_rx` unless it is explicitly interleaved inside a packet drain
835    /// or selected by its own lower-priority branch.
836    async fn drain_decrypt_priority_fallback(
837        &mut self,
838        priority_rx: &mut Receiver<DecryptWorkerEvent>,
839        first_event: Option<DecryptWorkerEvent>,
840        budget: usize,
841    ) -> usize {
842        self.begin_endpoint_event_batch();
843        let mut drain = SingleLaneDrainCursor::new(first_event, budget);
844        while let Some(event) = drain.next(priority_rx) {
845            self.process_decrypt_worker_event(event).await;
846        }
847        let drained = drain.drained();
848        self.finish_endpoint_event_batch();
849        drained
850    }
851
852    /// Drain up to `budget` queued fallbacks without yielding back to
853    /// `select!`. Returns the number processed. Called both from the
854    /// bulk-fallback select arm (after the selected head item) and interleaved
855    /// inside the packet_rx drain loop so bounced FMP plaintexts can't
856    /// accumulate behind a hot inbound packet turn.
857    async fn drain_decrypt_fallback(
858        &mut self,
859        rx: &mut DecryptWorkerFallbackReceivers,
860        first_priority_event: Option<DecryptWorkerEvent>,
861        first_authenticated_bulk_event: Option<DecryptWorkerEvent>,
862        first_bulk_event: Option<DecryptWorkerEvent>,
863        budget: usize,
864    ) -> usize {
865        self.begin_endpoint_event_batch();
866        let mut drain = DecryptReturnDrainCursor::new(
867            first_priority_event,
868            first_authenticated_bulk_event,
869            first_bulk_event,
870            budget,
871        );
872        while let Some(event) =
873            drain.next(&mut rx.priority, &mut rx.authenticated_bulk, &mut rx.bulk)
874        {
875            rx.release_dequeued_event(&event);
876            let extra = event.packet_count().saturating_sub(1);
877            self.process_decrypt_worker_event(event).await;
878            drain.charge_extra(extra);
879        }
880        let drained = drain.drained();
881        self.finish_endpoint_event_batch();
882        drained
883    }
884
885    /// Process a single received packet.
886    ///
887    /// Dispatches based on the phase field in the 4-byte common prefix.
888    #[cfg(test)]
889    pub(in crate::node) async fn process_packet(&mut self, packet: ReceivedPacket) {
890        let action = self.begin_process_packet(packet);
891        self.finish_packet_process(action).await;
892    }
893
894    fn begin_process_packet(&mut self, packet: ReceivedPacket) -> PacketProcessAction {
895        let timer = crate::perf_profile::Timer::start(crate::perf_profile::Stage::ProcessPacket);
896        let priority_sized = packet.is_priority_sized();
897        let priority_count = u64::from(priority_sized);
898        let bulk_count = u64::from(!priority_sized);
899        crate::perf_profile::record_since_split_count(
900            crate::perf_profile::Stage::TransportQueueWait,
901            crate::perf_profile::Stage::TransportPriorityQueueWait,
902            crate::perf_profile::Stage::TransportBulkQueueWait,
903            packet.trace_enqueued_at,
904            1,
905            priority_count,
906            bulk_count,
907        );
908        crate::perf_profile::record_since_split_count(
909            crate::perf_profile::Stage::TransportRxLoopWait,
910            crate::perf_profile::Stage::TransportPriorityRxLoopWait,
911            crate::perf_profile::Stage::TransportBulkRxLoopWait,
912            packet.trace_rx_loop_owned_at,
913            1,
914            priority_count,
915            bulk_count,
916        );
917        if is_punch_packet(&packet.data) {
918            trace!(
919                transport_id = %packet.transport_id,
920                remote_addr = %packet.remote_addr,
921                bytes = packet.data.len(),
922                "Dropping stray punch probe/ack in FMP rx loop"
923            );
924            return PacketProcessAction::Done;
925        }
926        if packet.data.len() < COMMON_PREFIX_SIZE {
927            return PacketProcessAction::Done; // Drop packets too short for common prefix
928        }
929
930        let prefix = match CommonPrefix::parse(&packet.data) {
931            Some(p) => p,
932            None => return PacketProcessAction::Done, // Malformed prefix
933        };
934        if matches!(prefix.phase, PHASE_MSG1 | PHASE_MSG2) {
935            debug!(
936                transport_id = %packet.transport_id,
937                remote_addr = %packet.remote_addr,
938                bytes = packet.data.len(),
939                phase = prefix.phase,
940                version = prefix.version,
941                "FMP handshake packet dispatch"
942            );
943        } else {
944            trace!(
945                transport_id = %packet.transport_id,
946                remote_addr = %packet.remote_addr,
947                bytes = packet.data.len(),
948                phase = prefix.phase,
949                version = prefix.version,
950                "FMP packet dispatch"
951            );
952        }
953
954        if prefix.version != FMP_VERSION {
955            debug!(
956                version = prefix.version,
957                transport_id = %packet.transport_id,
958                "Unknown FMP version, dropping"
959            );
960
961            // If the packet arrived on an adopted Nostr-NAT bootstrap
962            // transport, the originating peer is necessarily on a
963            // different FMP-protocol version than us — the discovery
964            // sweep would otherwise re-traverse them every cycle even
965            // though no msg1/msg2 exchange can ever succeed. Bump the
966            // discovery-layer cooldown to the long protocol-mismatch
967            // window and emit a single WARN per fresh observation.
968            let looks_like_fmp_phase =
969                matches!(prefix.phase, PHASE_ESTABLISHED | PHASE_MSG1 | PHASE_MSG2);
970            if looks_like_fmp_phase
971                && self.bootstrap_transports.contains(&packet.transport_id)
972                && let Some(npub) = self.bootstrap_transports.peer_npub(&packet.transport_id)
973                && let Some(handle) = self.nostr_discovery_handle()
974            {
975                let now_ms = Self::now_ms();
976                let cooldown_secs = handle.protocol_mismatch_cooldown_secs();
977                if handle.record_protocol_mismatch(npub, now_ms) {
978                    warn!(
979                        peer_npub = %npub,
980                        transport_id = %packet.transport_id,
981                        peer_version = prefix.version,
982                        our_version = FMP_VERSION,
983                        cooldown_secs,
984                        "Nostr-discovered peer speaks a different FMP version; suppressing retraversal"
985                    );
986                }
987            }
988            return PacketProcessAction::Done;
989        }
990
991        match prefix.phase {
992            PHASE_ESTABLISHED => match self.try_prepare_encrypted_frame_for_worker(packet) {
993                EncryptedFrameFastPath::Dispatch(job) => PacketProcessAction::DecryptJob { job },
994                EncryptedFrameFastPath::Dropped => PacketProcessAction::Done,
995                EncryptedFrameFastPath::Slow(packet) => {
996                    PacketProcessAction::EncryptedSlow { packet, timer }
997                }
998            },
999            PHASE_MSG1 => PacketProcessAction::Msg1 { packet, timer },
1000            PHASE_MSG2 => PacketProcessAction::Msg2 { packet, timer },
1001            _ => {
1002                debug!(
1003                    phase = prefix.phase,
1004                    transport_id = %packet.transport_id,
1005                    "Unknown FMP phase, dropping"
1006                );
1007                PacketProcessAction::Done
1008            }
1009        }
1010    }
1011
1012    async fn finish_packet_process(&mut self, action: PacketProcessAction) {
1013        match action {
1014            PacketProcessAction::Done => {}
1015            PacketProcessAction::DecryptJob { job } => {
1016                if let Some(workers) = self.decrypt_workers.as_ref() {
1017                    workers.dispatch_job(job);
1018                }
1019            }
1020            PacketProcessAction::EncryptedSlow {
1021                packet,
1022                timer: _timer,
1023            } => {
1024                self.handle_encrypted_frame_slow(packet).await;
1025            }
1026            PacketProcessAction::Msg1 {
1027                packet,
1028                timer: _timer,
1029            } => {
1030                self.handle_msg1(packet).await;
1031            }
1032            PacketProcessAction::Msg2 {
1033                packet,
1034                timer: _timer,
1035            } => {
1036                self.handle_msg2(packet).await;
1037            }
1038        }
1039    }
1040
1041    fn flush_decrypt_job_batcher(&self, batcher: &mut DecryptJobBatcher) {
1042        if let Some(workers) = self.decrypt_workers.as_ref() {
1043            batcher.flush(workers);
1044        }
1045    }
1046}