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