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