Skip to main content

rns_core/transport/
inbound_engine.rs

1use super::*;
2
3pub(super) fn extra_link_proof_timeout(interface: Option<&InterfaceInfo>) -> f64 {
4    interface
5        .and_then(|interface| interface.bitrate)
6        .filter(|bitrate| *bitrate > 0)
7        .map_or(0.0, |bitrate| {
8            (constants::MTU as f64 * 8.0) / bitrate as f64
9        })
10}
11
12impl TransportEngine {
13    fn record_invalid_announce(
14        &self,
15        announce: &AnnounceData,
16        interface: InterfaceId,
17        now: f64,
18        actions: &mut Vec<TransportAction>,
19    ) {
20        let identity_hash = crate::hash::truncated_hash(&announce.public_key);
21        if !self.is_blackholed(&identity_hash, now) {
22            actions.push(TransportAction::ProtocolViolation { interface });
23        }
24    }
25
26    /// Return whether an inbound frame passes parsing, hop, and packet filtering.
27    /// This does not mutate deduplication or routing state.
28    pub fn accepts_inbound_frame(&self, frame: InboundFrame<'_>) -> bool {
29        self.prepare_inbound_packet(frame).is_some()
30    }
31
32    /// Process an inbound raw packet from a network interface.
33    ///
34    /// Returns a list of actions for the caller to execute.
35    pub fn handle_inbound(
36        &mut self,
37        frame: InboundFrame<'_>,
38        rng: &mut dyn Rng,
39    ) -> Vec<TransportAction> {
40        self.handle_inbound_with_announce_queue(frame, rng, None)
41    }
42
43    pub fn handle_inbound_with_announce_queue(
44        &mut self,
45        frame: InboundFrame<'_>,
46        rng: &mut dyn Rng,
47        announce_queue: Option<&mut AnnounceVerifyQueue>,
48    ) -> Vec<TransportAction> {
49        let Ok(decoded) = self.decode_inbound(frame.raw.to_vec()) else {
50            return Vec::new();
51        };
52        self.handle_decoded_inbound_with_announce_queue(
53            DecodedInboundFrame {
54                decoded,
55                iface: frame.iface,
56                now: frame.now,
57                rx: frame.rx,
58            },
59            rng,
60            announce_queue,
61        )
62    }
63
64    /// Decode an inbound frame for [`handle_decoded_inbound_with_announce_queue`].
65    ///
66    /// Link traffic that this node only forwards, in a context the packet
67    /// filter admits without consulting deduplication, is not hashed: no
68    /// step that handles it reads the hash. Its hash cannot reject any later
69    /// packet either, since a packet with the same hash has the same context.
70    /// [`DecodedPacket::packet_hash`] reports whether hashing was deferred.
71    ///
72    /// [`handle_decoded_inbound_with_announce_queue`]: Self::handle_decoded_inbound_with_announce_queue
73    /// It takes ownership of the wire bytes, which the decoded packet keeps.
74    pub fn decode_inbound(
75        &self,
76        raw: Vec<u8>,
77    ) -> Result<DecodedPacket, crate::packet::PacketError> {
78        // Only link data qualifies; decode everything else directly.
79        let flags = raw
80            .first()
81            .map(|flags| crate::packet::PacketFlags::unpack(*flags));
82        if !flags.is_some_and(|flags| {
83            flags.packet_type == constants::PACKET_TYPE_DATA
84                && flags.destination_type == constants::DESTINATION_LINK
85        }) {
86            return DecodedPacket::unpack_owned(raw);
87        }
88        let mut packet = RawPacket::unpack_unhashed(raw)?;
89        let hash_deferred = self.forwards_link_packet_unhashed(&packet);
90        if !hash_deferred {
91            packet.hash_in_place();
92        }
93        Ok(DecodedPacket {
94            packet,
95            hash_deferred,
96        })
97    }
98
99    /// Whether every inbound step that handles `packet` leaves its hash unread:
100    /// it is link data for a validated link this node relays, in a context
101    /// that [`packet_filter_at_hops`](Self::packet_filter_at_hops) admits
102    /// before deduplication, and it is neither delivered locally nor routed
103    /// through the path table.
104    fn forwards_link_packet_unhashed(&self, packet: &RawPacket) -> bool {
105        packet.flags.packet_type == constants::PACKET_TYPE_DATA
106            && packet.flags.destination_type == constants::DESTINATION_LINK
107            && matches!(
108                packet.context,
109                constants::CONTEXT_KEEPALIVE
110                    | constants::CONTEXT_RESOURCE_REQ
111                    | constants::CONTEXT_RESOURCE
112                    | constants::CONTEXT_CACHE_REQUEST
113                    | constants::CONTEXT_CHANNEL
114            )
115            && (self.config.transport_enabled || self.config.identity_hash.is_some())
116            && self
117                .link_table
118                .get(&packet.destination_hash)
119                .is_some_and(|entry| entry.validated)
120            && !self
121                .local_destinations
122                .contains_key(&packet.destination_hash)
123            && !self.path_table.contains_key(&packet.destination_hash)
124    }
125
126    /// Recheck current routing/filter state and consume a previously decoded frame.
127    pub fn handle_decoded_inbound_with_announce_queue(
128        &mut self,
129        frame: DecodedInboundFrame,
130        rng: &mut dyn Rng,
131        announce_queue: Option<&mut AnnounceVerifyQueue>,
132    ) -> Vec<TransportAction> {
133        let Some(mut ctx) = self.prepare_decoded_packet(frame) else {
134            return Vec::new();
135        };
136        // Routing state may have changed since decoding. Any packet that is
137        // no longer only relayed is hashed before a step can read the hash.
138        if ctx.hash_deferred && !self.forwards_link_packet_unhashed(&ctx.packet) {
139            ctx.packet.hash_in_place();
140            ctx.hash_deferred = false;
141        }
142        let mut actions = Vec::new();
143
144        self.remember_inbound_packet_hash(&ctx.packet);
145        self.bridge_plain_broadcast(&ctx, &mut actions);
146        self.handle_transport_forwarding(&ctx, &mut actions);
147        self.handle_link_table_routing(&ctx, &mut actions);
148        self.handle_inbound_announce(&ctx, rng, announce_queue, &mut actions);
149
150        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_PROOF {
151            self.process_inbound_proof(&ctx, &mut actions);
152        }
153
154        self.handle_inbound_local_delivery(&ctx, &mut actions);
155        actions
156    }
157
158    pub(super) fn prepare_inbound_packet(
159        &self,
160        frame: InboundFrame<'_>,
161    ) -> Option<InboundPacketCtx> {
162        let decoded = DecodedPacket::unpack(frame.raw).ok()?;
163        self.prepare_decoded_packet(DecodedInboundFrame {
164            decoded,
165            iface: frame.iface,
166            now: frame.now,
167            rx: frame.rx,
168        })
169    }
170
171    pub(super) fn filtered_inbound_hops(
172        &self,
173        packet: &RawPacket,
174        iface: InterfaceId,
175    ) -> Option<(u8, bool)> {
176        let from_local_client = self
177            .interfaces
178            .get(&iface)
179            .is_some_and(|i| i.is_local_client);
180        let mut hops = packet.hops.checked_add(1)?;
181        if from_local_client {
182            hops = hops.saturating_sub(1);
183        }
184        self.packet_filter_at_hops(packet, hops)
185            .then_some((hops, from_local_client))
186    }
187
188    pub fn accepts_decoded_packet(&self, decoded: &DecodedPacket, iface: InterfaceId) -> bool {
189        self.filtered_inbound_hops(&decoded.packet, iface).is_some()
190    }
191
192    pub(super) fn prepare_decoded_packet(
193        &self,
194        frame: DecodedInboundFrame,
195    ) -> Option<InboundPacketCtx> {
196        let hash_deferred = frame.decoded.hash_deferred;
197        let mut packet = frame.decoded.packet;
198        let (hops, from_local_client) = self.filtered_inbound_hops(&packet, frame.iface)?;
199        packet.hops = hops;
200        packet.rssi = frame.rx.rssi;
201        packet.snr = frame.rx.snr;
202        let retain_original_raw = packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE;
203        let original_raw = retain_original_raw.then(|| packet.raw.clone());
204        Some(InboundPacketCtx {
205            packet,
206            hash_deferred,
207            original_raw,
208            iface: frame.iface,
209            now: frame.now,
210            from_local_client,
211        })
212    }
213
214    fn remember_inbound_packet_hash(&mut self, packet: &RawPacket) {
215        let remember_hash = !(self.link_table.contains_key(&packet.destination_hash)
216            || (packet.flags.packet_type == constants::PACKET_TYPE_PROOF
217                && packet.context == constants::CONTEXT_LRPROOF));
218        if remember_hash {
219            self.packet_hashlist.add(packet.packet_hash);
220        }
221    }
222
223    fn bridge_plain_broadcast(&self, ctx: &InboundPacketCtx, actions: &mut Vec<TransportAction>) {
224        if ctx.packet.flags.destination_type != constants::DESTINATION_PLAIN
225            || ctx.packet.flags.transport_type != constants::TRANSPORT_BROADCAST
226            || !self.has_local_clients()
227        {
228            return;
229        }
230
231        if ctx.from_local_client {
232            actions.push(TransportAction::ForwardPlainBroadcast {
233                raw: PacketBytes::from(ctx.packet.raw.clone()),
234                to_local: false,
235                exclude: Some(ctx.iface),
236            });
237        } else {
238            actions.push(TransportAction::ForwardPlainBroadcast {
239                raw: PacketBytes::from(ctx.packet.raw.clone()),
240                to_local: true,
241                exclude: None,
242            });
243        }
244    }
245
246    fn handle_transport_forwarding(
247        &mut self,
248        ctx: &InboundPacketCtx,
249        actions: &mut Vec<TransportAction>,
250    ) {
251        if !(self.config.transport_enabled || self.config.identity_hash.is_some()) {
252            return;
253        }
254        if ctx.packet.transport_id.is_none()
255            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
256        {
257            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
258                log::debug!(
259                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. not transport-addressed header={} iface={}",
260                    ctx.packet.destination_hash[0],
261                    ctx.packet.destination_hash[1],
262                    ctx.packet.destination_hash[2],
263                    ctx.packet.destination_hash[3],
264                    ctx.packet.flags.header_type,
265                    ctx.iface.0
266                );
267            }
268            return;
269        }
270
271        let Some(identity_hash) = self.config.identity_hash else {
272            return;
273        };
274        if ctx.packet.transport_id != Some(identity_hash) {
275            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
276                log::debug!(
277                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. transport mismatch got={:02x?} own={:02x?} iface={}",
278                    ctx.packet.destination_hash[0],
279                    ctx.packet.destination_hash[1],
280                    ctx.packet.destination_hash[2],
281                    ctx.packet.destination_hash[3],
282                    ctx.packet.transport_id.as_ref().map(|id| &id[..4]),
283                    &identity_hash[..4],
284                    ctx.iface.0
285                );
286            }
287            return;
288        }
289
290        let Some(path_entry) = self
291            .path_table
292            .get(&ctx.packet.destination_hash)
293            .and_then(|ps| ps.primary())
294        else {
295            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
296                log::debug!(
297                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. addressed to us but no path iface={}",
298                    ctx.packet.destination_hash[0],
299                    ctx.packet.destination_hash[1],
300                    ctx.packet.destination_hash[2],
301                    ctx.packet.destination_hash[3],
302                    ctx.iface.0
303                );
304            }
305            return;
306        };
307
308        let next_hop = path_entry.next_hop;
309        let remaining_hops = path_entry.hops;
310        let outbound_interface = path_entry.receiving_interface;
311        let outbound_is_local_client = self
312            .interfaces
313            .get(&outbound_interface)
314            .map(|info| info.is_local_client)
315            .unwrap_or(false);
316        let forwarded_remaining_hops = if outbound_is_local_client {
317            0
318        } else {
319            remaining_hops
320        };
321        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
322            log::debug!(
323                "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. remaining_hops={} out_iface={} next_hop={:02x?}",
324                ctx.packet.destination_hash[0],
325                ctx.packet.destination_hash[1],
326                ctx.packet.destination_hash[2],
327                ctx.packet.destination_hash[3],
328                remaining_hops,
329                outbound_interface.0,
330                &next_hop[..4]
331            );
332        }
333        let mut new_raw = forward_transport_packet(
334            &ctx.packet,
335            next_hop,
336            forwarded_remaining_hops,
337            outbound_interface,
338        );
339        if self.config.local_hops_delta != 0
340            && ctx.from_local_client
341            && !outbound_is_local_client
342            && ctx.packet.hops == 0
343            && ctx.packet.flags.destination_type != constants::DESTINATION_PLAIN
344            && ctx.packet.flags.destination_type != constants::DESTINATION_GROUP
345            && new_raw.len() > 1
346        {
347            new_raw[1] = self.config.local_hops_delta;
348        }
349
350        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST {
351            clamp_forwarded_link_request_mtu(
352                &mut new_raw,
353                ctx.packet.data().len() == constants::LINK_ECPUBSIZE + constants::LINK_MTU_SIZE,
354                self.interfaces.get(&ctx.iface).map(|info| info.mtu),
355                self.interfaces
356                    .get(&outbound_interface)
357                    .map(|info| info.mtu),
358            );
359            let extra_proof_timeout =
360                extra_link_proof_timeout(self.interfaces.get(&outbound_interface));
361            let proof_timeout = ctx.now
362                + constants::LINK_ESTABLISHMENT_TIMEOUT_PER_HOP * (remaining_hops.max(1) as f64)
363                + extra_proof_timeout;
364            let (link_id, link_entry) = create_link_entry(
365                &ctx.packet,
366                next_hop,
367                outbound_interface,
368                remaining_hops,
369                ctx.iface,
370                ctx.now,
371                proof_timeout,
372            );
373            self.link_table.insert(link_id, link_entry);
374            actions.push(TransportAction::LinkRequestReceived {
375                link_id,
376                destination_hash: ctx.packet.destination_hash,
377                receiving_interface: ctx.iface,
378            });
379        } else {
380            let (trunc_hash, reverse_entry) =
381                create_reverse_entry(&ctx.packet, outbound_interface, ctx.iface, ctx.now);
382            self.reverse_table.insert(trunc_hash, reverse_entry);
383        }
384
385        actions.push(TransportAction::SendOnInterface {
386            interface: outbound_interface,
387            raw: new_raw.into(),
388        });
389
390        if let Some(entry) = self
391            .path_table
392            .get_mut(&ctx.packet.destination_hash)
393            .and_then(|ps| ps.primary_mut())
394        {
395            entry.timestamp = ctx.now;
396        }
397    }
398
399    fn handle_link_table_routing(
400        &mut self,
401        ctx: &InboundPacketCtx,
402        actions: &mut Vec<TransportAction>,
403    ) {
404        if !self.config.transport_enabled && self.config.identity_hash.is_none() {
405            return;
406        }
407        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
408            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
409            || ctx.packet.context == constants::CONTEXT_LRPROOF
410        {
411            return;
412        }
413
414        // This single keyed lookup is the native link fast path. The engine is
415        // already exclusively borrowed, and the typed entry contains every
416        // routing and hop-rewrite field, so a second denormalized cache would
417        // add invalidation risk without avoiding locks or linear searches.
418        // Keep this authoritative-table path even when profiling suggests a
419        // cache: upstream's corresponding denormalized experiment was removed.
420        let Some(link_entry) = self.link_table.get(&ctx.packet.destination_hash).cloned() else {
421            return;
422        };
423        if !link_entry.validated {
424            return;
425        }
426        let instance_local_link = self.interface_is_local_client(link_entry.next_hop_interface)
427            && self.interface_is_local_client(link_entry.received_interface);
428        let Some((outbound_iface, new_raw)) = super::inbound::route_via_link_table_shared(
429            &ctx.packet,
430            &link_entry,
431            ctx.iface,
432            LocalHopRewrite {
433                local_hops_delta: self.config.local_hops_delta,
434                from_local_client: ctx.from_local_client,
435                skip_local_hops_delta: instance_local_link,
436            },
437        ) else {
438            // A link entry with no route from this receiving interface is an
439            // expected fast-path miss, not an operator-facing warning.
440            return;
441        };
442
443        // A deferred packet's context bypasses deduplication, so its hash
444        // could never mark a later packet as a duplicate.
445        if !ctx.hash_deferred {
446            self.packet_hashlist.add(ctx.packet.packet_hash);
447        }
448        actions.push(TransportAction::SendOnInterface {
449            interface: outbound_iface,
450            raw: new_raw,
451        });
452
453        if let Some(entry) = self.link_table.get_mut(&ctx.packet.destination_hash) {
454            entry.timestamp = ctx.now;
455        }
456    }
457
458    fn handle_inbound_announce(
459        &mut self,
460        ctx: &InboundPacketCtx,
461        rng: &mut dyn Rng,
462        announce_queue: Option<&mut AnnounceVerifyQueue>,
463        actions: &mut Vec<TransportAction>,
464    ) {
465        if ctx.packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE {
466            return;
467        }
468
469        if let Some(queue) = announce_queue {
470            self.try_enqueue_announce(ctx, rng, queue, actions);
471        } else {
472            let original_raw = ctx
473                .original_raw
474                .as_deref()
475                .expect("announce packets retain original raw bytes");
476            self.process_inbound_announce(
477                &ctx.packet,
478                original_raw,
479                ctx.iface,
480                ctx.now,
481                rng,
482                actions,
483            );
484        }
485    }
486
487    fn handle_inbound_local_delivery(
488        &self,
489        ctx: &InboundPacketCtx,
490        actions: &mut Vec<TransportAction>,
491    ) {
492        if (ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
493            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA)
494            // The engine's exclusive borrow makes this a direct, lock-free
495            // destination lookup for data and link requests alike; delivery
496            // reuses the same keyed registry.
497            && self
498                .local_destinations
499                .contains_key(&ctx.packet.destination_hash)
500        {
501            let mut delivery_raw = ctx.packet.raw.clone();
502            // Link responders learn the post-ingress hop metric from the
503            // authenticated LRRTT packet. Preserve that field when crossing
504            // the action boundary without changing ordinary callback bytes.
505            if ctx.packet.context == constants::CONTEXT_LRRTT && delivery_raw.len() >= 2 {
506                delivery_raw[1] = ctx.packet.hops;
507            }
508            actions.push(TransportAction::DeliverLocal {
509                destination_hash: ctx.packet.destination_hash,
510                raw: PacketBytes::from(delivery_raw),
511                packet_hash: ctx.packet.packet_hash,
512                receiving_interface: ctx.iface,
513            });
514        }
515    }
516
517    // =========================================================================
518    // Inbound announce processing
519    // =========================================================================
520
521    fn process_inbound_announce(
522        &mut self,
523        packet: &RawPacket,
524        original_raw: &[u8],
525        iface: InterfaceId,
526        now: f64,
527        rng: &mut dyn Rng,
528        actions: &mut Vec<TransportAction>,
529    ) {
530        if packet.flags.destination_type != constants::DESTINATION_SINGLE {
531            return;
532        }
533
534        let has_ratchet = packet.flags.context_flag == constants::FLAG_SET;
535
536        // Unpack and validate announce
537        let announce = match AnnounceData::unpack(packet.data(), has_ratchet) {
538            Ok(a) => a,
539            Err(_) => {
540                actions.push(TransportAction::ProtocolViolation { interface: iface });
541                return;
542            }
543        };
544
545        if self.should_hold_announce(packet, original_raw, iface, now) {
546            return;
547        }
548
549        let sig_cache_key = Self::announce_sig_cache_key(&packet.destination_hash, &announce);
550
551        let validated = if self.announce_sig_cache.contains(&sig_cache_key) {
552            announce.to_validated_with_cached_signature(&packet.destination_hash)
553        } else {
554            announce.validate(&packet.destination_hash).inspect(|_| {
555                self.announce_sig_cache.insert(sig_cache_key, now);
556            })
557        };
558        let validated = match validated {
559            Ok(v) => v,
560            Err(_) => {
561                self.record_invalid_announce(&announce, iface, now, actions);
562                return;
563            }
564        };
565
566        let received_from = self.announce_received_from(packet, now);
567        let random_blob = match extract_random_blob(packet.data()) {
568            Some(b) => b,
569            None => {
570                actions.push(TransportAction::ProtocolViolation { interface: iface });
571                return;
572            }
573        };
574        let announce_emitted = timebase_from_random_blob(&random_blob);
575
576        self.process_verified_announce(
577            VerifiedAnnounceCtx {
578                packet,
579                original_raw,
580                iface,
581                now,
582                validated,
583                received_from,
584                random_blob,
585                announce_emitted,
586            },
587            rng,
588            actions,
589        );
590    }
591
592    fn announce_raw_for_local_clients(&self, packet: &RawPacket) -> PacketBytes {
593        let Some(identity_hash) = self.config.identity_hash else {
594            return PacketBytes::from(packet.raw.clone());
595        };
596
597        if packet.raw.len() < 2 {
598            return PacketBytes::from(packet.raw.clone());
599        }
600
601        let payload_start = if packet.flags.header_type == constants::HEADER_2 {
602            18usize
603        } else {
604            2usize
605        };
606        if packet.raw.len() < payload_start {
607            return PacketBytes::from(packet.raw.clone());
608        }
609
610        let flags = (constants::HEADER_2 << 6)
611            | (constants::TRANSPORT_TRANSPORT << 4)
612            | (packet.raw[0] & 0x0F);
613        let mut raw = Vec::with_capacity(18 + packet.raw.len() - payload_start);
614        raw.push(flags);
615        raw.push(packet.hops);
616        raw.extend_from_slice(&identity_hash);
617        raw.extend_from_slice(&packet.raw[payload_start..]);
618        PacketBytes::from(raw)
619    }
620
621    /// Signature cache key for an announce: covers the full signed data
622    /// (see [`AnnounceData::signature_cache_key`]) plus the signature.
623    pub(super) fn announce_sig_cache_key(
624        destination_hash: &[u8; 16],
625        announce: &AnnounceData,
626    ) -> [u8; 32] {
627        announce.signature_cache_key(destination_hash)
628    }
629
630    fn announce_received_from(&mut self, packet: &RawPacket, now: f64) -> [u8; 16] {
631        if let Some(transport_id) = packet.transport_id {
632            if self.config.transport_enabled {
633                if let Some(announce_entry) = self.announce_table.get_mut(&packet.destination_hash)
634                {
635                    if packet.hops.checked_sub(1) == Some(announce_entry.hops) {
636                        announce_entry.local_rebroadcasts += 1;
637                        if announce_entry.retries > 0
638                            && announce_entry.local_rebroadcasts
639                                >= constants::LOCAL_REBROADCASTS_MAX
640                        {
641                            self.announce_table.remove(&packet.destination_hash);
642                        }
643                    }
644                    if let Some(announce_entry) = self.announce_table.get(&packet.destination_hash)
645                    {
646                        if packet.hops.checked_sub(1) == Some(announce_entry.hops + 1)
647                            && announce_entry.retries > 0
648                            && now < announce_entry.retransmit_timeout
649                        {
650                            self.announce_table.remove(&packet.destination_hash);
651                        }
652                    }
653                }
654            }
655            transport_id
656        } else {
657            packet.destination_hash
658        }
659    }
660
661    fn should_hold_announce(
662        &mut self,
663        packet: &RawPacket,
664        original_raw: &[u8],
665        iface: InterfaceId,
666        now: f64,
667    ) -> bool {
668        if self.has_path(&packet.destination_hash) {
669            return false;
670        }
671        if self
672            .discovery_path_requests
673            .contains_key(&packet.destination_hash)
674        {
675            return false;
676        }
677        let Some(info) = self.interfaces.get(&iface) else {
678            return false;
679        };
680        if packet.context == constants::CONTEXT_PATH_RESPONSE
681            || !self.ingress_control.should_ingress_limit(
682                iface,
683                &info.ingress_control,
684                info.ia_freq,
685                info.started,
686                now,
687            )
688        {
689            return false;
690        }
691        self.ingress_control.hold_announce(
692            iface,
693            &info.ingress_control,
694            packet.destination_hash,
695            ingress_control::HeldAnnounce {
696                raw: original_raw.to_vec(),
697                hops: packet.hops,
698                receiving_interface: iface,
699                rx: RxMetadata {
700                    rssi: packet.rssi,
701                    snr: packet.snr,
702                },
703                timestamp: now,
704            },
705        );
706        true
707    }
708
709    fn try_enqueue_announce(
710        &mut self,
711        ctx: &InboundPacketCtx,
712        rng: &mut dyn Rng,
713        announce_queue: &mut AnnounceVerifyQueue,
714        actions: &mut Vec<TransportAction>,
715    ) {
716        if ctx.packet.flags.destination_type != constants::DESTINATION_SINGLE {
717            return;
718        }
719
720        let has_ratchet = ctx.packet.flags.context_flag == constants::FLAG_SET;
721        let announce = match AnnounceData::unpack(ctx.packet.data(), has_ratchet) {
722            Ok(a) => a,
723            Err(_) => {
724                actions.push(TransportAction::ProtocolViolation {
725                    interface: ctx.iface,
726                });
727                return;
728            }
729        };
730
731        let received_from = self.announce_received_from(&ctx.packet, ctx.now);
732
733        // Announce verification and this registry read share the engine's
734        // exclusive processing turn, so no destinations-map lock is needed.
735        if self
736            .local_destinations
737            .contains_key(&ctx.packet.destination_hash)
738        {
739            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
740                "Announce:skipping local destination {:02x}{:02x}{:02x}{:02x}..",
741                ctx.packet.destination_hash[0],
742                ctx.packet.destination_hash[1],
743                ctx.packet.destination_hash[2],
744                ctx.packet.destination_hash[3],
745            );
746            return;
747        }
748
749        let original_raw = ctx
750            .original_raw
751            .as_deref()
752            .expect("announce packets retain original raw bytes");
753        if self.should_hold_announce(&ctx.packet, original_raw, ctx.iface, ctx.now) {
754            return;
755        }
756
757        let sig_cache_key = Self::announce_sig_cache_key(&ctx.packet.destination_hash, &announce);
758        if self.announce_sig_cache.contains(&sig_cache_key) {
759            let Ok(validated) =
760                announce.to_validated_with_cached_signature(&ctx.packet.destination_hash)
761            else {
762                self.record_invalid_announce(&announce, ctx.iface, ctx.now, actions);
763                return;
764            };
765            let random_blob = match extract_random_blob(ctx.packet.data()) {
766                Some(b) => b,
767                None => return,
768            };
769            let announce_emitted = timebase_from_random_blob(&random_blob);
770            self.process_verified_announce(
771                VerifiedAnnounceCtx {
772                    packet: &ctx.packet,
773                    original_raw,
774                    iface: ctx.iface,
775                    now: ctx.now,
776                    validated,
777                    received_from,
778                    random_blob,
779                    announce_emitted,
780                },
781                rng,
782                actions,
783            );
784            return;
785        }
786
787        if ctx.packet.context == constants::CONTEXT_PATH_RESPONSE {
788            let Ok(validated) = announce.validate(&ctx.packet.destination_hash) else {
789                self.record_invalid_announce(&announce, ctx.iface, ctx.now, actions);
790                return;
791            };
792            self.announce_sig_cache.insert(sig_cache_key, ctx.now);
793            let random_blob = match extract_random_blob(ctx.packet.data()) {
794                Some(b) => b,
795                None => return,
796            };
797            let announce_emitted = timebase_from_random_blob(&random_blob);
798            self.process_verified_announce(
799                VerifiedAnnounceCtx {
800                    packet: &ctx.packet,
801                    original_raw,
802                    iface: ctx.iface,
803                    now: ctx.now,
804                    validated,
805                    received_from,
806                    random_blob,
807                    announce_emitted,
808                },
809                rng,
810                actions,
811            );
812            return;
813        }
814
815        let random_blob = match extract_random_blob(ctx.packet.data()) {
816            Some(b) => b,
817            None => {
818                actions.push(TransportAction::ProtocolViolation {
819                    interface: ctx.iface,
820                });
821                return;
822            }
823        };
824        let announce_emitted = timebase_from_random_blob(&random_blob);
825        let key = AnnounceVerifyKey {
826            destination_hash: ctx.packet.destination_hash,
827            random_blob,
828            received_from,
829        };
830        let pending = PendingAnnounce {
831            original_raw: original_raw.to_vec(),
832            packet: ctx.packet.clone(),
833            interface: ctx.iface,
834            received_from,
835            queued_at: ctx.now,
836            best_hops: ctx.packet.hops,
837            emission_ts: announce_emitted,
838            random_blob,
839        };
840        let _ = announce_queue.enqueue(key, pending);
841    }
842
843    pub fn complete_verified_announce(
844        &mut self,
845        pending: PendingAnnounce,
846        validated: crate::announce::ValidatedAnnounce,
847        sig_cache_key: [u8; 32],
848        now: f64,
849        rng: &mut dyn Rng,
850    ) -> Vec<TransportAction> {
851        self.announce_sig_cache.insert(sig_cache_key, now);
852        let mut actions = Vec::new();
853        self.process_verified_announce(
854            VerifiedAnnounceCtx {
855                packet: &pending.packet,
856                original_raw: &pending.original_raw,
857                iface: pending.interface,
858                now,
859                validated,
860                received_from: pending.received_from,
861                random_blob: pending.random_blob,
862                announce_emitted: pending.emission_ts,
863            },
864            rng,
865            &mut actions,
866        );
867        actions
868    }
869
870    pub fn clear_failed_verified_announce(&mut self, _sig_cache_key: [u8; 32], _now: f64) {}
871
872    fn process_verified_announce(
873        &mut self,
874        ctx: VerifiedAnnounceCtx<'_>,
875        rng: &mut dyn Rng,
876        actions: &mut Vec<TransportAction>,
877    ) {
878        if self.is_blackholed(&ctx.validated.identity_hash, ctx.now) {
879            return;
880        }
881        if ctx.packet.hops > constants::PATHFINDER_M {
882            return;
883        }
884
885        let existing_set = self.path_table.get(&ctx.packet.destination_hash);
886        let was_unknown_destination = existing_set.is_none_or(|ps| ps.is_empty());
887
888        // Reset stale path state before first-path installation so path-state handling
889        // cannot race ahead of the path table for previously unknown destinations.
890        if was_unknown_destination {
891            self.path_states.remove(&ctx.packet.destination_hash);
892        }
893
894        // Multi-path aware decision
895        let is_unresponsive = self.path_is_unresponsive(&ctx.packet.destination_hash);
896
897        let current_gravity = existing_set
898            .and_then(|path_set| path_set.primary())
899            .and_then(|path| self.interfaces.get(&path.receiving_interface))
900            .map(|interface| interface.gravity);
901        let announce_gravity = self
902            .interfaces
903            .get(&ctx.iface)
904            .map(|interface| interface.gravity);
905        let higher_gravity_replacement = existing_set.is_some_and(|path_set| {
906            pathfinder::is_higher_gravity_replacement(
907                path_set,
908                ctx.packet.hops,
909                ctx.announce_emitted,
910                current_gravity,
911                announce_gravity,
912            )
913        });
914        let mp_decision = pathfinder::decide_announce_multipath_with_gravity(
915            existing_set,
916            ctx.packet.hops,
917            ctx.announce_emitted,
918            &ctx.random_blob,
919            &ctx.received_from,
920            is_unresponsive,
921            ctx.now,
922            self.config.prefer_shorter_path,
923            current_gravity,
924            announce_gravity,
925        );
926
927        if mp_decision == MultiPathDecision::Reject {
928            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
929                "Announce:path decision REJECT for dest={:02x}{:02x}{:02x}{:02x}..",
930                ctx.packet.destination_hash[0],
931                ctx.packet.destination_hash[1],
932                ctx.packet.destination_hash[2],
933                ctx.packet.destination_hash[3],
934            );
935            return;
936        }
937        if higher_gravity_replacement {
938            log::log!(
939                target: crate::logging::PATHING_LOG_TARGET,
940                crate::logging::GRAVITY_UPDATE_LOG_LEVEL,
941                "Replacing path table entry for {:02x}{:02x}{:02x}{:02x}.. due to higher gravity ({:?}->{:?})",
942                ctx.packet.destination_hash[0],
943                ctx.packet.destination_hash[1],
944                ctx.packet.destination_hash[2],
945                ctx.packet.destination_hash[3],
946                current_gravity,
947                announce_gravity,
948            );
949        }
950
951        // Rate limiting
952        let rate_blocked = if ctx.packet.context != constants::CONTEXT_PATH_RESPONSE {
953            if let Some(iface_info) = self.interfaces.get(&ctx.iface) {
954                self.rate_limiter.check_and_update(
955                    &ctx.packet.destination_hash,
956                    ctx.now,
957                    iface_info.announce_rate_target,
958                    iface_info.announce_rate_grace,
959                    iface_info.announce_rate_penalty,
960                )
961            } else {
962                false
963            }
964        } else {
965            false
966        };
967
968        // Get interface mode for expiry calculation
969        let interface_mode = self
970            .interfaces
971            .get(&ctx.iface)
972            .map(|i| i.mode)
973            .unwrap_or(constants::MODE_FULL);
974
975        let expires = compute_path_expires(ctx.now, interface_mode);
976
977        // Get existing random blobs from the matching path (same next_hop) or empty
978        let existing_blobs = self
979            .path_table
980            .get(&ctx.packet.destination_hash)
981            .and_then(|ps| ps.find_by_next_hop(&ctx.received_from))
982            .map(|e| e.random_blobs.clone())
983            .unwrap_or_default();
984
985        // Generate RNG value for retransmit timeout
986        let mut rng_bytes = [0u8; 8];
987        rng.fill_bytes(&mut rng_bytes);
988        let rng_value = (u64::from_le_bytes(rng_bytes) as f64) / (u64::MAX as f64);
989
990        let is_path_response = ctx.packet.context == constants::CONTEXT_PATH_RESPONSE;
991
992        let (path_entry, announce_entry) = announce_proc::process_validated_announce(
993            ctx.packet.destination_hash,
994            ctx.packet.hops,
995            ctx.packet.data(),
996            &ctx.packet.raw,
997            ctx.packet.packet_hash,
998            ctx.packet.flags.context_flag,
999            ctx.received_from,
1000            ctx.iface,
1001            ctx.now,
1002            existing_blobs,
1003            ctx.random_blob,
1004            expires,
1005            rng_value,
1006            self.config.transport_enabled,
1007            is_path_response,
1008            rate_blocked,
1009            Some(ctx.original_raw.to_vec()),
1010        );
1011
1012        // Emit CacheAnnounce for disk caching (pre-hop-increment raw)
1013        actions.push(TransportAction::CacheAnnounce {
1014            packet_hash: ctx.packet.packet_hash,
1015            raw: ctx.original_raw.to_vec().into(),
1016        });
1017
1018        // Store path via upsert into PathSet
1019        match mp_decision {
1020            MultiPathDecision::ReplacePrimary => self.upsert_primary_path_destination(
1021                ctx.packet.destination_hash,
1022                path_entry,
1023                ctx.now,
1024            ),
1025            MultiPathDecision::AddAlternative => {
1026                self.upsert_path_destination(ctx.packet.destination_hash, path_entry, ctx.now)
1027            }
1028            MultiPathDecision::Reject => unreachable!("rejected decisions returned above"),
1029        }
1030
1031        // If receiving interface has a tunnel_id, store path in tunnel table too
1032        if let Some(tunnel_id) = self.interfaces.get(&ctx.iface).and_then(|i| i.tunnel_id) {
1033            let blobs = self
1034                .path_table
1035                .get(&ctx.packet.destination_hash)
1036                .and_then(|ps| ps.find_by_next_hop(&ctx.received_from))
1037                .map(|e| e.random_blobs.clone())
1038                .unwrap_or_default();
1039            self.tunnel_table.store_tunnel_path(
1040                &tunnel_id,
1041                ctx.packet.destination_hash,
1042                tunnel::TunnelPath {
1043                    timestamp: ctx.now,
1044                    received_from: ctx.received_from,
1045                    hops: ctx.packet.hops,
1046                    expires,
1047                    random_blobs: blobs,
1048                    packet_hash: ctx.packet.packet_hash,
1049                },
1050                ctx.now,
1051                self.config.destination_timeout_secs,
1052                self.config.max_tunnel_destinations_total,
1053            );
1054        }
1055
1056        // Re-apply the path-state reset after storing the path entry so any transient
1057        // stale state is also cleared once the destination exists in the path table.
1058        self.path_states.remove(&ctx.packet.destination_hash);
1059
1060        // Store announce for retransmission
1061        if let Some(ann) = announce_entry {
1062            self.insert_announce_entry(ctx.packet.destination_hash, ann, ctx.now);
1063        }
1064
1065        // Emit actions
1066        actions.push(TransportAction::AnnounceReceived(alloc::boxed::Box::new(
1067            types::ReceivedAnnounce {
1068                destination_hash: ctx.packet.destination_hash,
1069                identity_hash: ctx.validated.identity_hash,
1070                public_key: ctx.validated.public_key,
1071                name_hash: ctx.validated.name_hash,
1072                random_hash: ctx.validated.random_hash,
1073                ratchet: ctx.validated.ratchet,
1074                app_data: ctx.validated.app_data,
1075                hops: ctx.packet.hops,
1076                receiving_interface: ctx.iface,
1077                rx: RxMetadata {
1078                    rssi: ctx.packet.rssi,
1079                    snr: ctx.packet.snr,
1080                },
1081            },
1082        )));
1083
1084        actions.push(TransportAction::PathUpdated {
1085            destination_hash: ctx.packet.destination_hash,
1086            hops: ctx.packet.hops,
1087            next_hop: ctx.received_from,
1088            interface: ctx.iface,
1089        });
1090
1091        // Forward announce to local clients if any are connected
1092        if self.has_local_clients() {
1093            actions.push(TransportAction::ForwardToLocalClients {
1094                raw: self.announce_raw_for_local_clients(ctx.packet),
1095                exclude: Some(ctx.iface),
1096            });
1097        }
1098
1099        // Check for discovery path requests waiting for this announce
1100        if let Some(requesting_interfaces) =
1101            self.discovery_path_requests_waiting(&ctx.packet.destination_hash)
1102        {
1103            // Build a path response announce and queue it
1104            let entry = AnnounceEntry {
1105                timestamp: ctx.now,
1106                retransmit_timeout: ctx.now,
1107                retries: constants::PATHFINDER_R,
1108                received_from: ctx.received_from,
1109                hops: ctx.packet.hops,
1110                packet_raw: ctx.packet.raw.clone(),
1111                packet_data: ctx.packet.data().to_vec(),
1112                destination_hash: ctx.packet.destination_hash,
1113                context_flag: ctx.packet.flags.context_flag,
1114                local_rebroadcasts: 0,
1115                block_rebroadcasts: true,
1116                attached_interface: requesting_interfaces.first().copied(),
1117            };
1118            if let Some(identity_hash) = self.config.identity_hash {
1119                let raw = announce_proc::build_retransmit_announce(&entry, &identity_hash);
1120                for interface in requesting_interfaces.iter().skip(1) {
1121                    actions.push(TransportAction::SendOnInterface {
1122                        interface: *interface,
1123                        raw: raw.clone().into(),
1124                    });
1125                }
1126            }
1127            self.insert_announce_entry(ctx.packet.destination_hash, entry, ctx.now);
1128        }
1129    }
1130
1131    pub fn announce_sig_cache_contains(&self, sig_cache_key: &[u8; 32]) -> bool {
1132        self.announce_sig_cache.contains(sig_cache_key)
1133    }
1134
1135    /// Check if there's a waiting discovery path request for a destination.
1136    /// Consumes the request if found (one-shot: the caller queues the announce response).
1137    pub(super) fn discovery_path_requests_waiting(
1138        &mut self,
1139        dest_hash: &[u8; 16],
1140    ) -> Option<Vec<InterfaceId>> {
1141        let request = self
1142            .discovery_path_requests
1143            .remove(dest_hash)
1144            .map(|req| req.requesting_interfaces);
1145        self.discovery_path_request_deadlines.remove(dest_hash);
1146        request
1147    }
1148
1149    // =========================================================================
1150    // Inbound proof processing
1151    // =========================================================================
1152
1153    fn process_inbound_proof(
1154        &mut self,
1155        ctx: &InboundPacketCtx,
1156        actions: &mut Vec<TransportAction>,
1157    ) {
1158        let packet = &ctx.packet;
1159        if packet.context == constants::CONTEXT_LRPROOF {
1160            // Link request proof routing
1161            if (self.config.transport_enabled)
1162                && self.link_table.contains_key(&packet.destination_hash)
1163            {
1164                let link_entry = self.link_table.get(&packet.destination_hash).cloned();
1165                if let Some(entry) = link_entry {
1166                    let instance_local_link = self
1167                        .interface_is_local_client(entry.next_hop_interface)
1168                        && self.interface_is_local_client(entry.received_interface);
1169                    if let Some((outbound_interface, new_raw)) = route_via_link_table(
1170                        packet,
1171                        &entry,
1172                        ctx.iface,
1173                        LocalHopRewrite {
1174                            local_hops_delta: self.config.local_hops_delta,
1175                            from_local_client: ctx.from_local_client,
1176                            skip_local_hops_delta: instance_local_link,
1177                        },
1178                    ) {
1179                        // Forward the proof (simplified: skip signature validation
1180                        // which requires Identity recall)
1181
1182                        // Mark link as validated
1183                        if let Some(le) = self.link_table.get_mut(&packet.destination_hash) {
1184                            le.validated = true;
1185                        }
1186
1187                        actions.push(TransportAction::LinkEstablished {
1188                            link_id: packet.destination_hash,
1189                            interface: outbound_interface,
1190                        });
1191
1192                        actions.push(TransportAction::SendOnInterface {
1193                            interface: outbound_interface,
1194                            raw: new_raw.into(),
1195                        });
1196                    } else if link_route_hops_match(packet.hops, &entry, ctx.iface) {
1197                        log::debug!(
1198                            "Link request proof received on wrong interface {}, not transporting it (expected {} or {})",
1199                            ctx.iface.0,
1200                            entry.next_hop_interface.0,
1201                            entry.received_interface.0,
1202                        );
1203                    } else {
1204                        log::debug!("{}", lrproof_hop_mismatch_diagnostic(packet.hops, &entry));
1205                    }
1206                }
1207            } else {
1208                // Could be for a local pending link - deliver locally
1209                let mut delivery_raw = packet.raw.clone();
1210                // LinkManager must see the same post-ingress hop metric that
1211                // was authenticated and considered by transport.
1212                if delivery_raw.len() >= 2 {
1213                    delivery_raw[1] = packet.hops;
1214                }
1215                actions.push(TransportAction::DeliverLocal {
1216                    destination_hash: packet.destination_hash,
1217                    raw: PacketBytes::from(delivery_raw),
1218                    packet_hash: packet.packet_hash,
1219                    receiving_interface: ctx.iface,
1220                });
1221            }
1222        } else {
1223            // Regular proof: check reverse table
1224            if self.config.transport_enabled {
1225                if let Some(reverse_entry) = self.reverse_table.remove(&packet.destination_hash) {
1226                    let proof_for_local_client =
1227                        self.interface_is_local_client(reverse_entry.receiving_interface);
1228                    if let Some(action) = route_proof_via_reverse(
1229                        packet,
1230                        &reverse_entry,
1231                        ctx.iface,
1232                        LocalHopRewrite {
1233                            local_hops_delta: self.config.local_hops_delta,
1234                            from_local_client: ctx.from_local_client,
1235                            skip_local_hops_delta: proof_for_local_client,
1236                        },
1237                    ) {
1238                        actions.push(action);
1239                    }
1240                }
1241            }
1242
1243            // Deliver to local receipts
1244            actions.push(TransportAction::DeliverLocal {
1245                destination_hash: packet.destination_hash,
1246                raw: PacketBytes::from(packet.raw.clone()),
1247                packet_hash: packet.packet_hash,
1248                receiving_interface: ctx.iface,
1249            });
1250        }
1251    }
1252}