Skip to main content

fips_core/node/handlers/
rx_loop.rs

1//! RX event loop and dataplane dispatch.
2
3use crate::control::queries;
4use crate::control::{ControlMessage, ControlSenders, ControlSocket, commands};
5use crate::dataplane::DataplaneFastIngressRx;
6use crate::node::{
7    EndpointDataBatchRx, EndpointEventSender, Node, NodeError, endpoint_data_batch_channel,
8};
9use crate::transport::PacketRx;
10use crate::upper::tun::TunOutboundRx;
11use std::time::{Duration, Instant};
12use tokio::sync::mpsc::Receiver;
13use tracing::{debug, info, warn};
14
15mod budget;
16mod dataplane;
17mod drain;
18
19#[cfg(test)]
20mod tests;
21
22use budget::*;
23use drain::*;
24
25pub(in crate::node) struct RxLoopDataplaneIo<'a> {
26    packet_rx: &'a mut PacketRx,
27    dataplane_fast_ingress_rx: &'a mut DataplaneFastIngressRx,
28    endpoint_data_rx: &'a mut EndpointDataBatchRx,
29    tun_outbound_rx: &'a mut TunOutboundRx,
30    endpoint_tx: &'a EndpointEventSender,
31}
32
33struct RxLoopDataplaneRuntime {
34    packet_rx: PacketRx,
35    dataplane_fast_ingress_rx: DataplaneFastIngressRx,
36    endpoint_data_rx: EndpointDataBatchRx,
37    tun_outbound_rx: TunOutboundRx,
38    endpoint_tx: EndpointEventSender,
39}
40
41impl RxLoopDataplaneRuntime {
42    fn io(&mut self) -> RxLoopDataplaneIo<'_> {
43        RxLoopDataplaneIo {
44            packet_rx: &mut self.packet_rx,
45            dataplane_fast_ingress_rx: &mut self.dataplane_fast_ingress_rx,
46            endpoint_data_rx: &mut self.endpoint_data_rx,
47            tun_outbound_rx: &mut self.tun_outbound_rx,
48            endpoint_tx: &self.endpoint_tx,
49        }
50    }
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub(in crate::node) struct RxLoopDataplaneTurnLimits {
55    packet: usize,
56    endpoint: usize,
57    tun: usize,
58    crypto: usize,
59}
60
61impl RxLoopDataplaneTurnLimits {
62    pub(in crate::node) fn new(packet: usize, endpoint: usize, tun: usize, crypto: usize) -> Self {
63        Self {
64            packet,
65            endpoint,
66            tun,
67            crypto,
68        }
69    }
70}
71
72#[cfg(test)]
73pub(in crate::node) fn rx_loop_dataplane_io<'a>(
74    packet_rx: &'a mut PacketRx,
75    dataplane_fast_ingress_rx: &'a mut DataplaneFastIngressRx,
76    endpoint_data_rx: &'a mut EndpointDataBatchRx,
77    tun_outbound_rx: &'a mut TunOutboundRx,
78    endpoint_tx: &'a EndpointEventSender,
79) -> RxLoopDataplaneIo<'a> {
80    RxLoopDataplaneIo {
81        packet_rx,
82        dataplane_fast_ingress_rx,
83        endpoint_data_rx,
84        tun_outbound_rx,
85        endpoint_tx,
86    }
87}
88
89impl Node {
90    /// Run the receive event loop.
91    ///
92    /// Processes packets from all transports, dispatching based on
93    /// the phase field in the 4-byte common prefix:
94    /// - Phase 0x0: Encrypted frame (session data)
95    /// - Phase 0x1: Handshake message 1 (initiator -> responder)
96    /// - Phase 0x2: Handshake message 2 (responder -> initiator)
97    ///
98    /// Also processes outbound IPv6 packets from the TUN reader for session
99    /// encapsulation and routing through the mesh.
100    ///
101    /// Also processes DNS-resolved identities for identity cache population.
102    ///
103    /// Also runs a periodic tick (1s) to clean up stale handshake connections
104    /// that never received a response. This prevents resource leaks when peers
105    /// are unreachable.
106    ///
107    /// This method takes ownership of the packet_rx channel and runs
108    /// until the channel is closed (typically when stop() is called).
109    pub async fn run_rx_loop(&mut self) -> Result<(), NodeError> {
110        let packet_rx = self.packet_rx.take().ok_or(NodeError::NotStarted)?;
111
112        // Take the TUN outbound receiver, or create a dummy channel that never
113        // produces messages (when TUN is disabled). Holding the sender prevents
114        // the channel from closing.
115        let (tun_outbound_rx, _tun_guard) = match self.tun_outbound_rx.take() {
116            Some(rx) => (rx, None),
117            None => {
118                let (tx, rx) = crate::upper::tun::tun_outbound_channel(1);
119                (rx, Some(tx))
120            }
121        };
122
123        // Take the DNS identity receiver, or create a dummy channel (when DNS
124        // is disabled). Same pattern as TUN outbound.
125        let (mut dns_identity_rx, _dns_guard) = match self.dns_identity_rx.take() {
126            Some(rx) => (rx, None),
127            None => {
128                let (tx, rx) = tokio::sync::mpsc::channel(1);
129                (rx, Some(tx))
130            }
131        };
132
133        // Take the endpoint control receiver, or create a dummy channel
134        // when the embedded endpoint API is not in use.
135        let (mut endpoint_control_rx, _endpoint_control_guard) =
136            match self.endpoint_control_rx.take() {
137                Some(rx) => (rx, None),
138                None => {
139                    let (tx, rx) = tokio::sync::mpsc::channel(1);
140                    (rx, Some(tx))
141                }
142            };
143        let (endpoint_data_rx, _endpoint_data_guard) = match self.endpoint_data_rx.take() {
144            Some(rx) => (rx, None),
145            None => {
146                let (tx, rx) = endpoint_data_batch_channel(1);
147                (rx, Some(tx))
148            }
149        };
150        let (dataplane_fast_ingress_rx, _dataplane_fast_ingress_guard) =
151            match self.dataplane_fast_ingress_rx.take() {
152                Some(rx) => (rx, None),
153                None => {
154                    let (tx, rx) = tokio::sync::mpsc::channel(1);
155                    (rx, Some(tx))
156                }
157            };
158
159        let mut tick =
160            tokio::time::interval(Duration::from_secs(self.config.node.tick_interval_secs));
161        tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
162        let mut maintenance_state = RxLoopMaintenanceState::default();
163
164        // Set up control socket channels. Read-only queries are separated
165        // from mutating commands so operator status reads can get reserved
166        // progress while a command awaits slower discovery/transport work.
167        let (control_query_tx, mut control_query_rx) =
168            tokio::sync::mpsc::channel::<ControlMessage>(32);
169        let (control_command_tx, mut control_command_rx) =
170            tokio::sync::mpsc::channel::<ControlMessage>(32);
171
172        if self.config.node.control.enabled {
173            let config = self.config.node.control.clone();
174            let senders = ControlSenders::new(control_query_tx.clone(), control_command_tx.clone());
175            tokio::spawn(async move {
176                match ControlSocket::bind(&config) {
177                    Ok(socket) => {
178                        socket.accept_loop_split(senders).await;
179                    }
180                    Err(e) => {
181                        warn!(error = %e, "Failed to bind control socket");
182                    }
183                }
184            });
185        }
186        // Drop unused sender to avoid keeping channel open if control is disabled
187        drop(control_query_tx);
188        drop(control_command_tx);
189
190        let dataplane_endpoint_tx = self.endpoint_events.sender().unwrap_or_else(|| {
191            let (tx, rx) = EndpointEventSender::channel(1);
192            drop(rx);
193            tx
194        });
195        let dataplane_completion_notify = self.dataplane.completion_notify();
196        let mut dataplane_runtime = RxLoopDataplaneRuntime {
197            packet_rx,
198            dataplane_fast_ingress_rx,
199            endpoint_data_rx,
200            tun_outbound_rx,
201            endpoint_tx: dataplane_endpoint_tx,
202        };
203
204        info!("RX event loop started");
205        // Optional perf profiler (FIPS_PERF=1). No-op otherwise.
206        crate::perf_profile::maybe_spawn_reporter();
207        // Tokio intervals tick immediately on first poll. Consume that startup
208        // tick so the reserved-progress branch below represents a due periodic
209        // maintenance turn, not an eager pre-data maintenance pass.
210        tick.tick().await;
211
212        loop {
213            tokio::select! {
214                biased;
215                // Timer-driven liveness is a reserved-progress branch. It
216                // performs bounded pre/post data drains and timeboxes slow
217                // discovery/status work, so hot packet or endpoint/TUN queues
218                // cannot indefinitely postpone heartbeat, rekey, MMP, route
219                // aging, or path maintenance.
220                _ = tick.tick() => {
221                    let drained = {
222                        let mut dataplane_io = dataplane_runtime.io();
223                        self.drain_rx_loop_data_queues(
224                            &mut dataplane_io,
225                            ENDPOINT_DRAIN_BUDGET,
226                        ).await
227                    };
228                    if drained.has_drained() {
229                        maintenance_state.record_data_activity(Instant::now());
230                        debug!(
231                            drained = drained.total(),
232                            drained_packets = drained.packets,
233                            drained_tun = drained.tun,
234                            drained_endpoint = drained.endpoint,
235                            "Drained queued packets before rx-loop maintenance"
236                        );
237                    }
238                    let maintenance_plan = maintenance_state.plan_maintenance(
239                        drained,
240                        Instant::now(),
241                        RX_LOOP_RECENT_DATA_ACTIVITY_WINDOW,
242                        RX_LOOP_SLOW_MAINTENANCE_IDLE_TIMEOUT,
243                        RX_LOOP_SLOW_MAINTENANCE_BUSY_TIMEOUT,
244                    );
245
246                    let slow_timed_out = self.run_rx_loop_maintenance_tick(
247                        maintenance_plan,
248                    ).await;
249                    maintenance_state.record_maintenance_result(
250                        maintenance_plan.data_pressure(),
251                        slow_timed_out,
252                    );
253
254                    let post_drained = {
255                        let mut dataplane_io = dataplane_runtime.io();
256                        self.drain_rx_loop_data_queues(
257                            &mut dataplane_io,
258                            PACKET_DRAIN_BUDGET,
259                        ).await
260                    };
261                    if post_drained.has_drained() {
262                        maintenance_state.record_data_activity(Instant::now());
263                        debug!(
264                            drained = post_drained.total(),
265                            drained_packets = post_drained.packets,
266                            drained_tun = post_drained.tun,
267                            drained_endpoint = post_drained.endpoint,
268                            "Drained queued packets after rx-loop maintenance"
269                        );
270                    }
271                }
272                Some(message) = control_query_rx.recv() => {
273                    self.drain_control_queries(
274                        &mut control_query_rx,
275                        Some(message),
276                        ENDPOINT_DRAIN_BUDGET,
277                    ).await;
278                }
279                // Endpoint control carries management/lifecycle commands.
280                // Endpoint payload batches stay on the data lane; this branch
281                // keeps control work from waiting behind hot raw receive.
282                // Endpoint data batches intentionally remain below packet_rx.
283                Some(command) = endpoint_control_rx.recv() => {
284                    self.handle_endpoint_control(command).await;
285                }
286                packet = dataplane_runtime.packet_rx.recv() => {
287                    match packet {
288                        Some(p) => {
289                            let latency_packet = p.is_transport_priority();
290                            let mut firsts = crate::dataplane::DataplaneLiveTurnFirsts {
291                                raw_packet: Some(p),
292                                ..Default::default()
293                            };
294                            if let Ok(packet) = dataplane_runtime.tun_outbound_rx.try_recv() {
295                                firsts.tun_packet = Some(packet);
296                            }
297                            let latency_work_ready = latency_packet
298                                || dataplane_runtime.packet_rx.priority_ready_packets() > 0;
299                            if latency_work_ready {
300                                let packet_budget = packet_drain_budget(true);
301                                let endpoint_budget = endpoint_drain_budget(packet_budget);
302                                let tun_budget = tun_drain_budget(packet_budget);
303                                let crypto_budget = mixed_dataplane_crypto_budget(
304                                    packet_budget,
305                                    endpoint_budget,
306                                    tun_budget,
307                                );
308                                let mut turn = {
309                                    let mut dataplane_io = dataplane_runtime.io();
310                                    self.drain_dataplane_turn_with_firsts(
311                                        &mut dataplane_io,
312                                        firsts,
313                                        RxLoopDataplaneTurnLimits::new(
314                                            packet_budget,
315                                            endpoint_budget,
316                                            tun_budget,
317                                            crypto_budget,
318                                        ),
319                                    ).await
320                                };
321                                self.finish_dataplane_turn(
322                                    &mut turn,
323                                    &mut maintenance_state,
324                                    &mut control_query_rx,
325                                    CONTROL_QUERY_INTERLEAVE_BUDGET,
326                                ).await;
327                            } else {
328                                firsts.raw_ingress_prefetch = true;
329                                let mut dataplane_io = dataplane_runtime.io();
330                                self.service_dataplane_bulk_turns(
331                                    &mut dataplane_io,
332                                    firsts,
333                                    &mut maintenance_state,
334                                    &mut control_query_rx,
335                                ).await;
336                            }
337                        }
338                        None => break, // channel closed
339                    }
340                }
341                Some(fast_ingress) = dataplane_runtime.dataplane_fast_ingress_rx.recv() => {
342                    let mut dataplane_io = dataplane_runtime.io();
343                    self.service_dataplane_bulk_turns(
344                        &mut dataplane_io,
345                        crate::dataplane::DataplaneLiveTurnFirsts {
346                            fast_ingress: Some(fast_ingress),
347                            ..Default::default()
348                        },
349                        &mut maintenance_state,
350                        &mut control_query_rx,
351                    ).await;
352                }
353                _ = dataplane_completion_notify.notified() => {
354                    let mut dataplane_io = dataplane_runtime.io();
355                    self.service_dataplane_completion_turns(
356                        &mut dataplane_io,
357                        &mut maintenance_state,
358                        &mut control_query_rx,
359                    ).await;
360                }
361                Some(ipv6_packet) = dataplane_runtime.tun_outbound_rx.recv() => {
362                    let tun_budget = tun_drain_budget(LATENCY_PACKET_DRAIN_BUDGET);
363                    let mut turn = {
364                        let mut dataplane_io = dataplane_runtime.io();
365                        self.drain_dataplane_turn_with_firsts(
366                            &mut dataplane_io,
367                            crate::dataplane::DataplaneLiveTurnFirsts {
368                                tun_packet: Some(ipv6_packet),
369                                ..Default::default()
370                            },
371                            RxLoopDataplaneTurnLimits::new(0, 0, tun_budget, tun_budget),
372                        ).await
373                    };
374                    self.finish_dataplane_turn(
375                        &mut turn,
376                        &mut maintenance_state,
377                        &mut control_query_rx,
378                        0,
379                    ).await;
380                }
381                Some(identity) = dns_identity_rx.recv() => {
382                    debug!(
383                        node_addr = %identity.node_addr,
384                        "Registering identity from DNS resolution"
385                    );
386                    self.register_identity(identity.node_addr, identity.pubkey);
387                }
388                Some(batch) = dataplane_runtime.endpoint_data_rx.recv() => {
389                    let mut turn = {
390                        let mut dataplane_io = dataplane_runtime.io();
391                        self.drain_dataplane_turn_with_firsts(
392                            &mut dataplane_io,
393                            crate::dataplane::DataplaneLiveTurnFirsts {
394                                endpoint_data_batch: Some(batch),
395                                ..Default::default()
396                            },
397                            RxLoopDataplaneTurnLimits::new(
398                                0,
399                                ENDPOINT_DRAIN_BUDGET,
400                                0,
401                                PACKET_DRAIN_BUDGET,
402                            ),
403                        ).await
404                    };
405                    self.finish_dataplane_turn(
406                        &mut turn,
407                        &mut maintenance_state,
408                        &mut control_query_rx,
409                        0,
410                    ).await;
411                }
412                Some((request, response_tx)) = control_command_rx.recv() => {
413                    let response = commands::dispatch(
414                        self,
415                        &request.command,
416                        request.params.as_ref(),
417                    ).await;
418                    let _ = response_tx.send(response);
419                }
420            }
421        }
422
423        info!("RX event loop stopped (channel closed)");
424        Ok(())
425    }
426
427    async fn drain_rx_loop_data_queues(
428        &mut self,
429        io: &mut RxLoopDataplaneIo<'_>,
430        budget: usize,
431    ) -> RxLoopDataDrainStats {
432        let fast_ingress =
433            Self::take_dataplane_fast_ingress_batch(io.dataplane_fast_ingress_rx, budget);
434        let packet_budget = budget.max(
435            fast_ingress
436                .as_ref()
437                .map_or(0, |fast_ingress| fast_ingress.len()),
438        );
439        let endpoint_budget = endpoint_drain_budget(packet_budget);
440        let tun_budget = tun_drain_budget(packet_budget);
441        let crypto_budget =
442            mixed_dataplane_crypto_budget(packet_budget, endpoint_budget, tun_budget);
443        let mut turn = self
444            .drain_dataplane_turn_with_firsts(
445                io,
446                crate::dataplane::DataplaneLiveTurnFirsts {
447                    fast_ingress,
448                    ..Default::default()
449                },
450                RxLoopDataplaneTurnLimits::new(
451                    packet_budget,
452                    endpoint_budget,
453                    tun_budget,
454                    crypto_budget,
455                ),
456            )
457            .await;
458        let drained_packets = Self::dataplane_packet_activity(&turn);
459        let control_drained = self.process_dataplane_control_ingress(&mut turn).await;
460        RxLoopDataDrainStats::new(
461            drained_packets,
462            turn.tun_source_drained(),
463            turn.endpoint_source_drained(),
464            control_drained,
465        )
466    }
467
468    fn take_dataplane_fast_ingress_batch(
469        dataplane_fast_ingress_rx: &mut crate::dataplane::DataplaneFastIngressRx,
470        limit: usize,
471    ) -> Option<crate::dataplane::DataplaneFastIngressBatch> {
472        let fast_ingress = dataplane_fast_ingress_rx.try_recv().ok()?;
473        Some(Self::coalesce_dataplane_fast_ingress(
474            fast_ingress,
475            dataplane_fast_ingress_rx,
476            limit,
477        ))
478    }
479
480    fn coalesce_dataplane_fast_ingress(
481        mut fast_ingress: crate::dataplane::DataplaneFastIngressBatch,
482        dataplane_fast_ingress_rx: &mut crate::dataplane::DataplaneFastIngressRx,
483        limit: usize,
484    ) -> crate::dataplane::DataplaneFastIngressBatch {
485        while fast_ingress.len() < limit {
486            let Ok(next) = dataplane_fast_ingress_rx.try_recv() else {
487                break;
488            };
489            fast_ingress.absorb(next);
490        }
491        fast_ingress
492    }
493
494    async fn service_dataplane_bulk_turns(
495        &mut self,
496        io: &mut RxLoopDataplaneIo<'_>,
497        firsts: crate::dataplane::DataplaneLiveTurnFirsts,
498        maintenance_state: &mut RxLoopMaintenanceState,
499        control_query_rx: &mut Receiver<ControlMessage>,
500    ) {
501        let started = Instant::now();
502        let mut firsts = Some(firsts);
503        let mut turns = 0usize;
504
505        loop {
506            if turns > 0
507                && (turns >= RX_LOOP_BULK_SERVICE_MAX_TURNS
508                    || started.elapsed() >= RX_LOOP_BULK_SERVICE_MAX_ELAPSED
509                    || io.packet_rx.priority_ready_packets() > 0)
510            {
511                break;
512            }
513
514            let packet_budget = PACKET_DRAIN_BUDGET;
515            let mut turn_firsts = firsts.take().unwrap_or_default();
516            turn_firsts.raw_ingress_prefetch = true;
517            turn_firsts.fast_ingress = match turn_firsts.fast_ingress.take() {
518                Some(fast_ingress) => Some(Self::coalesce_dataplane_fast_ingress(
519                    fast_ingress,
520                    io.dataplane_fast_ingress_rx,
521                    packet_budget,
522                )),
523                None => Self::take_dataplane_fast_ingress_batch(
524                    io.dataplane_fast_ingress_rx,
525                    packet_budget,
526                ),
527            };
528            let packet_budget = packet_budget.max(
529                turn_firsts
530                    .fast_ingress
531                    .as_ref()
532                    .map_or(0, |fast_ingress| fast_ingress.len()),
533            );
534            let endpoint_budget = endpoint_drain_budget(packet_budget);
535            let tun_budget = tun_drain_budget(packet_budget);
536            let crypto_budget =
537                mixed_dataplane_crypto_budget(packet_budget, endpoint_budget, tun_budget);
538
539            let mut turn = self
540                .drain_dataplane_turn_with_firsts(
541                    io,
542                    turn_firsts,
543                    RxLoopDataplaneTurnLimits::new(
544                        packet_budget,
545                        endpoint_budget,
546                        tun_budget,
547                        crypto_budget,
548                    ),
549                )
550                .await;
551            let raw_drained = Self::dataplane_raw_ingress_activity(&turn);
552            let control_activity = Self::dataplane_control_activity(&turn);
553            let completions_drained = turn.summary().completions();
554            let admission_dropped =
555                turn.summary().inbound_dropped() > 0 || turn.summary().outbound_dropped() > 0;
556            let keep_servicing = !admission_dropped
557                && (raw_drained >= packet_budget
558                    || completions_drained >= crypto_budget
559                    || turn.tun_source_drained() >= tun_budget
560                    || turn.endpoint_source_drained() >= endpoint_budget);
561            let control_drained = self
562                .finish_dataplane_turn(
563                    &mut turn,
564                    maintenance_state,
565                    control_query_rx,
566                    CONTROL_QUERY_INTERLEAVE_BUDGET,
567                )
568                .await;
569            turns += 1;
570            let runnable_work = self.dataplane.has_runnable_work();
571
572            if control_drained > 0
573                || (!keep_servicing && !runnable_work)
574                || (control_activity > 0 && !runnable_work)
575            {
576                break;
577            }
578        }
579    }
580
581    async fn service_dataplane_completion_turns(
582        &mut self,
583        io: &mut RxLoopDataplaneIo<'_>,
584        maintenance_state: &mut RxLoopMaintenanceState,
585        control_query_rx: &mut Receiver<ControlMessage>,
586    ) {
587        let started = Instant::now();
588        let mut turns = 0usize;
589
590        loop {
591            if turns > 0
592                && (turns >= RX_LOOP_BULK_SERVICE_MAX_TURNS
593                    || started.elapsed() >= RX_LOOP_BULK_SERVICE_MAX_ELAPSED
594                    || io.packet_rx.priority_ready_packets() > 0)
595            {
596                break;
597            }
598
599            let mut turn = self
600                .drain_dataplane_completion_turn(io, LATENCY_PACKET_DRAIN_BUDGET)
601                .await;
602            let control_drained = self
603                .finish_dataplane_turn(&mut turn, maintenance_state, control_query_rx, 0)
604                .await;
605            turns += 1;
606
607            let runnable_work = self.dataplane.has_runnable_work();
608            if control_drained > 0 || !runnable_work {
609                break;
610            }
611        }
612    }
613
614    async fn finish_dataplane_turn(
615        &mut self,
616        turn: &mut crate::dataplane::DataplaneLiveNodeTurn,
617        maintenance_state: &mut RxLoopMaintenanceState,
618        control_query_rx: &mut Receiver<ControlMessage>,
619        control_query_budget: usize,
620    ) -> usize {
621        let had_activity = turn.has_activity();
622        let control_drained = self
623            .process_dataplane_control_ingress(turn)
624            .await
625            .saturating_add(self.drain_deferred_dataplane_control_turns().await);
626        if control_drained > 0 && self.dataplane.has_deferred_raw_ingress() {
627            self.dataplane.completion_notify().notify_one();
628        }
629        let query_drained = if control_query_budget > 0 {
630            self.drain_control_queries(control_query_rx, None, control_query_budget)
631                .await
632        } else {
633            0
634        };
635        if had_activity || control_drained > 0 {
636            maintenance_state.record_data_activity(Instant::now());
637        }
638        control_drained.saturating_add(query_drained)
639    }
640
641    async fn drain_control_queries(
642        &mut self,
643        control_query_rx: &mut Receiver<ControlMessage>,
644        first_message: Option<ControlMessage>,
645        budget: usize,
646    ) -> usize {
647        let mut drain = SingleLaneDrainCursor::new(first_message, budget);
648        while let Some((request, response_tx)) = drain.next(control_query_rx) {
649            let response = queries::dispatch(self, &request.command, request.params.as_ref());
650            let _ = response_tx.send(response);
651        }
652
653        drain.drained()
654    }
655
656    async fn run_rx_loop_maintenance_tick(&mut self, plan: RxLoopMaintenancePlan) -> bool {
657        self.check_timeouts();
658        let now_ms = Self::now_ms();
659        // Link/session liveness must run before slower retry/discovery work:
660        // under bulk send pressure a late heartbeat or MMP report is
661        // indistinguishable from a dead direct path on the remote peer.
662        self.check_link_heartbeats().await;
663        self.reload_peer_acl();
664        self.resend_pending_handshakes(now_ms).await;
665        self.resend_pending_rekeys(now_ms).await;
666        self.resend_pending_session_handshakes(now_ms).await;
667        self.resend_pending_session_msg3(now_ms).await;
668        self.purge_idle_sessions(now_ms);
669        self.purge_learned_routes(now_ms);
670        self.check_mmp_reports().await;
671        self.check_session_mmp_reports().await;
672        self.check_rekey().await;
673        self.check_session_rekey().await;
674        self.check_pending_lookups(now_ms).await;
675        self.poll_pending_connects().await;
676        self.process_pending_retries(now_ms).await;
677        self.poll_transport_discovery().await;
678        self.sample_transport_congestion();
679
680        let Some(slow_timeout) = plan.slow_timeout() else {
681            crate::perf_profile::record_event(
682                crate::perf_profile::Event::RxLoopSlowMaintenanceSkipped,
683            );
684            return false;
685        };
686
687        if tokio::time::timeout(slow_timeout, self.run_rx_loop_slow_maintenance_tick())
688            .await
689            .is_err()
690        {
691            crate::perf_profile::record_event(
692                crate::perf_profile::Event::RxLoopSlowMaintenanceTimeout,
693            );
694            self.mark_rx_loop_maintenance_timeout();
695            warn!(
696                timeout_ms = slow_timeout.as_millis() as u64,
697                data_pressure = plan.data_pressure(),
698                "RX loop slow maintenance timed out; continuing packet processing"
699            );
700            return true;
701        }
702        false
703    }
704
705    async fn run_rx_loop_slow_maintenance_tick(&mut self) {
706        if let Some(delay) = rx_loop_slow_maintenance_fault_delay() {
707            tokio::time::sleep(delay).await;
708        }
709
710        // Discovery and graph/stat maintenance can involve relay work or
711        // larger scans. Keep it bounded after direct-path liveness and session
712        // upkeep so a slow Nostr/LAN tick degrades discovery freshness, not
713        // packet flow.
714        self.poll_nostr_discovery().await;
715        self.poll_lan_discovery().await;
716        self.poll_local_instance_discovery().await;
717        self.check_tree_state().await;
718        self.check_bloom_state().await;
719        self.compute_mesh_size();
720        self.record_stats_history();
721    }
722}