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