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        // Transport.py: clients behind a shared instance are announced with a
255        // "spoofed" hop count of 0, so they look directly reachable and
256        // senders strip the transport header. Treat a packet for such a
257        // client as addressed to us so normal forwarding handles it (#183).
258        let for_local_client = ctx.packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
259            && self
260                .path_table
261                .get(&ctx.packet.destination_hash)
262                .and_then(|ps| ps.primary())
263                .is_some_and(|entry| {
264                    entry.hops == 0 && self.interface_is_local_client(entry.receiving_interface)
265                });
266        let transport_id = match ctx.packet.transport_id {
267            None if for_local_client => self.config.identity_hash,
268            transport_id => transport_id,
269        };
270        if transport_id.is_none() || ctx.packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
271        {
272            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
273                log::debug!(
274                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. not transport-addressed header={} iface={}",
275                    ctx.packet.destination_hash[0],
276                    ctx.packet.destination_hash[1],
277                    ctx.packet.destination_hash[2],
278                    ctx.packet.destination_hash[3],
279                    ctx.packet.flags.header_type,
280                    ctx.iface.0
281                );
282            }
283            return;
284        }
285
286        let Some(identity_hash) = self.config.identity_hash else {
287            return;
288        };
289        if transport_id != Some(identity_hash) {
290            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
291                log::debug!(
292                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. transport mismatch got={:02x?} own={:02x?} iface={}",
293                    ctx.packet.destination_hash[0],
294                    ctx.packet.destination_hash[1],
295                    ctx.packet.destination_hash[2],
296                    ctx.packet.destination_hash[3],
297                    ctx.packet.transport_id.as_ref().map(|id| &id[..4]),
298                    &identity_hash[..4],
299                    ctx.iface.0
300                );
301            }
302            return;
303        }
304
305        let Some(path_entry) = self
306            .path_table
307            .get(&ctx.packet.destination_hash)
308            .and_then(|ps| ps.primary())
309        else {
310            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
311                log::debug!(
312                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. addressed to us but no path iface={}",
313                    ctx.packet.destination_hash[0],
314                    ctx.packet.destination_hash[1],
315                    ctx.packet.destination_hash[2],
316                    ctx.packet.destination_hash[3],
317                    ctx.iface.0
318                );
319            }
320            return;
321        };
322
323        let next_hop = path_entry.next_hop;
324        let remaining_hops = path_entry.hops;
325        let outbound_interface = path_entry.receiving_interface;
326        let outbound_is_local_client = self
327            .interfaces
328            .get(&outbound_interface)
329            .map(|info| info.is_local_client)
330            .unwrap_or(false);
331        let forwarded_remaining_hops = if outbound_is_local_client {
332            0
333        } else {
334            remaining_hops
335        };
336        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
337            log::debug!(
338                "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. remaining_hops={} out_iface={} next_hop={:02x?}",
339                ctx.packet.destination_hash[0],
340                ctx.packet.destination_hash[1],
341                ctx.packet.destination_hash[2],
342                ctx.packet.destination_hash[3],
343                remaining_hops,
344                outbound_interface.0,
345                &next_hop[..4]
346            );
347        }
348        let mut new_raw = forward_transport_packet(
349            &ctx.packet,
350            next_hop,
351            forwarded_remaining_hops,
352            outbound_interface,
353        );
354        if self.config.local_hops_delta != 0
355            && ctx.from_local_client
356            && !outbound_is_local_client
357            && ctx.packet.hops == 0
358            && ctx.packet.flags.destination_type != constants::DESTINATION_PLAIN
359            && ctx.packet.flags.destination_type != constants::DESTINATION_GROUP
360            && new_raw.len() > 1
361        {
362            new_raw[1] = self.config.local_hops_delta;
363        }
364
365        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST {
366            clamp_forwarded_link_request_mtu(
367                &mut new_raw,
368                ctx.packet.data().len() == constants::LINK_ECPUBSIZE + constants::LINK_MTU_SIZE,
369                self.interfaces.get(&ctx.iface).map(|info| info.mtu),
370                self.interfaces
371                    .get(&outbound_interface)
372                    .map(|info| info.mtu),
373            );
374            let extra_proof_timeout =
375                extra_link_proof_timeout(self.interfaces.get(&outbound_interface));
376            let proof_timeout = ctx.now
377                + constants::LINK_ESTABLISHMENT_TIMEOUT_PER_HOP * (remaining_hops.max(1) as f64)
378                + extra_proof_timeout;
379            let (link_id, link_entry) = create_link_entry(
380                &ctx.packet,
381                next_hop,
382                outbound_interface,
383                remaining_hops,
384                ctx.iface,
385                ctx.now,
386                proof_timeout,
387            );
388            self.link_table.insert(link_id, link_entry);
389            actions.push(TransportAction::LinkRequestReceived {
390                link_id,
391                destination_hash: ctx.packet.destination_hash,
392                receiving_interface: ctx.iface,
393            });
394        } else {
395            let (trunc_hash, reverse_entry) =
396                create_reverse_entry(&ctx.packet, outbound_interface, ctx.iface, ctx.now);
397            self.reverse_table.insert(trunc_hash, reverse_entry);
398        }
399
400        actions.push(TransportAction::SendOnInterface {
401            interface: outbound_interface,
402            raw: new_raw.into(),
403        });
404
405        if let Some(entry) = self
406            .path_table
407            .get_mut(&ctx.packet.destination_hash)
408            .and_then(|ps| ps.primary_mut())
409        {
410            entry.timestamp = ctx.now;
411        }
412    }
413
414    fn handle_link_table_routing(
415        &mut self,
416        ctx: &InboundPacketCtx,
417        actions: &mut Vec<TransportAction>,
418    ) {
419        if !self.config.transport_enabled && self.config.identity_hash.is_none() {
420            return;
421        }
422        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
423            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
424            || ctx.packet.context == constants::CONTEXT_LRPROOF
425        {
426            return;
427        }
428
429        // This single keyed lookup is the native link fast path. The engine is
430        // already exclusively borrowed, and the typed entry contains every
431        // routing and hop-rewrite field, so a second denormalized cache would
432        // add invalidation risk without avoiding locks or linear searches.
433        // Keep this authoritative-table path even when profiling suggests a
434        // cache: upstream's corresponding denormalized experiment was removed.
435        let Some(link_entry) = self.link_table.get(&ctx.packet.destination_hash).cloned() else {
436            return;
437        };
438        if !link_entry.validated {
439            return;
440        }
441        let instance_local_link = self.interface_is_local_client(link_entry.next_hop_interface)
442            && self.interface_is_local_client(link_entry.received_interface);
443        let Some((outbound_iface, new_raw)) = super::inbound::route_via_link_table_shared(
444            &ctx.packet,
445            &link_entry,
446            ctx.iface,
447            LocalHopRewrite {
448                local_hops_delta: self.config.local_hops_delta,
449                from_local_client: ctx.from_local_client,
450                skip_local_hops_delta: instance_local_link,
451            },
452        ) else {
453            // A link entry with no route from this receiving interface is an
454            // expected fast-path miss, not an operator-facing warning.
455            return;
456        };
457
458        // A deferred packet's context bypasses deduplication, so its hash
459        // could never mark a later packet as a duplicate.
460        if !ctx.hash_deferred {
461            self.packet_hashlist.add(ctx.packet.packet_hash);
462        }
463        actions.push(TransportAction::SendOnInterface {
464            interface: outbound_iface,
465            raw: new_raw,
466        });
467
468        if let Some(entry) = self.link_table.get_mut(&ctx.packet.destination_hash) {
469            entry.timestamp = ctx.now;
470        }
471    }
472
473    fn handle_inbound_announce(
474        &mut self,
475        ctx: &InboundPacketCtx,
476        rng: &mut dyn Rng,
477        announce_queue: Option<&mut AnnounceVerifyQueue>,
478        actions: &mut Vec<TransportAction>,
479    ) {
480        if ctx.packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE {
481            return;
482        }
483
484        if let Some(queue) = announce_queue {
485            self.try_enqueue_announce(ctx, rng, queue, actions);
486        } else {
487            let original_raw = ctx
488                .original_raw
489                .as_deref()
490                .expect("announce packets retain original raw bytes");
491            self.process_inbound_announce(
492                &ctx.packet,
493                original_raw,
494                ctx.iface,
495                ctx.now,
496                rng,
497                actions,
498            );
499        }
500    }
501
502    fn handle_inbound_local_delivery(
503        &self,
504        ctx: &InboundPacketCtx,
505        actions: &mut Vec<TransportAction>,
506    ) {
507        if (ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
508            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA)
509            // The engine's exclusive borrow makes this a direct, lock-free
510            // destination lookup for data and link requests alike; delivery
511            // reuses the same keyed registry.
512            && self
513                .local_destinations
514                .contains_key(&ctx.packet.destination_hash)
515        {
516            let mut delivery_raw = ctx.packet.raw.clone();
517            // Link responders learn the post-ingress hop metric from the
518            // authenticated LRRTT packet. Preserve that field when crossing
519            // the action boundary without changing ordinary callback bytes.
520            if ctx.packet.context == constants::CONTEXT_LRRTT && delivery_raw.len() >= 2 {
521                delivery_raw[1] = ctx.packet.hops;
522            }
523            actions.push(TransportAction::DeliverLocal {
524                destination_hash: ctx.packet.destination_hash,
525                raw: PacketBytes::from(delivery_raw),
526                packet_hash: ctx.packet.packet_hash,
527                receiving_interface: ctx.iface,
528            });
529        }
530    }
531
532    // =========================================================================
533    // Inbound announce processing
534    // =========================================================================
535
536    fn process_inbound_announce(
537        &mut self,
538        packet: &RawPacket,
539        original_raw: &[u8],
540        iface: InterfaceId,
541        now: f64,
542        rng: &mut dyn Rng,
543        actions: &mut Vec<TransportAction>,
544    ) {
545        if packet.flags.destination_type != constants::DESTINATION_SINGLE {
546            return;
547        }
548
549        let has_ratchet = packet.flags.context_flag == constants::FLAG_SET;
550
551        // Unpack and validate announce
552        let announce = match AnnounceData::unpack(packet.data(), has_ratchet) {
553            Ok(a) => a,
554            Err(_) => {
555                actions.push(TransportAction::ProtocolViolation { interface: iface });
556                return;
557            }
558        };
559
560        if self.should_hold_announce(packet, original_raw, iface, now) {
561            return;
562        }
563
564        let sig_cache_key = Self::announce_sig_cache_key(&packet.destination_hash, &announce);
565
566        let validated = if self.announce_sig_cache.contains(&sig_cache_key) {
567            announce.to_validated_with_cached_signature(&packet.destination_hash)
568        } else {
569            announce.validate(&packet.destination_hash).inspect(|_| {
570                self.announce_sig_cache.insert(sig_cache_key, now);
571            })
572        };
573        let validated = match validated {
574            Ok(v) => v,
575            Err(_) => {
576                self.record_invalid_announce(&announce, iface, now, actions);
577                return;
578            }
579        };
580
581        let received_from = self.announce_received_from(packet, now);
582        let random_blob = match extract_random_blob(packet.data()) {
583            Some(b) => b,
584            None => {
585                actions.push(TransportAction::ProtocolViolation { interface: iface });
586                return;
587            }
588        };
589        let announce_emitted = timebase_from_random_blob(&random_blob);
590
591        self.process_verified_announce(
592            VerifiedAnnounceCtx {
593                packet,
594                original_raw,
595                iface,
596                now,
597                validated,
598                received_from,
599                random_blob,
600                announce_emitted,
601            },
602            rng,
603            actions,
604        );
605    }
606
607    fn announce_raw_for_local_clients(&self, packet: &RawPacket) -> PacketBytes {
608        let Some(identity_hash) = self.config.identity_hash else {
609            return PacketBytes::from(packet.raw.clone());
610        };
611
612        if packet.raw.len() < 2 {
613            return PacketBytes::from(packet.raw.clone());
614        }
615
616        let payload_start = if packet.flags.header_type == constants::HEADER_2 {
617            18usize
618        } else {
619            2usize
620        };
621        if packet.raw.len() < payload_start {
622            return PacketBytes::from(packet.raw.clone());
623        }
624
625        let flags = (constants::HEADER_2 << 6)
626            | (constants::TRANSPORT_TRANSPORT << 4)
627            | (packet.raw[0] & 0x0F);
628        let mut raw = Vec::with_capacity(18 + packet.raw.len() - payload_start);
629        raw.push(flags);
630        raw.push(packet.hops);
631        raw.extend_from_slice(&identity_hash);
632        raw.extend_from_slice(&packet.raw[payload_start..]);
633        PacketBytes::from(raw)
634    }
635
636    /// Signature cache key for an announce: covers the full signed data
637    /// (see [`AnnounceData::signature_cache_key`]) plus the signature.
638    pub(super) fn announce_sig_cache_key(
639        destination_hash: &[u8; 16],
640        announce: &AnnounceData,
641    ) -> [u8; 32] {
642        announce.signature_cache_key(destination_hash)
643    }
644
645    fn announce_received_from(&mut self, packet: &RawPacket, now: f64) -> [u8; 16] {
646        if let Some(transport_id) = packet.transport_id {
647            if self.config.transport_enabled {
648                if let Some(announce_entry) = self.announce_table.get_mut(&packet.destination_hash)
649                {
650                    if packet.hops.checked_sub(1) == Some(announce_entry.hops) {
651                        announce_entry.local_rebroadcasts += 1;
652                        if announce_entry.retries > 0
653                            && announce_entry.local_rebroadcasts
654                                >= constants::LOCAL_REBROADCASTS_MAX
655                        {
656                            self.announce_table.remove(&packet.destination_hash);
657                        }
658                    }
659                    if let Some(announce_entry) = self.announce_table.get(&packet.destination_hash)
660                    {
661                        if packet.hops.checked_sub(1) == Some(announce_entry.hops + 1)
662                            && announce_entry.retries > 0
663                            && now < announce_entry.retransmit_timeout
664                        {
665                            self.announce_table.remove(&packet.destination_hash);
666                        }
667                    }
668                }
669            }
670            transport_id
671        } else {
672            packet.destination_hash
673        }
674    }
675
676    fn should_hold_announce(
677        &mut self,
678        packet: &RawPacket,
679        original_raw: &[u8],
680        iface: InterfaceId,
681        now: f64,
682    ) -> bool {
683        if self.has_path(&packet.destination_hash) {
684            return false;
685        }
686        if self
687            .discovery_path_requests
688            .contains_key(&packet.destination_hash)
689        {
690            return false;
691        }
692        let Some(info) = self.interfaces.get(&iface) else {
693            return false;
694        };
695        if packet.context == constants::CONTEXT_PATH_RESPONSE
696            || !self.ingress_control.should_ingress_limit(
697                iface,
698                &info.ingress_control,
699                info.ia_freq,
700                info.started,
701                now,
702            )
703        {
704            return false;
705        }
706        self.ingress_control.hold_announce(
707            iface,
708            &info.ingress_control,
709            packet.destination_hash,
710            ingress_control::HeldAnnounce {
711                raw: original_raw.to_vec(),
712                hops: packet.hops,
713                receiving_interface: iface,
714                rx: RxMetadata {
715                    rssi: packet.rssi,
716                    snr: packet.snr,
717                },
718                timestamp: now,
719            },
720        );
721        true
722    }
723
724    fn try_enqueue_announce(
725        &mut self,
726        ctx: &InboundPacketCtx,
727        rng: &mut dyn Rng,
728        announce_queue: &mut AnnounceVerifyQueue,
729        actions: &mut Vec<TransportAction>,
730    ) {
731        if ctx.packet.flags.destination_type != constants::DESTINATION_SINGLE {
732            return;
733        }
734
735        let has_ratchet = ctx.packet.flags.context_flag == constants::FLAG_SET;
736        let announce = match AnnounceData::unpack(ctx.packet.data(), has_ratchet) {
737            Ok(a) => a,
738            Err(_) => {
739                actions.push(TransportAction::ProtocolViolation {
740                    interface: ctx.iface,
741                });
742                return;
743            }
744        };
745
746        let received_from = self.announce_received_from(&ctx.packet, ctx.now);
747
748        // Announce verification and this registry read share the engine's
749        // exclusive processing turn, so no destinations-map lock is needed.
750        if self
751            .local_destinations
752            .contains_key(&ctx.packet.destination_hash)
753        {
754            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
755                "Announce:skipping local destination {:02x}{:02x}{:02x}{:02x}..",
756                ctx.packet.destination_hash[0],
757                ctx.packet.destination_hash[1],
758                ctx.packet.destination_hash[2],
759                ctx.packet.destination_hash[3],
760            );
761            return;
762        }
763
764        let original_raw = ctx
765            .original_raw
766            .as_deref()
767            .expect("announce packets retain original raw bytes");
768        if self.should_hold_announce(&ctx.packet, original_raw, ctx.iface, ctx.now) {
769            return;
770        }
771
772        let sig_cache_key = Self::announce_sig_cache_key(&ctx.packet.destination_hash, &announce);
773        if self.announce_sig_cache.contains(&sig_cache_key) {
774            let Ok(validated) =
775                announce.to_validated_with_cached_signature(&ctx.packet.destination_hash)
776            else {
777                self.record_invalid_announce(&announce, ctx.iface, ctx.now, actions);
778                return;
779            };
780            let random_blob = match extract_random_blob(ctx.packet.data()) {
781                Some(b) => b,
782                None => return,
783            };
784            let announce_emitted = timebase_from_random_blob(&random_blob);
785            self.process_verified_announce(
786                VerifiedAnnounceCtx {
787                    packet: &ctx.packet,
788                    original_raw,
789                    iface: ctx.iface,
790                    now: ctx.now,
791                    validated,
792                    received_from,
793                    random_blob,
794                    announce_emitted,
795                },
796                rng,
797                actions,
798            );
799            return;
800        }
801
802        if ctx.packet.context == constants::CONTEXT_PATH_RESPONSE {
803            let Ok(validated) = announce.validate(&ctx.packet.destination_hash) else {
804                self.record_invalid_announce(&announce, ctx.iface, ctx.now, actions);
805                return;
806            };
807            self.announce_sig_cache.insert(sig_cache_key, ctx.now);
808            let random_blob = match extract_random_blob(ctx.packet.data()) {
809                Some(b) => b,
810                None => return,
811            };
812            let announce_emitted = timebase_from_random_blob(&random_blob);
813            self.process_verified_announce(
814                VerifiedAnnounceCtx {
815                    packet: &ctx.packet,
816                    original_raw,
817                    iface: ctx.iface,
818                    now: ctx.now,
819                    validated,
820                    received_from,
821                    random_blob,
822                    announce_emitted,
823                },
824                rng,
825                actions,
826            );
827            return;
828        }
829
830        let random_blob = match extract_random_blob(ctx.packet.data()) {
831            Some(b) => b,
832            None => {
833                actions.push(TransportAction::ProtocolViolation {
834                    interface: ctx.iface,
835                });
836                return;
837            }
838        };
839        let announce_emitted = timebase_from_random_blob(&random_blob);
840        let key = AnnounceVerifyKey {
841            destination_hash: ctx.packet.destination_hash,
842            random_blob,
843            received_from,
844        };
845        let pending = PendingAnnounce {
846            original_raw: original_raw.to_vec(),
847            packet: ctx.packet.clone(),
848            interface: ctx.iface,
849            received_from,
850            queued_at: ctx.now,
851            best_hops: ctx.packet.hops,
852            emission_ts: announce_emitted,
853            random_blob,
854        };
855        let _ = announce_queue.enqueue(key, pending);
856    }
857
858    pub fn complete_verified_announce(
859        &mut self,
860        pending: PendingAnnounce,
861        validated: crate::announce::ValidatedAnnounce,
862        sig_cache_key: [u8; 32],
863        now: f64,
864        rng: &mut dyn Rng,
865    ) -> Vec<TransportAction> {
866        self.announce_sig_cache.insert(sig_cache_key, now);
867        let mut actions = Vec::new();
868        self.process_verified_announce(
869            VerifiedAnnounceCtx {
870                packet: &pending.packet,
871                original_raw: &pending.original_raw,
872                iface: pending.interface,
873                now,
874                validated,
875                received_from: pending.received_from,
876                random_blob: pending.random_blob,
877                announce_emitted: pending.emission_ts,
878            },
879            rng,
880            &mut actions,
881        );
882        actions
883    }
884
885    pub fn clear_failed_verified_announce(&mut self, _sig_cache_key: [u8; 32], _now: f64) {}
886
887    fn process_verified_announce(
888        &mut self,
889        ctx: VerifiedAnnounceCtx<'_>,
890        rng: &mut dyn Rng,
891        actions: &mut Vec<TransportAction>,
892    ) {
893        if self.is_blackholed(&ctx.validated.identity_hash, ctx.now) {
894            return;
895        }
896        if ctx.packet.hops > constants::PATHFINDER_M {
897            return;
898        }
899
900        let existing_set = self.path_table.get(&ctx.packet.destination_hash);
901        let was_unknown_destination = existing_set.is_none_or(|ps| ps.is_empty());
902
903        // Reset stale path state before first-path installation so path-state handling
904        // cannot race ahead of the path table for previously unknown destinations.
905        if was_unknown_destination {
906            self.path_states.remove(&ctx.packet.destination_hash);
907        }
908
909        // Multi-path aware decision
910        let is_unresponsive = self.path_is_unresponsive(&ctx.packet.destination_hash);
911
912        let current_gravity = existing_set
913            .and_then(|path_set| path_set.primary())
914            .and_then(|path| self.interfaces.get(&path.receiving_interface))
915            .map(|interface| interface.gravity);
916        let announce_gravity = self
917            .interfaces
918            .get(&ctx.iface)
919            .map(|interface| interface.gravity);
920        let higher_gravity_replacement = existing_set.is_some_and(|path_set| {
921            pathfinder::is_higher_gravity_replacement(
922                path_set,
923                ctx.packet.hops,
924                ctx.announce_emitted,
925                ctx.now,
926                current_gravity,
927                announce_gravity,
928            )
929        });
930        let mp_decision = pathfinder::decide_announce_multipath_with_gravity(
931            existing_set,
932            ctx.packet.hops,
933            ctx.announce_emitted,
934            &ctx.random_blob,
935            &ctx.received_from,
936            is_unresponsive,
937            ctx.now,
938            self.config.prefer_shorter_path,
939            current_gravity,
940            announce_gravity,
941        );
942
943        if mp_decision == MultiPathDecision::Reject {
944            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
945                "Announce:path decision REJECT for dest={:02x}{:02x}{:02x}{:02x}..",
946                ctx.packet.destination_hash[0],
947                ctx.packet.destination_hash[1],
948                ctx.packet.destination_hash[2],
949                ctx.packet.destination_hash[3],
950            );
951            return;
952        }
953        if higher_gravity_replacement {
954            log::log!(
955                target: crate::logging::PATHING_LOG_TARGET,
956                crate::logging::GRAVITY_UPDATE_LOG_LEVEL,
957                "Replacing path table entry for {:02x}{:02x}{:02x}{:02x}.. due to higher gravity ({:?}->{:?})",
958                ctx.packet.destination_hash[0],
959                ctx.packet.destination_hash[1],
960                ctx.packet.destination_hash[2],
961                ctx.packet.destination_hash[3],
962                current_gravity,
963                announce_gravity,
964            );
965        }
966
967        // Rate limiting
968        let rate_blocked = if ctx.packet.context != constants::CONTEXT_PATH_RESPONSE {
969            if let Some(iface_info) = self.interfaces.get(&ctx.iface) {
970                self.rate_limiter.check_and_update(
971                    &ctx.packet.destination_hash,
972                    ctx.now,
973                    iface_info.announce_rate_target,
974                    iface_info.announce_rate_grace,
975                    iface_info.announce_rate_penalty,
976                )
977            } else {
978                false
979            }
980        } else {
981            false
982        };
983
984        // Get interface mode for expiry calculation
985        let interface_mode = self
986            .interfaces
987            .get(&ctx.iface)
988            .map(|i| i.mode)
989            .unwrap_or(constants::MODE_FULL);
990
991        let expires = compute_path_expires(ctx.now, interface_mode);
992
993        // Get existing random blobs from the matching path (same next_hop) or empty
994        let existing_blobs = self
995            .path_table
996            .get(&ctx.packet.destination_hash)
997            .and_then(|ps| ps.find_by_next_hop(&ctx.received_from))
998            .map(|e| e.random_blobs.clone())
999            .unwrap_or_default();
1000
1001        // Generate RNG value for retransmit timeout
1002        let mut rng_bytes = [0u8; 8];
1003        rng.fill_bytes(&mut rng_bytes);
1004        let rng_value = (u64::from_le_bytes(rng_bytes) as f64) / (u64::MAX as f64);
1005
1006        let is_path_response = ctx.packet.context == constants::CONTEXT_PATH_RESPONSE;
1007
1008        let (path_entry, announce_entry) = announce_proc::process_validated_announce(
1009            ctx.packet.destination_hash,
1010            ctx.packet.hops,
1011            ctx.packet.data(),
1012            &ctx.packet.raw,
1013            ctx.packet.packet_hash,
1014            ctx.packet.flags.context_flag,
1015            ctx.received_from,
1016            ctx.iface,
1017            ctx.now,
1018            existing_blobs,
1019            ctx.random_blob,
1020            expires,
1021            rng_value,
1022            self.config.transport_enabled,
1023            self.serves_local_client(ctx.iface),
1024            is_path_response,
1025            rate_blocked,
1026            Some(ctx.original_raw.to_vec()),
1027        );
1028
1029        // Emit CacheAnnounce for disk caching (pre-hop-increment raw)
1030        actions.push(TransportAction::CacheAnnounce {
1031            packet_hash: ctx.packet.packet_hash,
1032            raw: ctx.original_raw.to_vec().into(),
1033        });
1034
1035        // Store path via upsert into PathSet
1036        match mp_decision {
1037            MultiPathDecision::ReplacePrimary => self.upsert_primary_path_destination(
1038                ctx.packet.destination_hash,
1039                path_entry,
1040                ctx.now,
1041            ),
1042            MultiPathDecision::AddAlternative => {
1043                self.upsert_path_destination(ctx.packet.destination_hash, path_entry, ctx.now)
1044            }
1045            MultiPathDecision::Reject => unreachable!("rejected decisions returned above"),
1046        }
1047
1048        // If receiving interface has a tunnel_id, store path in tunnel table too
1049        if let Some(tunnel_id) = self.interfaces.get(&ctx.iface).and_then(|i| i.tunnel_id) {
1050            let blobs = self
1051                .path_table
1052                .get(&ctx.packet.destination_hash)
1053                .and_then(|ps| ps.find_by_next_hop(&ctx.received_from))
1054                .map(|e| e.random_blobs.clone())
1055                .unwrap_or_default();
1056            self.tunnel_table.store_tunnel_path(
1057                &tunnel_id,
1058                ctx.packet.destination_hash,
1059                tunnel::TunnelPath {
1060                    timestamp: ctx.now,
1061                    received_from: ctx.received_from,
1062                    hops: ctx.packet.hops,
1063                    expires,
1064                    random_blobs: blobs,
1065                    packet_hash: ctx.packet.packet_hash,
1066                },
1067                ctx.now,
1068                self.config.destination_timeout_secs,
1069                self.config.max_tunnel_destinations_total,
1070            );
1071        }
1072
1073        // Re-apply the path-state reset after storing the path entry so any transient
1074        // stale state is also cleared once the destination exists in the path table.
1075        self.path_states.remove(&ctx.packet.destination_hash);
1076
1077        // Store announce for retransmission
1078        if let Some(ann) = announce_entry {
1079            self.insert_announce_entry(ctx.packet.destination_hash, ann, ctx.now);
1080        } else if is_path_response && self.serves_local_client(ctx.iface) {
1081            // Transport.py:2375-2395: relay a local client's path response,
1082            // immediately and once, to an interface that asked for it.
1083            if let Some((requesting_interface, _)) = self
1084                .pending_local_path_requests
1085                .remove(&ctx.packet.destination_hash)
1086            {
1087                let entry = AnnounceEntry {
1088                    timestamp: ctx.now,
1089                    retransmit_timeout: ctx.now,
1090                    retries: constants::PATHFINDER_R,
1091                    received_from: ctx.received_from,
1092                    hops: ctx.packet.hops,
1093                    packet_raw: ctx.packet.raw.clone(),
1094                    packet_data: ctx.packet.data().to_vec(),
1095                    destination_hash: ctx.packet.destination_hash,
1096                    context_flag: ctx.packet.flags.context_flag,
1097                    local_rebroadcasts: 0,
1098                    block_rebroadcasts: true,
1099                    attached_interface: Some(requesting_interface),
1100                };
1101                self.insert_announce_entry(ctx.packet.destination_hash, entry, ctx.now);
1102            }
1103        }
1104
1105        // Emit actions
1106        actions.push(TransportAction::AnnounceReceived(alloc::boxed::Box::new(
1107            types::ReceivedAnnounce {
1108                destination_hash: ctx.packet.destination_hash,
1109                identity_hash: ctx.validated.identity_hash,
1110                public_key: ctx.validated.public_key,
1111                name_hash: ctx.validated.name_hash,
1112                random_hash: ctx.validated.random_hash,
1113                ratchet: ctx.validated.ratchet,
1114                app_data: ctx.validated.app_data,
1115                hops: ctx.packet.hops,
1116                receiving_interface: ctx.iface,
1117                rx: RxMetadata {
1118                    rssi: ctx.packet.rssi,
1119                    snr: ctx.packet.snr,
1120                },
1121            },
1122        )));
1123
1124        actions.push(TransportAction::PathUpdated {
1125            destination_hash: ctx.packet.destination_hash,
1126            hops: ctx.packet.hops,
1127            next_hop: ctx.received_from,
1128            interface: ctx.iface,
1129        });
1130
1131        // Forward announce to local clients if any are connected
1132        if self.has_local_clients() {
1133            actions.push(TransportAction::ForwardToLocalClients {
1134                raw: self.announce_raw_for_local_clients(ctx.packet),
1135                exclude: Some(ctx.iface),
1136            });
1137        }
1138
1139        // Check for discovery path requests waiting for this announce
1140        if let Some(requesting_interfaces) =
1141            self.discovery_path_requests_waiting(&ctx.packet.destination_hash)
1142        {
1143            // Build a path response announce and queue it
1144            let entry = AnnounceEntry {
1145                timestamp: ctx.now,
1146                retransmit_timeout: ctx.now,
1147                retries: constants::PATHFINDER_R,
1148                received_from: ctx.received_from,
1149                hops: ctx.packet.hops,
1150                packet_raw: ctx.packet.raw.clone(),
1151                packet_data: ctx.packet.data().to_vec(),
1152                destination_hash: ctx.packet.destination_hash,
1153                context_flag: ctx.packet.flags.context_flag,
1154                local_rebroadcasts: 0,
1155                block_rebroadcasts: true,
1156                attached_interface: requesting_interfaces.first().copied(),
1157            };
1158            if let Some(identity_hash) = self.config.identity_hash {
1159                let raw = announce_proc::build_retransmit_announce(&entry, &identity_hash);
1160                for interface in requesting_interfaces.iter().skip(1) {
1161                    actions.push(TransportAction::SendOnInterface {
1162                        interface: *interface,
1163                        raw: raw.clone().into(),
1164                    });
1165                }
1166            }
1167            self.insert_announce_entry(ctx.packet.destination_hash, entry, ctx.now);
1168        }
1169    }
1170
1171    pub fn announce_sig_cache_contains(&self, sig_cache_key: &[u8; 32]) -> bool {
1172        self.announce_sig_cache.contains(sig_cache_key)
1173    }
1174
1175    /// Check if there's a waiting discovery path request for a destination.
1176    /// Consumes the request if found (one-shot: the caller queues the announce response).
1177    pub(super) fn discovery_path_requests_waiting(
1178        &mut self,
1179        dest_hash: &[u8; 16],
1180    ) -> Option<Vec<InterfaceId>> {
1181        let request = self
1182            .discovery_path_requests
1183            .remove(dest_hash)
1184            .map(|req| req.requesting_interfaces);
1185        self.discovery_path_request_deadlines.remove(dest_hash);
1186        request
1187    }
1188
1189    // =========================================================================
1190    // Inbound proof processing
1191    // =========================================================================
1192
1193    fn process_inbound_proof(
1194        &mut self,
1195        ctx: &InboundPacketCtx,
1196        actions: &mut Vec<TransportAction>,
1197    ) {
1198        let packet = &ctx.packet;
1199        if packet.context == constants::CONTEXT_LRPROOF {
1200            // Link request proof routing. As in Transport.py, a shared
1201            // instance without transport also routes proofs for links to or
1202            // from its local clients (#183).
1203            let for_local_client_link =
1204                self.link_table
1205                    .get(&packet.destination_hash)
1206                    .is_some_and(|entry| {
1207                        self.interface_is_local_client(entry.next_hop_interface)
1208                            || self.interface_is_local_client(entry.received_interface)
1209                    });
1210            if (self.config.transport_enabled || ctx.from_local_client || for_local_client_link)
1211                && self.link_table.contains_key(&packet.destination_hash)
1212            {
1213                let link_entry = self.link_table.get(&packet.destination_hash).cloned();
1214                if let Some(entry) = link_entry {
1215                    let instance_local_link = self
1216                        .interface_is_local_client(entry.next_hop_interface)
1217                        && self.interface_is_local_client(entry.received_interface);
1218                    if let Some((outbound_interface, new_raw)) = route_via_link_table(
1219                        packet,
1220                        &entry,
1221                        ctx.iface,
1222                        LocalHopRewrite {
1223                            local_hops_delta: self.config.local_hops_delta,
1224                            from_local_client: ctx.from_local_client,
1225                            skip_local_hops_delta: instance_local_link,
1226                        },
1227                    ) {
1228                        // Forward the proof (simplified: skip signature validation
1229                        // which requires Identity recall)
1230
1231                        // Mark link as validated
1232                        if let Some(le) = self.link_table.get_mut(&packet.destination_hash) {
1233                            le.validated = true;
1234                        }
1235
1236                        actions.push(TransportAction::LinkEstablished {
1237                            link_id: packet.destination_hash,
1238                            interface: outbound_interface,
1239                        });
1240
1241                        actions.push(TransportAction::SendOnInterface {
1242                            interface: outbound_interface,
1243                            raw: new_raw.into(),
1244                        });
1245                    } else if link_route_hops_match(packet.hops, &entry, ctx.iface) {
1246                        log::debug!(
1247                            "Link request proof received on wrong interface {}, not transporting it (expected {} or {})",
1248                            ctx.iface.0,
1249                            entry.next_hop_interface.0,
1250                            entry.received_interface.0,
1251                        );
1252                    } else {
1253                        log::debug!("{}", lrproof_hop_mismatch_diagnostic(packet.hops, &entry));
1254                    }
1255                }
1256            } else {
1257                // Could be for a local pending link - deliver locally
1258                let mut delivery_raw = packet.raw.clone();
1259                // LinkManager must see the same post-ingress hop metric that
1260                // was authenticated and considered by transport.
1261                if delivery_raw.len() >= 2 {
1262                    delivery_raw[1] = packet.hops;
1263                }
1264                actions.push(TransportAction::DeliverLocal {
1265                    destination_hash: packet.destination_hash,
1266                    raw: PacketBytes::from(delivery_raw),
1267                    packet_hash: packet.packet_hash,
1268                    receiving_interface: ctx.iface,
1269                });
1270            }
1271        } else {
1272            // Regular proof: check reverse table. As in Transport.py, a shared
1273            // instance without transport also routes proofs to or from its
1274            // local clients (#183).
1275            let proof_for_local_client = self
1276                .reverse_table
1277                .get(&packet.destination_hash)
1278                .is_some_and(|entry| self.interface_is_local_client(entry.receiving_interface));
1279            if self.config.transport_enabled || ctx.from_local_client || proof_for_local_client {
1280                if let Some(reverse_entry) = self.reverse_table.remove(&packet.destination_hash) {
1281                    if let Some(action) = route_proof_via_reverse(
1282                        packet,
1283                        &reverse_entry,
1284                        ctx.iface,
1285                        LocalHopRewrite {
1286                            local_hops_delta: self.config.local_hops_delta,
1287                            from_local_client: ctx.from_local_client,
1288                            skip_local_hops_delta: proof_for_local_client,
1289                        },
1290                    ) {
1291                        actions.push(action);
1292                    }
1293                }
1294            }
1295
1296            // Deliver to local receipts
1297            actions.push(TransportAction::DeliverLocal {
1298                destination_hash: packet.destination_hash,
1299                raw: PacketBytes::from(packet.raw.clone()),
1300                packet_hash: packet.packet_hash,
1301                receiving_interface: ctx.iface,
1302            });
1303        }
1304    }
1305}