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 endpoint_bulk_feedback_rx,
177                        &mut tun_outbound_rx,
178                        &mut endpoint_priority_command_rx,
179                        &mut endpoint_command_rx,
180                        SIDE_QUEUE_INTERLEAVE_BUDGET,
181                    ).await;
182                    if fallback_drained > 0 || side_drained.has_data_drained() {
183                        maintenance_state.record_data_activity(Instant::now());
184                    }
185                }
186                // Timer-driven liveness is a reserved-progress branch. It
187                // performs bounded pre/post data drains and timeboxes slow
188                // discovery/status work, so hot packet or bulk-fallback
189                // queues cannot indefinitely postpone heartbeat, rekey, MMP,
190                // route aging, or path maintenance.
191                _ = tick.tick() => {
192                    let drained = self.drain_rx_loop_data_queues(
193                        &mut packet_rx,
194                        &mut decrypt_fallback_rx,
195                        &mut endpoint_bulk_feedback_rx,
196                        &mut tun_outbound_rx,
197                        &mut endpoint_priority_command_rx,
198                        &mut endpoint_command_rx,
199                        NON_PACKET_DRAIN_BUDGET,
200                    ).await;
201                    if drained.has_drained() {
202                        maintenance_state.record_data_activity(Instant::now());
203                        debug!(
204                            drained = drained.total(),
205                            drained_packets = drained.packets,
206                            drained_decrypt = drained.decrypt,
207                            drained_tun = drained.tun,
208                            drained_endpoint = drained.endpoint,
209                            "Drained queued packets before rx-loop maintenance"
210                        );
211                    }
212                    let maintenance_plan = maintenance_state.plan_maintenance(
213                        drained,
214                        Instant::now(),
215                        RX_LOOP_RECENT_DATA_ACTIVITY_WINDOW,
216                        RX_LOOP_SLOW_MAINTENANCE_IDLE_TIMEOUT,
217                        RX_LOOP_SLOW_MAINTENANCE_BUSY_TIMEOUT,
218                    );
219
220                    let slow_timed_out = self.run_rx_loop_maintenance_tick(
221                        maintenance_plan,
222                    ).await;
223                    maintenance_state.record_maintenance_result(
224                        maintenance_plan.data_pressure(),
225                        slow_timed_out,
226                    );
227
228                    let post_drained = self.drain_rx_loop_data_queues(
229                        &mut packet_rx,
230                        &mut decrypt_fallback_rx,
231                        &mut endpoint_bulk_feedback_rx,
232                        &mut tun_outbound_rx,
233                        &mut endpoint_priority_command_rx,
234                        &mut endpoint_command_rx,
235                        PACKET_DRAIN_BUDGET,
236                    ).await;
237                    if post_drained.has_drained() {
238                        maintenance_state.record_data_activity(Instant::now());
239                        debug!(
240                            drained = post_drained.total(),
241                            drained_packets = post_drained.packets,
242                            drained_decrypt = post_drained.decrypt,
243                            drained_tun = post_drained.tun,
244                            drained_endpoint = post_drained.endpoint,
245                            "Drained queued packets after rx-loop maintenance"
246                        );
247                    }
248                }
249                Some(event) = decrypt_fallback_rx.authenticated_bulk.recv(),
250                    if authenticated_bulk_preempts_packet_rx(packet_rx.priority_ready_packets()) =>
251                {
252                    let fallback_drained = self.drain_decrypt_fallback(
253                        &mut decrypt_fallback_rx,
254                        None,
255                        Some(event),
256                        None,
257                        NON_PACKET_DRAIN_BUDGET,
258                    ).await;
259                    let side_drained = self.drain_rx_loop_side_queues(
260                        &mut control_query_rx,
261                        &mut endpoint_bulk_feedback_rx,
262                        &mut tun_outbound_rx,
263                        &mut endpoint_priority_command_rx,
264                        &mut endpoint_command_rx,
265                        SIDE_QUEUE_INTERLEAVE_BUDGET,
266                    ).await;
267                    if fallback_drained > 0 || side_drained.has_data_drained() {
268                        maintenance_state.record_data_activity(Instant::now());
269                    }
270                }
271                Some(message) = control_query_rx.recv() => {
272                    self.drain_control_queries(
273                        &mut control_query_rx,
274                        Some(message),
275                        NON_PACKET_DRAIN_BUDGET,
276                    ).await;
277                }
278                Some(feedback) = endpoint_bulk_feedback_rx.recv() => {
279                    let drained = self.drain_endpoint_bulk_send_feedback(
280                        &mut endpoint_bulk_feedback_rx,
281                        Some(feedback),
282                        NON_PACKET_DRAIN_BUDGET,
283                    );
284                    if drained > 0 {
285                        maintenance_state.record_data_activity(Instant::now());
286                    }
287                }
288                // Endpoint priority is app-owned latency-sensitive traffic
289                // (ICMP, TCP ACK/SYN, tiny TCP data). On platforms without the
290                // unix encrypt-worker fast path, this branch is the outbound
291                // dataplane path, so give it an explicit turn before hot raw
292                // receive. Bulk endpoint commands intentionally remain below
293                // packet_rx.
294                Some(command) = endpoint_priority_command_rx.recv() => {
295                    let drained = self.drain_endpoint_commands(
296                        &mut endpoint_priority_command_rx,
297                        &mut endpoint_command_rx,
298                        Some(command),
299                        None,
300                        NON_PACKET_DRAIN_BUDGET,
301                    ).await;
302                    if drained > 0 {
303                        maintenance_state.record_data_activity(Instant::now());
304                    }
305                }
306                packet = packet_rx.recv() => {
307                    match packet {
308                        Some(p) => {
309                            let drained = self.drain_packet_rx(
310                                &mut packet_rx,
311                                &mut decrypt_fallback_rx,
312                                Some(RxLoopSideQueues {
313                                    control_query_rx: &mut control_query_rx,
314                                    endpoint_bulk_feedback_rx: &mut endpoint_bulk_feedback_rx,
315                                    tun_outbound_rx: &mut tun_outbound_rx,
316                                    endpoint_priority_command_rx: &mut endpoint_priority_command_rx,
317                                    endpoint_command_rx: &mut endpoint_command_rx,
318                                }),
319                                Some(p),
320                                PACKET_DRAIN_BUDGET,
321                            ).await;
322                            if drained > 0 {
323                                maintenance_state.record_data_activity(Instant::now());
324                            }
325                        }
326                        None => break, // channel closed
327                    }
328                }
329                Some(event) = decrypt_fallback_rx.bulk.recv() => {
330                    let fallback_plan = fallback_drain_plan();
331                    let fallback_drained = self.drain_decrypt_fallback(
332                        &mut decrypt_fallback_rx,
333                        None,
334                        None,
335                        Some(event),
336                        fallback_plan.trailing_budget,
337                    ).await;
338                    let side_drained = self.drain_rx_loop_side_queues(
339                        &mut control_query_rx,
340                        &mut endpoint_bulk_feedback_rx,
341                        &mut tun_outbound_rx,
342                        &mut endpoint_priority_command_rx,
343                        &mut endpoint_command_rx,
344                        SIDE_QUEUE_INTERLEAVE_BUDGET,
345                    ).await;
346                    if fallback_drained > 0 || side_drained.has_data_drained() {
347                        maintenance_state.record_data_activity(Instant::now());
348                    }
349                }
350                Some(ipv6_packet) = tun_outbound_rx.recv() => {
351                    let drained = self.drain_tun_outbound(
352                        &mut tun_outbound_rx,
353                        Some(ipv6_packet),
354                        NON_PACKET_DRAIN_BUDGET,
355                    ).await;
356                    if drained > 0 {
357                        maintenance_state.record_data_activity(Instant::now());
358                    }
359                }
360                Some(identity) = dns_identity_rx.recv() => {
361                    debug!(
362                        node_addr = %identity.node_addr,
363                        "Registering identity from DNS resolution"
364                    );
365                    self.register_identity(identity.node_addr, identity.pubkey);
366                }
367                Some(command) = endpoint_command_rx.recv() => {
368                    let drained = self.drain_endpoint_commands(
369                        &mut endpoint_priority_command_rx,
370                        &mut endpoint_command_rx,
371                        None,
372                        Some(command),
373                        NON_PACKET_DRAIN_BUDGET,
374                    ).await;
375                    if drained > 0 {
376                        maintenance_state.record_data_activity(Instant::now());
377                    }
378                }
379                Some((request, response_tx)) = control_command_rx.recv() => {
380                    let response = commands::dispatch(
381                        self,
382                        &request.command,
383                        request.params.as_ref(),
384                    ).await;
385                    let _ = response_tx.send(response);
386                }
387            }
388        }
389
390        info!("RX event loop stopped (channel closed)");
391        Ok(())
392    }
393
394    async fn drain_rx_loop_data_queues(
395        &mut self,
396        packet_rx: &mut PacketRx,
397        decrypt_fallback_rx: &mut DecryptWorkerFallbackReceivers,
398        endpoint_bulk_feedback_rx: &mut Receiver<crate::node::EndpointBulkSendFeedback>,
399        tun_outbound_rx: &mut TunOutboundRx,
400        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
401        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
402        budget: usize,
403    ) -> RxLoopDataDrainStats {
404        let drained_packets = self
405            .drain_packet_rx(packet_rx, decrypt_fallback_rx, None, None, budget)
406            .await;
407        let non_packet_budget = non_packet_drain_budget(budget);
408        let drained_decrypt = if decrypt_fallback_has_ready(decrypt_fallback_rx) {
409            self.drain_decrypt_fallback(decrypt_fallback_rx, None, None, None, non_packet_budget)
410                .await
411        } else {
412            0
413        };
414        let drained_endpoint_feedback = self.drain_endpoint_bulk_send_feedback(
415            endpoint_bulk_feedback_rx,
416            None,
417            non_packet_budget,
418        );
419        let drained_tun = self
420            .drain_tun_outbound(tun_outbound_rx, None, non_packet_budget)
421            .await;
422        let drained_endpoint = self
423            .drain_endpoint_commands(
424                endpoint_priority_command_rx,
425                endpoint_command_rx,
426                None,
427                None,
428                non_packet_budget,
429            )
430            .await;
431        RxLoopDataDrainStats::with_feedback(
432            drained_packets,
433            drained_decrypt,
434            drained_endpoint_feedback,
435            drained_tun,
436            drained_endpoint,
437        )
438    }
439
440    async fn drain_packet_rx(
441        &mut self,
442        packet_rx: &mut PacketRx,
443        decrypt_fallback_rx: &mut DecryptWorkerFallbackReceivers,
444        mut side_queues: Option<RxLoopSideQueues<'_>>,
445        first_packet: Option<ReceivedPacket>,
446        budget: usize,
447    ) -> usize {
448        // Drain remaining ready inbound packets in a tight loop before
449        // yielding back to select! Every yield is a scheduler hop, and at
450        // line rate transports typically have several packets available per
451        // wake. Caps at a batch boundary so other branches eventually get a
452        // turn even under sustained load.
453        self.begin_endpoint_event_batch();
454        let side_queue_interleave_every = if side_queues.is_some() {
455            SIDE_QUEUE_INTERLEAVE_EVERY
456        } else {
457            0
458        };
459        let fallback_plan = fallback_drain_plan();
460        let mut drain = PacketDrainCursor::new(
461            first_packet,
462            budget,
463            fallback_plan.interleave_every,
464            side_queue_interleave_every,
465        );
466        let mut decrypt_jobs = DecryptJobBatcher::new();
467        while let Some(action) = drain.next(packet_rx) {
468            match action {
469                PacketDrainAction::Packet(packet) => {
470                    let action = self.begin_process_packet(packet);
471                    match action {
472                        PacketProcessAction::DecryptJob { job } => {
473                            if let Some(workers) = self.decrypt_workers.as_ref() {
474                                decrypt_jobs.push(workers, job);
475                            }
476                        }
477                        PacketProcessAction::Done => {}
478                        action => {
479                            self.flush_decrypt_job_batcher(&mut decrypt_jobs);
480                            self.finish_packet_process(action).await;
481                        }
482                    }
483                }
484                PacketDrainAction::InterleaveFallback => {
485                    self.flush_decrypt_job_batcher(&mut decrypt_jobs);
486                    let drained = if decrypt_fallback_has_ready(decrypt_fallback_rx) {
487                        self.drain_decrypt_fallback(
488                            decrypt_fallback_rx,
489                            None,
490                            None,
491                            None,
492                            fallback_plan.interleave_budget,
493                        )
494                        .await
495                    } else {
496                        0
497                    };
498                    if drained == 0 {
499                        drain.refund_empty_interleave_turn();
500                    }
501                }
502                PacketDrainAction::InterleaveSideQueues => {
503                    self.flush_decrypt_job_batcher(&mut decrypt_jobs);
504                    let drained = if let Some(side_queues) = side_queues.as_mut() {
505                        if rx_loop_side_queues_have_ready(side_queues) {
506                            self.drain_rx_loop_side_queues(
507                                side_queues.control_query_rx,
508                                side_queues.endpoint_bulk_feedback_rx,
509                                side_queues.tun_outbound_rx,
510                                side_queues.endpoint_priority_command_rx,
511                                side_queues.endpoint_command_rx,
512                                SIDE_QUEUE_INTERLEAVE_BUDGET,
513                            )
514                            .await
515                        } else {
516                            RxLoopDataDrainStats::default()
517                        }
518                    } else {
519                        RxLoopDataDrainStats::default()
520                    };
521                    if !drained.has_drained() {
522                        drain.refund_empty_interleave_turn();
523                    }
524                }
525            }
526        }
527
528        self.flush_decrypt_job_batcher(&mut decrypt_jobs);
529        let drained = drain.drained();
530        if drained > 0 {
531            // One trailing fallback slice so the last bounced packets of the
532            // burst aren't held up by the post-burst send flush. Keep it a
533            // non-packet turn: bulk fallback should not convoy ahead of fresh
534            // transport receive work after every hot packet drain.
535            self.drain_decrypt_fallback(
536                decrypt_fallback_rx,
537                None,
538                None,
539                None,
540                fallback_plan.trailing_budget.min(budget),
541            )
542            .await;
543            self.finish_endpoint_event_batch();
544        } else {
545            self.finish_endpoint_event_batch();
546        }
547        drained
548    }
549
550    async fn drain_rx_loop_side_queues(
551        &mut self,
552        control_query_rx: &mut Receiver<ControlMessage>,
553        endpoint_bulk_feedback_rx: &mut Receiver<crate::node::EndpointBulkSendFeedback>,
554        tun_outbound_rx: &mut TunOutboundRx,
555        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
556        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
557        budget: usize,
558    ) -> RxLoopDataDrainStats {
559        let drained_endpoint_feedback =
560            self.drain_endpoint_bulk_send_feedback(endpoint_bulk_feedback_rx, None, budget);
561        let feedback_remaining_budget = budget.saturating_sub(drained_endpoint_feedback);
562        let control_budget = feedback_remaining_budget.min(CONTROL_QUERY_INTERLEAVE_BUDGET);
563        let drained_control = self
564            .drain_control_queries(control_query_rx, None, control_budget)
565            .await;
566        let remaining_budget = feedback_remaining_budget.saturating_sub(drained_control);
567        let (endpoint_budget, tun_budget) = split_side_queue_budget(remaining_budget);
568        let mut drained_endpoint = self
569            .drain_endpoint_commands(
570                endpoint_priority_command_rx,
571                endpoint_command_rx,
572                None,
573                None,
574                endpoint_budget,
575            )
576            .await;
577        let mut drained_tun = self
578            .drain_tun_outbound(tun_outbound_rx, None, tun_budget)
579            .await;
580
581        let endpoint_remainder = remaining_side_queue_budget(endpoint_budget, drained_endpoint);
582        let tun_remainder = remaining_side_queue_budget(tun_budget, drained_tun);
583        if endpoint_remainder > 0 && !tun_outbound_rx.is_empty() {
584            drained_tun += self
585                .drain_tun_outbound(tun_outbound_rx, None, endpoint_remainder)
586                .await;
587        }
588        if tun_remainder > 0
589            && (!endpoint_priority_command_rx.is_empty() || !endpoint_command_rx.is_empty())
590        {
591            drained_endpoint += self
592                .drain_endpoint_commands(
593                    endpoint_priority_command_rx,
594                    endpoint_command_rx,
595                    None,
596                    None,
597                    tun_remainder,
598                )
599                .await;
600        }
601
602        RxLoopDataDrainStats::with_control(
603            0,
604            drained_endpoint_feedback,
605            drained_tun,
606            drained_endpoint,
607            drained_control,
608        )
609    }
610
611    async fn drain_control_queries(
612        &mut self,
613        control_query_rx: &mut Receiver<ControlMessage>,
614        first_message: Option<ControlMessage>,
615        budget: usize,
616    ) -> usize {
617        let mut drain = SingleLaneDrainCursor::new(first_message, budget);
618        while let Some((request, response_tx)) = drain.next(control_query_rx) {
619            let response = queries::dispatch(self, &request.command, request.params.as_ref());
620            let _ = response_tx.send(response);
621        }
622
623        drain.drained()
624    }
625
626    async fn drain_tun_outbound(
627        &mut self,
628        tun_outbound_rx: &mut TunOutboundRx,
629        first_packet: Option<Vec<u8>>,
630        budget: usize,
631    ) -> usize {
632        let mut drain = SingleLaneDrainCursor::new(first_packet, budget);
633        while let Some(packet) = drain.next(tun_outbound_rx) {
634            self.handle_tun_outbound(packet).await;
635        }
636
637        drain.drained()
638    }
639
640    async fn drain_endpoint_commands(
641        &mut self,
642        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
643        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
644        first_priority_command: Option<NodeEndpointCommand>,
645        first_bulk_command: Option<NodeEndpointCommand>,
646        budget: usize,
647    ) -> usize {
648        let mut drain =
649            PriorityBulkDrainCursor::new(first_priority_command, first_bulk_command, budget);
650        while let Some(command) = drain.next(endpoint_priority_command_rx, endpoint_command_rx) {
651            let drain_cost = command.drain_cost();
652            match command.into_send_batch_oneway() {
653                Ok((batch, _lane)) => {
654                    let mut batch_commands = vec![batch];
655                    self.coalesce_endpoint_send_batch_commands(
656                        &mut drain,
657                        endpoint_priority_command_rx,
658                        endpoint_command_rx,
659                        &mut batch_commands,
660                    );
661                    self.handle_endpoint_send_batch_commands(batch_commands)
662                        .await;
663                }
664                Err(command) => {
665                    self.handle_endpoint_data_command(command).await;
666                }
667            }
668            drain.charge_extra(drain_cost.saturating_sub(1));
669        }
670
671        drain.drained()
672    }
673
674    fn drain_endpoint_bulk_send_feedback(
675        &mut self,
676        endpoint_bulk_feedback_rx: &mut Receiver<crate::node::EndpointBulkSendFeedback>,
677        first_feedback: Option<crate::node::EndpointBulkSendFeedback>,
678        budget: usize,
679    ) -> usize {
680        let mut drain = SingleLaneDrainCursor::new(first_feedback, budget);
681        while let Some(feedback) = drain.next(endpoint_bulk_feedback_rx) {
682            self.apply_endpoint_bulk_send_feedback(feedback);
683        }
684
685        drain.drained()
686    }
687
688    fn coalesce_endpoint_send_batch_commands(
689        &mut self,
690        drain: &mut PriorityBulkDrainCursor<NodeEndpointCommand>,
691        endpoint_priority_command_rx: &mut Receiver<NodeEndpointCommand>,
692        endpoint_command_rx: &mut Receiver<NodeEndpointCommand>,
693        batch_commands: &mut Vec<EndpointSendBatchCommand>,
694    ) {
695        let mut payloads = batch_commands
696            .iter()
697            .fold(0usize, |total, command| total.saturating_add(command.len()));
698        while payloads < ENDPOINT_COMMAND_COALESCE_MAX_PACKETS {
699            let Some(command) =
700                drain.next_bulk_if_no_priority(endpoint_priority_command_rx, endpoint_command_rx)
701            else {
702                break;
703            };
704            let drain_cost = command.drain_cost();
705            match command.into_send_batch_oneway() {
706                Ok((batch, _lane))
707                    if batch_commands.last().is_some_and(|last| {
708                        last.can_coalesce_with(&batch, ENDPOINT_COMMAND_COALESCE_MAX_PACKETS)
709                    }) =>
710                {
711                    payloads = payloads.saturating_add(batch.len());
712                    batch_commands.push(batch);
713                    drain.charge_extra(drain_cost.saturating_sub(1));
714                }
715                Ok((batch, lane)) => {
716                    drain.defer_bulk(NodeEndpointCommand::SendBatchOneway {
717                        command: batch,
718                        lane,
719                    });
720                    break;
721                }
722                Err(command) => {
723                    drain.defer_bulk(command);
724                    break;
725                }
726            }
727        }
728    }
729
730    async fn run_rx_loop_maintenance_tick(&mut self, plan: RxLoopMaintenancePlan) -> bool {
731        self.check_timeouts();
732        let now_ms = Self::now_ms();
733        // Link/session liveness must run before slower retry/discovery work:
734        // under bulk send pressure a late heartbeat or MMP report is
735        // indistinguishable from a dead direct path on the remote peer.
736        self.check_link_heartbeats().await;
737        self.reload_peer_acl();
738        self.resend_pending_handshakes(now_ms).await;
739        self.resend_pending_rekeys(now_ms).await;
740        self.resend_pending_session_handshakes(now_ms).await;
741        self.resend_pending_session_msg3(now_ms).await;
742        self.purge_idle_sessions(now_ms);
743        self.purge_learned_routes(now_ms);
744        self.check_mmp_reports().await;
745        self.check_session_mmp_reports().await;
746        self.check_rekey().await;
747        self.check_session_rekey().await;
748        self.check_pending_lookups(now_ms).await;
749        self.poll_pending_connects().await;
750        self.process_pending_retries(now_ms).await;
751        self.poll_transport_discovery().await;
752        self.activate_connected_udp_sessions().await;
753        self.sample_transport_congestion();
754
755        let Some(slow_timeout) = plan.slow_timeout() else {
756            crate::perf_profile::record_event(
757                crate::perf_profile::Event::RxLoopSlowMaintenanceSkipped,
758            );
759            return false;
760        };
761
762        if tokio::time::timeout(slow_timeout, self.run_rx_loop_slow_maintenance_tick())
763            .await
764            .is_err()
765        {
766            crate::perf_profile::record_event(
767                crate::perf_profile::Event::RxLoopSlowMaintenanceTimeout,
768            );
769            self.mark_rx_loop_maintenance_timeout();
770            warn!(
771                timeout_ms = slow_timeout.as_millis() as u64,
772                data_pressure = plan.data_pressure(),
773                "RX loop slow maintenance timed out; continuing packet processing"
774            );
775            return true;
776        }
777        false
778    }
779
780    async fn run_rx_loop_slow_maintenance_tick(&mut self) {
781        if let Some(delay) = rx_loop_slow_maintenance_fault_delay() {
782            tokio::time::sleep(delay).await;
783        }
784
785        // Discovery and graph/stat maintenance can involve relay work or
786        // larger scans. Keep it bounded after direct-path liveness and session
787        // upkeep so a slow Nostr/LAN tick degrades discovery freshness, not
788        // packet flow.
789        self.poll_nostr_discovery().await;
790        self.poll_lan_discovery().await;
791        self.poll_local_instance_discovery().await;
792        self.check_tree_state().await;
793        self.check_bloom_state().await;
794        self.compute_mesh_size();
795        self.record_stats_history();
796    }
797
798    /// Hand a decrypt-worker fallback to the canonical post-FMP-decrypt
799    /// processor as one authenticated receive envelope. The envelope keeps the
800    /// worker-captured source peer, FMP flags, packet facts, and plaintext slice
801    /// together so peer bookkeeping and link dispatch cannot drift apart.
802    async fn process_decrypt_worker_event(&mut self, event: DecryptWorkerEvent) {
803        event.record_queue_wait();
804        match event {
805            DecryptWorkerEvent::Plaintext(fallback) => {
806                self.process_decrypt_fallback(fallback).await;
807            }
808            DecryptWorkerEvent::PlaintextBatch(fallbacks) => {
809                for fallback in fallbacks {
810                    self.process_decrypt_fallback(fallback).await;
811                }
812            }
813            DecryptWorkerEvent::AuthenticatedFmpReceive(receive) => {
814                self.process_authenticated_fmp_receive_from_worker(receive);
815            }
816            DecryptWorkerEvent::AuthenticatedSession(session) => {
817                self.process_authenticated_session_from_worker(session)
818                    .await;
819            }
820            DecryptWorkerEvent::AuthenticatedSessionBatch(sessions) => {
821                self.process_authenticated_session_batch_from_worker(sessions)
822                    .await;
823            }
824            DecryptWorkerEvent::DirectSessionCommit(commit) => {
825                self.process_direct_session_commit_from_worker(commit).await;
826            }
827            DecryptWorkerEvent::DirectSessionCommitBatch(commits) => {
828                self.process_direct_session_commit_batch_from_worker(commits)
829                    .await;
830            }
831            DecryptWorkerEvent::DirectSessionData(direct) => {
832                self.process_direct_session_data_from_worker(direct).await;
833            }
834            DecryptWorkerEvent::DirectSessionDataBatch(directs) => {
835                self.process_direct_session_data_batch_from_worker(directs)
836                    .await;
837            }
838            DecryptWorkerEvent::FspDecryptFailure(report) => {
839                self.process_fsp_decrypt_failure_from_worker(report).await;
840            }
841            DecryptWorkerEvent::DecryptFailure(report) => {
842                self.process_decrypt_failure_report(report).await;
843            }
844        }
845    }
846
847    async fn process_decrypt_fallback(&mut self, fallback: DecryptFallback) {
848        let plaintext = &fallback.packet_data[fallback.fmp_plaintext_offset
849            ..fallback.fmp_plaintext_offset + fallback.fmp_plaintext_len];
850        self.process_authentic_fmp_plaintext(AuthenticatedFmpPlaintext::new(
851            fallback.source_peer,
852            fallback.transport_id,
853            &fallback.remote_addr,
854            fallback.timestamp_ms,
855            fallback.packet_len,
856            fallback.fmp_counter,
857            fallback.fmp_flags,
858            plaintext,
859        ))
860        .await;
861    }
862
863    async fn process_decrypt_failure_report(&mut self, report: DecryptFailureReport) {
864        debug!(
865            peer = %self.peer_display_name(report.source_peer.node_addr()),
866            counter = report.fmp_counter,
867            replay_highest = report.fmp_replay_highest,
868            "Worker FMP AEAD decryption failed"
869        );
870        self.handle_decrypt_failure_report(&report).await;
871    }
872
873    /// Drain only the priority decrypt-worker fallback lane.
874    ///
875    /// This is the top-level reserved-progress arm: priority plaintext and
876    /// decrypt failures get first service, but bulk fallback stays behind
877    /// `packet_rx` unless it is explicitly interleaved inside a packet drain
878    /// or selected by its own lower-priority branch.
879    async fn drain_decrypt_priority_fallback(
880        &mut self,
881        priority_rx: &mut Receiver<DecryptWorkerEvent>,
882        first_event: Option<DecryptWorkerEvent>,
883        budget: usize,
884    ) -> usize {
885        self.begin_endpoint_event_batch();
886        let mut drain = SingleLaneDrainCursor::new(first_event, budget);
887        while let Some(event) = drain.next(priority_rx) {
888            self.process_decrypt_worker_event(event).await;
889        }
890        let drained = drain.drained();
891        self.finish_endpoint_event_batch();
892        drained
893    }
894
895    /// Drain up to `budget` queued fallbacks without yielding back to
896    /// `select!`. Returns the number processed. Called both from the
897    /// bulk-fallback select arm (after the selected head item) and interleaved
898    /// inside the packet_rx drain loop so bounced FMP plaintexts can't
899    /// accumulate behind a hot inbound packet turn.
900    async fn drain_decrypt_fallback(
901        &mut self,
902        rx: &mut DecryptWorkerFallbackReceivers,
903        first_priority_event: Option<DecryptWorkerEvent>,
904        first_authenticated_bulk_event: Option<DecryptWorkerEvent>,
905        first_bulk_event: Option<DecryptWorkerEvent>,
906        budget: usize,
907    ) -> usize {
908        self.begin_endpoint_event_batch();
909        let mut drain = DecryptReturnDrainCursor::new(
910            first_priority_event,
911            first_authenticated_bulk_event,
912            first_bulk_event,
913            budget,
914        );
915        while let Some(event) =
916            drain.next(&mut rx.priority, &mut rx.authenticated_bulk, &mut rx.bulk)
917        {
918            rx.release_dequeued_event(&event);
919            let extra = event.packet_count().saturating_sub(1);
920            self.process_decrypt_worker_event(event).await;
921            drain.charge_extra(extra);
922        }
923        let drained = drain.drained();
924        self.finish_endpoint_event_batch();
925        drained
926    }
927
928    /// Process a single received packet.
929    ///
930    /// Dispatches based on the phase field in the 4-byte common prefix.
931    #[cfg(test)]
932    pub(in crate::node) async fn process_packet(&mut self, packet: ReceivedPacket) {
933        let action = self.begin_process_packet(packet);
934        self.finish_packet_process(action).await;
935    }
936
937    fn begin_process_packet(&mut self, packet: ReceivedPacket) -> PacketProcessAction {
938        let timer = crate::perf_profile::Timer::start(crate::perf_profile::Stage::ProcessPacket);
939        let priority_sized = packet.is_priority_sized();
940        let priority_count = u64::from(priority_sized);
941        let bulk_count = u64::from(!priority_sized);
942        crate::perf_profile::record_since_split_count(
943            crate::perf_profile::Stage::TransportQueueWait,
944            crate::perf_profile::Stage::TransportPriorityQueueWait,
945            crate::perf_profile::Stage::TransportBulkQueueWait,
946            packet.trace_enqueued_at,
947            1,
948            priority_count,
949            bulk_count,
950        );
951        crate::perf_profile::record_since_split_count(
952            crate::perf_profile::Stage::TransportRxLoopWait,
953            crate::perf_profile::Stage::TransportPriorityRxLoopWait,
954            crate::perf_profile::Stage::TransportBulkRxLoopWait,
955            packet.trace_rx_loop_owned_at,
956            1,
957            priority_count,
958            bulk_count,
959        );
960        if is_punch_packet(&packet.data) {
961            trace!(
962                transport_id = %packet.transport_id,
963                remote_addr = %packet.remote_addr,
964                bytes = packet.data.len(),
965                "Dropping stray punch probe/ack in FMP rx loop"
966            );
967            return PacketProcessAction::Done;
968        }
969        if packet.data.len() < COMMON_PREFIX_SIZE {
970            return PacketProcessAction::Done; // Drop packets too short for common prefix
971        }
972
973        let prefix = match CommonPrefix::parse(&packet.data) {
974            Some(p) => p,
975            None => return PacketProcessAction::Done, // Malformed prefix
976        };
977        if matches!(prefix.phase, PHASE_MSG1 | PHASE_MSG2) {
978            debug!(
979                transport_id = %packet.transport_id,
980                remote_addr = %packet.remote_addr,
981                bytes = packet.data.len(),
982                phase = prefix.phase,
983                version = prefix.version,
984                "FMP handshake packet dispatch"
985            );
986        } else {
987            trace!(
988                transport_id = %packet.transport_id,
989                remote_addr = %packet.remote_addr,
990                bytes = packet.data.len(),
991                phase = prefix.phase,
992                version = prefix.version,
993                "FMP packet dispatch"
994            );
995        }
996
997        if prefix.version != FMP_VERSION {
998            debug!(
999                version = prefix.version,
1000                transport_id = %packet.transport_id,
1001                "Unknown FMP version, dropping"
1002            );
1003
1004            // If the packet arrived on an adopted Nostr-NAT bootstrap
1005            // transport, the originating peer is necessarily on a
1006            // different FMP-protocol version than us — the discovery
1007            // sweep would otherwise re-traverse them every cycle even
1008            // though no msg1/msg2 exchange can ever succeed. Bump the
1009            // discovery-layer cooldown to the long protocol-mismatch
1010            // window and emit a single WARN per fresh observation.
1011            let looks_like_fmp_phase =
1012                matches!(prefix.phase, PHASE_ESTABLISHED | PHASE_MSG1 | PHASE_MSG2);
1013            if looks_like_fmp_phase
1014                && self.bootstrap_transports.contains(&packet.transport_id)
1015                && let Some(npub) = self.bootstrap_transports.peer_npub(&packet.transport_id)
1016                && let Some(handle) = self.nostr_discovery_handle()
1017            {
1018                let now_ms = Self::now_ms();
1019                let cooldown_secs = handle.protocol_mismatch_cooldown_secs();
1020                if handle.record_protocol_mismatch(npub, now_ms) {
1021                    warn!(
1022                        peer_npub = %npub,
1023                        transport_id = %packet.transport_id,
1024                        peer_version = prefix.version,
1025                        our_version = FMP_VERSION,
1026                        cooldown_secs,
1027                        "Nostr-discovered peer speaks a different FMP version; suppressing retraversal"
1028                    );
1029                }
1030            }
1031            return PacketProcessAction::Done;
1032        }
1033
1034        match prefix.phase {
1035            PHASE_ESTABLISHED => match self.try_prepare_encrypted_frame_for_worker(packet) {
1036                EncryptedFrameFastPath::Dispatch(job) => PacketProcessAction::DecryptJob { job },
1037                EncryptedFrameFastPath::Dropped => PacketProcessAction::Done,
1038                EncryptedFrameFastPath::Slow(packet) => {
1039                    PacketProcessAction::EncryptedSlow { packet, timer }
1040                }
1041            },
1042            PHASE_MSG1 => PacketProcessAction::Msg1 { packet, timer },
1043            PHASE_MSG2 => PacketProcessAction::Msg2 { packet, timer },
1044            _ => {
1045                debug!(
1046                    phase = prefix.phase,
1047                    transport_id = %packet.transport_id,
1048                    "Unknown FMP phase, dropping"
1049                );
1050                PacketProcessAction::Done
1051            }
1052        }
1053    }
1054
1055    async fn finish_packet_process(&mut self, action: PacketProcessAction) {
1056        match action {
1057            PacketProcessAction::Done => {}
1058            PacketProcessAction::DecryptJob { job } => {
1059                if let Some(workers) = self.decrypt_workers.as_ref() {
1060                    workers.dispatch_job(job);
1061                }
1062            }
1063            PacketProcessAction::EncryptedSlow {
1064                packet,
1065                timer: _timer,
1066            } => {
1067                self.handle_encrypted_frame_slow(packet).await;
1068            }
1069            PacketProcessAction::Msg1 {
1070                packet,
1071                timer: _timer,
1072            } => {
1073                self.handle_msg1(packet).await;
1074            }
1075            PacketProcessAction::Msg2 {
1076                packet,
1077                timer: _timer,
1078            } => {
1079                self.handle_msg2(packet).await;
1080            }
1081        }
1082    }
1083
1084    fn flush_decrypt_job_batcher(&self, batcher: &mut DecryptJobBatcher) {
1085        if let Some(workers) = self.decrypt_workers.as_ref() {
1086            batcher.flush(workers);
1087        }
1088    }
1089}