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