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