Skip to main content

rns_core/transport/
mod.rs

1pub mod announce_proc;
2pub mod announce_queue;
3pub mod announce_verify_queue;
4pub mod dedup;
5pub mod inbound;
6pub mod ingress_control;
7pub mod jobs;
8pub mod outbound;
9pub mod path_requests;
10pub mod pathfinder;
11pub mod persistence;
12pub mod queries;
13pub mod rate_limit;
14pub mod retention;
15pub mod tables;
16pub mod tunnel;
17pub mod types;
18
19use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
20use alloc::string::String;
21use alloc::vec::Vec;
22use core::mem::size_of;
23
24use rns_crypto::Rng;
25
26use crate::announce::AnnounceData;
27use crate::constants;
28use crate::hash;
29use crate::packet::RawPacket;
30
31use self::announce_proc::compute_path_expires;
32use self::announce_queue::AnnounceQueues;
33use self::announce_verify_queue::{AnnounceVerifyKey, AnnounceVerifyQueue, PendingAnnounce};
34use self::dedup::{AnnounceSignatureCache, PacketHashlist};
35use self::inbound::{
36    create_link_entry, create_reverse_entry, forward_transport_packet, route_proof_via_reverse,
37    route_via_link_table, LocalHopRewrite,
38};
39use self::ingress_control::IngressControl;
40use self::outbound::{route_outbound_with_options, should_transmit_announce, OutboundRouteOptions};
41use self::pathfinder::{
42    extract_random_blob, timebase_from_random_blob, timebase_from_random_blobs, MultiPathDecision,
43};
44use self::rate_limit::AnnounceRateLimiter;
45use self::tables::{AnnounceEntry, DiscoveryPathRequest, LinkEntry, PathEntry, PathSet};
46use self::tunnel::TunnelTable;
47use self::types::{
48    BlackholeEntry, InterfaceId, InterfaceInfo, PacketBytes, TransportAction, TransportConfig,
49};
50
51pub type PathTableRow = ([u8; 16], f64, [u8; 16], u8, f64, String);
52pub type RateTableRow = ([u8; 16], f64, u32, f64, Vec<f64>);
53
54fn lrproof_hop_mismatch_diagnostic(packet_hops: u8, entry: &LinkEntry) -> String {
55    alloc::format!(
56        "Received link request proof with hop mismatch ({}/{}:{}->{}), not transporting it",
57        packet_hops,
58        entry.remaining_hops,
59        entry.next_hop_interface.0,
60        entry.received_interface.0,
61    )
62}
63
64fn link_route_hops_match(
65    packet_hops: u8,
66    entry: &LinkEntry,
67    receiving_interface: InterfaceId,
68) -> bool {
69    if entry.next_hop_interface == entry.received_interface {
70        packet_hops == entry.remaining_hops || packet_hops == entry.taken_hops
71    } else if receiving_interface == entry.next_hop_interface {
72        packet_hops == entry.remaining_hops
73    } else if receiving_interface == entry.received_interface {
74        packet_hops == entry.taken_hops
75    } else {
76        // The routing failure is the interface, not a hop mismatch.
77        true
78    }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Default)]
82pub struct RxMetadata {
83    pub rssi: Option<i16>,
84    pub snr: Option<f32>,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq)]
88pub struct InboundFrame<'a> {
89    pub raw: &'a [u8],
90    pub iface: InterfaceId,
91    pub now: f64,
92    pub rx: RxMetadata,
93}
94
95impl<'a> InboundFrame<'a> {
96    pub fn new(raw: &'a [u8], iface: InterfaceId, now: f64) -> Self {
97        Self {
98            raw,
99            iface,
100            now,
101            rx: RxMetadata::default(),
102        }
103    }
104
105    pub fn with_rx(mut self, rx: RxMetadata) -> Self {
106        self.rx = rx;
107        self
108    }
109}
110
111struct InboundPacketCtx {
112    packet: RawPacket,
113    original_raw: Option<Vec<u8>>,
114    iface: InterfaceId,
115    now: f64,
116    from_local_client: bool,
117}
118
119struct VerifiedAnnounceCtx<'a> {
120    packet: &'a RawPacket,
121    original_raw: &'a [u8],
122    iface: InterfaceId,
123    now: f64,
124    validated: crate::announce::ValidatedAnnounce,
125    received_from: [u8; 16],
126    random_blob: [u8; 10],
127    announce_emitted: u64,
128}
129
130struct TickCtx<'a> {
131    now: f64,
132    rng: &'a mut dyn Rng,
133    actions: Vec<TransportAction>,
134}
135
136struct PathRequestCtx<'a> {
137    tag: &'a [u8],
138    interface_id: InterfaceId,
139    now: f64,
140    destination_hash: [u8; 16],
141}
142
143/// The core transport/routing engine.
144///
145/// Maintains routing tables and processes packets without performing any I/O.
146/// Returns `Vec<TransportAction>` that the caller must execute.
147pub struct TransportEngine {
148    config: TransportConfig,
149    path_table: BTreeMap<[u8; 16], PathSet>,
150    announce_table: BTreeMap<[u8; 16], AnnounceEntry>,
151    reverse_table: BTreeMap<[u8; 16], tables::ReverseEntry>,
152    link_table: BTreeMap<[u8; 16], LinkEntry>,
153    held_announces: BTreeMap<[u8; 16], AnnounceEntry>,
154    packet_hashlist: PacketHashlist,
155    announce_sig_cache: AnnounceSignatureCache,
156    rate_limiter: AnnounceRateLimiter,
157    path_states: BTreeMap<[u8; 16], u8>,
158    interfaces: BTreeMap<InterfaceId, InterfaceInfo>,
159    interface_hashes: BTreeMap<InterfaceId, [u8; 32]>,
160    local_destinations: BTreeMap<[u8; 16], u8>,
161    blackholed_identities: BTreeMap<[u8; 16], BlackholeEntry>,
162    announce_queues: AnnounceQueues,
163    ingress_control: IngressControl,
164    tunnel_table: TunnelTable,
165    discovery_pr_tags: VecDeque<[u8; 32]>,
166    discovery_pr_tag_set: BTreeSet<[u8; 32]>,
167    discovery_path_requests: BTreeMap<[u8; 16], DiscoveryPathRequest>,
168    path_destination_cap_evict_count: usize,
169    // Job timing
170    announces_last_checked: f64,
171    tables_last_culled: f64,
172}
173
174impl TransportEngine {
175    pub fn new(config: TransportConfig) -> Self {
176        let packet_hashlist_max_entries = config.packet_hashlist_max_entries;
177        let packet_hashlist_allocation = config.packet_hashlist_allocation;
178        let sig_cache_max = if config.announce_sig_cache_enabled {
179            config.announce_sig_cache_max_entries
180        } else {
181            0
182        };
183        let sig_cache_ttl = config.announce_sig_cache_ttl_secs;
184        let announce_queue_max_interfaces = config.announce_queue_max_interfaces;
185        TransportEngine {
186            config,
187            path_table: BTreeMap::new(),
188            announce_table: BTreeMap::new(),
189            reverse_table: BTreeMap::new(),
190            link_table: BTreeMap::new(),
191            held_announces: BTreeMap::new(),
192            packet_hashlist: PacketHashlist::with_allocation(
193                packet_hashlist_max_entries,
194                packet_hashlist_allocation,
195            ),
196            announce_sig_cache: AnnounceSignatureCache::new(sig_cache_max, sig_cache_ttl),
197            rate_limiter: AnnounceRateLimiter::new(),
198            path_states: BTreeMap::new(),
199            interfaces: BTreeMap::new(),
200            interface_hashes: BTreeMap::new(),
201            local_destinations: BTreeMap::new(),
202            blackholed_identities: BTreeMap::new(),
203            announce_queues: AnnounceQueues::new(announce_queue_max_interfaces),
204            ingress_control: IngressControl::new(),
205            tunnel_table: TunnelTable::new(),
206            discovery_pr_tags: VecDeque::new(),
207            discovery_pr_tag_set: BTreeSet::new(),
208            discovery_path_requests: BTreeMap::new(),
209            path_destination_cap_evict_count: 0,
210            announces_last_checked: 0.0,
211            tables_last_culled: 0.0,
212        }
213    }
214
215    // =========================================================================
216    // Interface management
217    // =========================================================================
218
219    pub fn register_interface(&mut self, info: InterfaceInfo) {
220        self.interface_hashes
221            .insert(info.id, hash::full_hash(info.name.as_bytes()));
222        self.interfaces.insert(info.id, info);
223    }
224
225    pub fn deregister_interface(&mut self, id: InterfaceId) {
226        self.interfaces.remove(&id);
227        self.interface_hashes.remove(&id);
228        self.drop_paths_for_interface(id);
229        self.drop_reverse_for_interface(id);
230        self.drop_links_for_interface(id);
231        self.announce_queues.remove_interface(id);
232        self.ingress_control.remove_interface(&id);
233    }
234
235    // =========================================================================
236    // Destination management
237    // =========================================================================
238
239    pub fn register_destination(&mut self, dest_hash: [u8; 16], dest_type: u8) {
240        self.local_destinations.insert(dest_hash, dest_type);
241    }
242
243    pub fn deregister_destination(&mut self, dest_hash: &[u8; 16]) {
244        self.local_destinations.remove(dest_hash);
245    }
246
247    // =========================================================================
248    // Path queries
249    // =========================================================================
250
251    pub fn has_path(&self, dest_hash: &[u8; 16]) -> bool {
252        self.path_table
253            .get(dest_hash)
254            .is_some_and(|ps| !ps.is_empty())
255    }
256
257    pub fn hops_to(&self, dest_hash: &[u8; 16]) -> Option<u8> {
258        self.path_table
259            .get(dest_hash)
260            .and_then(|ps| ps.primary())
261            .map(|e| e.hops)
262    }
263
264    pub fn next_hop(&self, dest_hash: &[u8; 16]) -> Option<[u8; 16]> {
265        self.path_table
266            .get(dest_hash)
267            .and_then(|ps| ps.primary())
268            .map(|e| e.next_hop)
269    }
270
271    pub fn next_hop_interface(&self, dest_hash: &[u8; 16]) -> Option<InterfaceId> {
272        self.path_table
273            .get(dest_hash)
274            .and_then(|ps| ps.primary())
275            .map(|e| e.receiving_interface)
276    }
277
278    // =========================================================================
279    // Path state
280    // =========================================================================
281
282    /// Mark a path as unresponsive.
283    ///
284    /// If `receiving_interface` is provided and points to a MODE_BOUNDARY interface,
285    /// the marking is skipped — boundary interfaces must not poison path tables.
286    /// (Python Transport.py: mark_path_unknown/unresponsive boundary exemption)
287    pub fn mark_path_unresponsive(
288        &mut self,
289        dest_hash: &[u8; 16],
290        receiving_interface: Option<InterfaceId>,
291    ) {
292        if let Some(iface_id) = receiving_interface {
293            if let Some(info) = self.interfaces.get(&iface_id) {
294                if info.mode == constants::MODE_BOUNDARY {
295                    return;
296                }
297            }
298        }
299
300        // Failover: if we have alternative paths, promote the next one
301        if let Some(ps) = self.path_table.get_mut(dest_hash) {
302            if ps.len() > 1 {
303                ps.failover(false); // demote old primary to back
304                                    // Clear unresponsive state since we promoted a fresh primary
305                self.path_states.remove(dest_hash);
306                return;
307            }
308        }
309
310        self.path_states
311            .insert(*dest_hash, constants::STATE_UNRESPONSIVE);
312    }
313
314    pub fn mark_path_responsive(&mut self, dest_hash: &[u8; 16]) {
315        self.path_states
316            .insert(*dest_hash, constants::STATE_RESPONSIVE);
317    }
318
319    pub fn path_is_unresponsive(&self, dest_hash: &[u8; 16]) -> bool {
320        self.path_states.get(dest_hash) == Some(&constants::STATE_UNRESPONSIVE)
321    }
322
323    pub fn expire_path(&mut self, dest_hash: &[u8; 16]) {
324        if let Some(ps) = self.path_table.get_mut(dest_hash) {
325            ps.expire_all();
326        }
327    }
328
329    // =========================================================================
330    // Link table
331    // =========================================================================
332
333    pub fn register_link(&mut self, link_id: [u8; 16], entry: LinkEntry) {
334        self.link_table.insert(link_id, entry);
335    }
336
337    pub fn validate_link(&mut self, link_id: &[u8; 16]) {
338        if let Some(entry) = self.link_table.get_mut(link_id) {
339            entry.validated = true;
340        }
341    }
342
343    pub fn remove_link(&mut self, link_id: &[u8; 16]) {
344        self.link_table.remove(link_id);
345    }
346
347    /// Return the destination whose unvalidated link route can be rebalanced
348    /// by a mismatched-hop LRPROOF received from its recorded next hop.
349    pub fn link_rebalance_destination(
350        &self,
351        link_id: &[u8; 16],
352        packet_hops: u8,
353        receiving_interface: InterfaceId,
354    ) -> Option<[u8; 16]> {
355        let entry = self.link_table.get(link_id)?;
356        (self.config.transport_enabled
357            && !entry.validated
358            && packet_hops != entry.remaining_hops
359            && receiving_interface == entry.next_hop_interface)
360            .then_some(entry.destination_hash)
361    }
362
363    /// Parse and filter an inbound frame before offering it for LRPROOF path
364    /// rebalancing. The returned hops are the post-ingress metric used by the
365    /// normal transport pipeline.
366    pub fn inbound_lrproof_rebalance_candidate(
367        &self,
368        raw: &[u8],
369        receiving_interface: InterfaceId,
370    ) -> Option<([u8; 16], [u8; 16], u8, Vec<u8>)> {
371        let ctx = self.prepare_inbound_packet(InboundFrame {
372            raw,
373            iface: receiving_interface,
374            now: 0.0,
375            rx: RxMetadata::default(),
376        })?;
377        if ctx.packet.flags.packet_type != constants::PACKET_TYPE_PROOF
378            || ctx.packet.context != constants::CONTEXT_LRPROOF
379        {
380            return None;
381        }
382        let link_id = ctx.packet.destination_hash;
383        let destination_hash =
384            self.link_rebalance_destination(&link_id, ctx.packet.hops, receiving_interface)?;
385        Some((link_id, destination_hash, ctx.packet.hops, ctx.packet.data))
386    }
387
388    /// Validate a mismatched-hop LRPROOF and update the relay link route and
389    /// destination path atomically enough for normal proof routing to resume.
390    pub fn rebalance_link_path_from_lrproof(
391        &mut self,
392        link_id: &[u8; 16],
393        packet_hops: u8,
394        receiving_interface: InterfaceId,
395        proof_data: &[u8],
396        destination_sig_pub_bytes: &[u8; 32],
397    ) -> bool {
398        if self
399            .link_rebalance_destination(link_id, packet_hops, receiving_interface)
400            .is_none()
401        {
402            return false;
403        }
404
405        let destination_sig_pub =
406            rns_crypto::ed25519::Ed25519PublicKey::from_bytes(destination_sig_pub_bytes);
407        if crate::link::handshake::validate_lrproof(
408            proof_data,
409            link_id,
410            &destination_sig_pub,
411            destination_sig_pub_bytes,
412        )
413        .is_err()
414        {
415            return false;
416        }
417
418        let destination_hash = match self.link_table.get_mut(link_id) {
419            Some(entry) if !entry.validated => {
420                entry.remaining_hops = packet_hops;
421                entry.destination_hash
422            }
423            _ => return false,
424        };
425        if let Some(paths) = self.path_table.get_mut(&destination_hash) {
426            paths.update_primary_hops(packet_hops);
427        }
428        true
429    }
430
431    /// Update the current path metric after a terminus validates an LRPROOF.
432    pub fn rebalance_destination_path_hops(
433        &mut self,
434        destination_hash: &[u8; 16],
435        packet_hops: u8,
436    ) -> bool {
437        self.path_table
438            .get_mut(destination_hash)
439            .is_some_and(|paths| paths.update_primary_hops(packet_hops))
440    }
441
442    // =========================================================================
443    // Blackhole management
444    // =========================================================================
445
446    /// Add an identity hash to the blackhole list.
447    ///
448    /// `identity_hash` is the 16-byte identity hash to blackhole. `now` is the
449    /// current Unix timestamp. If `duration_hours` is `Some` and greater than
450    /// zero, the entry expires after that many hours; otherwise it does not
451    /// expire. `reason` is optional descriptive text retained with the entry.
452    pub fn blackhole_identity(
453        &mut self,
454        identity_hash: [u8; 16],
455        now: f64,
456        duration_hours: Option<f64>,
457        reason: Option<String>,
458    ) {
459        let expires = match duration_hours {
460            Some(h) if h > 0.0 => now + h * 3600.0,
461            _ => 0.0, // never expires
462        };
463        self.blackholed_identities.insert(
464            identity_hash,
465            BlackholeEntry {
466                created: now,
467                expires,
468                reason,
469            },
470        );
471    }
472
473    /// Remove an identity hash from the blackhole list.
474    ///
475    /// Returns `true` if an entry was removed, or `false` if the identity was
476    /// not blackholed.
477    pub fn unblackhole_identity(&mut self, identity_hash: &[u8; 16]) -> bool {
478        self.blackholed_identities.remove(identity_hash).is_some()
479    }
480
481    /// Check if an identity hash is blackholed (and not expired).
482    pub fn is_blackholed(&self, identity_hash: &[u8; 16], now: f64) -> bool {
483        if let Some(entry) = self.blackholed_identities.get(identity_hash) {
484            if entry.expires == 0.0 || entry.expires > now {
485                return true;
486            }
487        }
488        false
489    }
490
491    /// Get all blackhole entries (for queries).
492    pub fn blackholed_entries(&self) -> impl Iterator<Item = (&[u8; 16], &BlackholeEntry)> {
493        self.blackholed_identities.iter()
494    }
495
496    /// Cull expired blackhole entries.
497    fn cull_blackholed(&mut self, now: f64) {
498        self.blackholed_identities
499            .retain(|_, entry| entry.expires == 0.0 || entry.expires > now);
500    }
501
502    // =========================================================================
503    // Tunnel management
504    // =========================================================================
505
506    /// Handle a validated tunnel synthesis — create new or reattach.
507    ///
508    /// Returns actions for any restored paths.
509    pub fn handle_tunnel(
510        &mut self,
511        tunnel_id: [u8; 32],
512        interface: InterfaceId,
513        now: f64,
514    ) -> Vec<TransportAction> {
515        let mut actions = Vec::new();
516        let reattaching = self.tunnel_table.get(&tunnel_id).is_some();
517        if reattaching {
518            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
519                "Tunnel endpoint {:02x?} reappeared on interface {}; restoring paths",
520                &tunnel_id[..4],
521                interface.0,
522            );
523        } else {
524            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
525                "Tunnel endpoint {:02x?} established on interface {}",
526                &tunnel_id[..4],
527                interface.0,
528            );
529        }
530
531        // Set tunnel_id on the interface
532        if let Some(info) = self.interfaces.get_mut(&interface) {
533            info.tunnel_id = Some(tunnel_id);
534        }
535
536        let restored_paths = self.tunnel_table.handle_tunnel(
537            tunnel_id,
538            interface,
539            now,
540            self.config.destination_timeout_secs,
541        );
542
543        // Restore paths to path table if they're better than existing
544        for (dest_hash, tunnel_path) in &restored_paths {
545            let should_restore = match self.path_table.get(dest_hash).and_then(|ps| ps.primary()) {
546                Some(existing) => {
547                    // Restore if fewer/equal hops or existing expired, but never
548                    // overwrite a path learned from a more recent announce.
549                    if tunnel_path.hops <= existing.hops || existing.expires < now {
550                        let existing_timebase = timebase_from_random_blobs(&existing.random_blobs);
551                        let tunnel_timebase = timebase_from_random_blobs(&tunnel_path.random_blobs);
552                        tunnel_timebase >= existing_timebase
553                    } else {
554                        false
555                    }
556                }
557                None => now < tunnel_path.expires,
558            };
559
560            if should_restore {
561                let entry = PathEntry {
562                    timestamp: tunnel_path.timestamp,
563                    next_hop: tunnel_path.received_from,
564                    hops: tunnel_path.hops,
565                    expires: tunnel_path.expires,
566                    random_blobs: tunnel_path.random_blobs.clone(),
567                    receiving_interface: interface,
568                    packet_hash: tunnel_path.packet_hash,
569                    announce_raw: None,
570                };
571                self.upsert_path_destination(*dest_hash, entry, now);
572                log::trace!(target: crate::logging::PATHING_LOG_TARGET,
573                    "Restored tunnel path to {:02x?}: hops={} via={:02x?} interface={}",
574                    &dest_hash[..4],
575                    tunnel_path.hops,
576                    &tunnel_path.received_from[..4],
577                    interface.0,
578                );
579            } else {
580                log::trace!(target: crate::logging::PATHING_LOG_TARGET,
581                    "Did not restore tunnel path to {:02x?}: existing path is preferred or tunnel path expired",
582                    &dest_hash[..4],
583                );
584            }
585        }
586
587        actions.push(TransportAction::TunnelEstablished {
588            tunnel_id,
589            interface,
590        });
591
592        actions
593    }
594
595    /// Synthesize a tunnel on an interface.
596    ///
597    /// `identity`: the transport identity (must have private key for signing)
598    /// `interface_id`: which interface to send the synthesis on
599    /// `rng`: random number generator
600    ///
601    /// Returns TunnelSynthesize action to send the synthesis packet.
602    pub fn synthesize_tunnel(
603        &self,
604        identity: &rns_crypto::identity::Identity,
605        interface_id: InterfaceId,
606        rng: &mut dyn Rng,
607    ) -> Vec<TransportAction> {
608        let mut actions = Vec::new();
609
610        let interface_hash = if let Some(interface_hash) = self.interface_hashes.get(&interface_id)
611        {
612            *interface_hash
613        } else {
614            log::warn!(
615                "Cannot synthesize tunnel on {:?}: unknown interface or missing cached hash",
616                interface_id
617            );
618            return actions;
619        };
620
621        match tunnel::build_tunnel_synthesize_data(identity, &interface_hash, rng) {
622            Ok((data, _tunnel_id)) => {
623                let dest_hash = crate::destination::destination_hash(
624                    "rnstransport",
625                    &["tunnel", "synthesize"],
626                    None,
627                );
628                actions.push(TransportAction::TunnelSynthesize {
629                    interface: interface_id,
630                    data,
631                    dest_hash,
632                });
633            }
634            Err(e) => {
635                log::warn!("Cannot synthesize tunnel on {:?}: {}", interface_id, e);
636            }
637        }
638
639        actions
640    }
641
642    /// Void a tunnel's interface connection (tunnel disconnected).
643    pub fn void_tunnel_interface(&mut self, tunnel_id: &[u8; 32]) {
644        self.tunnel_table.void_tunnel_interface(tunnel_id);
645    }
646
647    /// Access the tunnel table for queries.
648    pub fn tunnel_table(&self) -> &TunnelTable {
649        &self.tunnel_table
650    }
651
652    // =========================================================================
653    // Packet filter
654    // =========================================================================
655
656    /// Check if any local client interfaces are registered.
657    fn has_local_clients(&self) -> bool {
658        self.interfaces.values().any(|i| i.is_local_client)
659    }
660
661    fn interface_is_local_client(&self, iface: InterfaceId) -> bool {
662        self.interfaces
663            .get(&iface)
664            .map(|i| i.is_local_client)
665            .unwrap_or(false)
666    }
667
668    /// Packet filter: dedup + basic validity.
669    ///
670    /// Transport.py:1187-1238
671    fn packet_filter(&self, packet: &RawPacket) -> bool {
672        // Filter packets for other transport instances
673        if packet.transport_id.is_some()
674            && packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
675        {
676            if let Some(ref identity_hash) = self.config.identity_hash {
677                if packet.transport_id.as_ref() != Some(identity_hash) {
678                    return false;
679                }
680            }
681        }
682
683        // Allow certain contexts unconditionally
684        match packet.context {
685            constants::CONTEXT_KEEPALIVE
686            | constants::CONTEXT_RESOURCE_REQ
687            | constants::CONTEXT_RESOURCE_PRF
688            | constants::CONTEXT_RESOURCE
689            | constants::CONTEXT_CACHE_REQUEST
690            | constants::CONTEXT_CHANNEL => return true,
691            _ => {}
692        }
693
694        // PLAIN/GROUP checks
695        if packet.flags.destination_type == constants::DESTINATION_PLAIN
696            || packet.flags.destination_type == constants::DESTINATION_GROUP
697        {
698            if packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE {
699                return packet.hops <= 1;
700            } else {
701                // PLAIN/GROUP ANNOUNCE is invalid
702                return false;
703            }
704        }
705
706        // Deduplication
707        if !self.packet_hashlist.is_duplicate(&packet.packet_hash) {
708            return true;
709        }
710
711        // Duplicate announce for SINGLE dest is allowed (path update)
712        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
713            && packet.flags.destination_type == constants::DESTINATION_SINGLE
714        {
715            return true;
716        }
717
718        false
719    }
720
721    // =========================================================================
722    // Core API: handle_inbound
723    // =========================================================================
724
725    /// Process an inbound raw packet from a network interface.
726    ///
727    /// Returns a list of actions for the caller to execute.
728    pub fn handle_inbound(
729        &mut self,
730        frame: InboundFrame<'_>,
731        rng: &mut dyn Rng,
732    ) -> Vec<TransportAction> {
733        self.handle_inbound_with_announce_queue(frame, rng, None)
734    }
735
736    pub fn handle_inbound_with_announce_queue(
737        &mut self,
738        frame: InboundFrame<'_>,
739        rng: &mut dyn Rng,
740        announce_queue: Option<&mut AnnounceVerifyQueue>,
741    ) -> Vec<TransportAction> {
742        let Some(ctx) = self.prepare_inbound_packet(frame) else {
743            return Vec::new();
744        };
745        let mut actions = Vec::new();
746
747        self.remember_inbound_packet_hash(&ctx.packet);
748        self.bridge_plain_broadcast(&ctx, &mut actions);
749        self.handle_transport_forwarding(&ctx, &mut actions);
750        self.handle_link_table_routing(&ctx, &mut actions);
751        self.handle_inbound_announce(&ctx, rng, announce_queue, &mut actions);
752
753        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_PROOF {
754            self.process_inbound_proof(&ctx, &mut actions);
755        }
756
757        self.handle_inbound_local_delivery(&ctx, &mut actions);
758        actions
759    }
760
761    fn prepare_inbound_packet(&self, frame: InboundFrame<'_>) -> Option<InboundPacketCtx> {
762        let mut packet = RawPacket::unpack(frame.raw).ok()?;
763        let from_local_client = self
764            .interfaces
765            .get(&frame.iface)
766            .map(|i| i.is_local_client)
767            .unwrap_or(false);
768        packet.hops = packet.hops.checked_add(1)?;
769        packet.rssi = frame.rx.rssi;
770        packet.snr = frame.rx.snr;
771        if from_local_client {
772            packet.hops = packet.hops.saturating_sub(1);
773        }
774        if !self.packet_filter(&packet) {
775            return None;
776        }
777        let retain_original_raw = packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE;
778        Some(InboundPacketCtx {
779            packet,
780            original_raw: if retain_original_raw {
781                Some(frame.raw.to_vec())
782            } else {
783                None
784            },
785            iface: frame.iface,
786            now: frame.now,
787            from_local_client,
788        })
789    }
790
791    fn remember_inbound_packet_hash(&mut self, packet: &RawPacket) {
792        let remember_hash = !(self.link_table.contains_key(&packet.destination_hash)
793            || (packet.flags.packet_type == constants::PACKET_TYPE_PROOF
794                && packet.context == constants::CONTEXT_LRPROOF));
795        if remember_hash {
796            self.packet_hashlist.add(packet.packet_hash);
797        }
798    }
799
800    fn bridge_plain_broadcast(&self, ctx: &InboundPacketCtx, actions: &mut Vec<TransportAction>) {
801        if ctx.packet.flags.destination_type != constants::DESTINATION_PLAIN
802            || ctx.packet.flags.transport_type != constants::TRANSPORT_BROADCAST
803            || !self.has_local_clients()
804        {
805            return;
806        }
807
808        if ctx.from_local_client {
809            actions.push(TransportAction::ForwardPlainBroadcast {
810                raw: PacketBytes::from(ctx.packet.raw.clone()),
811                to_local: false,
812                exclude: Some(ctx.iface),
813            });
814        } else {
815            actions.push(TransportAction::ForwardPlainBroadcast {
816                raw: PacketBytes::from(ctx.packet.raw.clone()),
817                to_local: true,
818                exclude: None,
819            });
820        }
821    }
822
823    fn handle_transport_forwarding(
824        &mut self,
825        ctx: &InboundPacketCtx,
826        actions: &mut Vec<TransportAction>,
827    ) {
828        if !(self.config.transport_enabled || self.config.identity_hash.is_some()) {
829            return;
830        }
831        if ctx.packet.transport_id.is_none()
832            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
833        {
834            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
835                log::debug!(
836                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. not transport-addressed header={} iface={}",
837                    ctx.packet.destination_hash[0],
838                    ctx.packet.destination_hash[1],
839                    ctx.packet.destination_hash[2],
840                    ctx.packet.destination_hash[3],
841                    ctx.packet.flags.header_type,
842                    ctx.iface.0
843                );
844            }
845            return;
846        }
847
848        let Some(identity_hash) = self.config.identity_hash else {
849            return;
850        };
851        if ctx.packet.transport_id != Some(identity_hash) {
852            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
853                log::debug!(
854                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. transport mismatch got={:02x?} own={:02x?} iface={}",
855                    ctx.packet.destination_hash[0],
856                    ctx.packet.destination_hash[1],
857                    ctx.packet.destination_hash[2],
858                    ctx.packet.destination_hash[3],
859                    ctx.packet.transport_id.as_ref().map(|id| &id[..4]),
860                    &identity_hash[..4],
861                    ctx.iface.0
862                );
863            }
864            return;
865        }
866
867        let Some(path_entry) = self
868            .path_table
869            .get(&ctx.packet.destination_hash)
870            .and_then(|ps| ps.primary())
871        else {
872            if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
873                log::debug!(
874                    "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. addressed to us but no path iface={}",
875                    ctx.packet.destination_hash[0],
876                    ctx.packet.destination_hash[1],
877                    ctx.packet.destination_hash[2],
878                    ctx.packet.destination_hash[3],
879                    ctx.iface.0
880                );
881            }
882            return;
883        };
884
885        let next_hop = path_entry.next_hop;
886        let remaining_hops = path_entry.hops;
887        let outbound_interface = path_entry.receiving_interface;
888        let outbound_is_local_client = self
889            .interfaces
890            .get(&outbound_interface)
891            .map(|info| info.is_local_client)
892            .unwrap_or(false);
893        let forwarded_remaining_hops = if outbound_is_local_client {
894            0
895        } else {
896            remaining_hops
897        };
898        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA {
899            log::debug!(
900                "TransportForward: DATA dest={:02x}{:02x}{:02x}{:02x}.. remaining_hops={} out_iface={} next_hop={:02x?}",
901                ctx.packet.destination_hash[0],
902                ctx.packet.destination_hash[1],
903                ctx.packet.destination_hash[2],
904                ctx.packet.destination_hash[3],
905                remaining_hops,
906                outbound_interface.0,
907                &next_hop[..4]
908            );
909        }
910        let mut new_raw = forward_transport_packet(
911            &ctx.packet,
912            next_hop,
913            forwarded_remaining_hops,
914            outbound_interface,
915        );
916        if self.config.local_hops_delta != 0
917            && ctx.from_local_client
918            && !outbound_is_local_client
919            && ctx.packet.hops == 0
920            && ctx.packet.flags.destination_type != constants::DESTINATION_PLAIN
921            && ctx.packet.flags.destination_type != constants::DESTINATION_GROUP
922            && new_raw.len() > 1
923        {
924            new_raw[1] = self.config.local_hops_delta;
925        }
926
927        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST {
928            let proof_timeout = ctx.now
929                + constants::LINK_ESTABLISHMENT_TIMEOUT_PER_HOP * (remaining_hops.max(1) as f64);
930            let (link_id, link_entry) = create_link_entry(
931                &ctx.packet,
932                next_hop,
933                outbound_interface,
934                remaining_hops,
935                ctx.iface,
936                ctx.now,
937                proof_timeout,
938            );
939            self.link_table.insert(link_id, link_entry);
940            actions.push(TransportAction::LinkRequestReceived {
941                link_id,
942                destination_hash: ctx.packet.destination_hash,
943                receiving_interface: ctx.iface,
944            });
945        } else {
946            let (trunc_hash, reverse_entry) =
947                create_reverse_entry(&ctx.packet, outbound_interface, ctx.iface, ctx.now);
948            self.reverse_table.insert(trunc_hash, reverse_entry);
949        }
950
951        actions.push(TransportAction::SendOnInterface {
952            interface: outbound_interface,
953            raw: new_raw.into(),
954        });
955
956        if let Some(entry) = self
957            .path_table
958            .get_mut(&ctx.packet.destination_hash)
959            .and_then(|ps| ps.primary_mut())
960        {
961            entry.timestamp = ctx.now;
962        }
963    }
964
965    fn handle_link_table_routing(
966        &mut self,
967        ctx: &InboundPacketCtx,
968        actions: &mut Vec<TransportAction>,
969    ) {
970        if !self.config.transport_enabled && self.config.identity_hash.is_none() {
971            return;
972        }
973        if ctx.packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
974            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
975            || ctx.packet.context == constants::CONTEXT_LRPROOF
976        {
977            return;
978        }
979
980        let Some(link_entry) = self.link_table.get(&ctx.packet.destination_hash).cloned() else {
981            return;
982        };
983        let instance_local_link = self.interface_is_local_client(link_entry.next_hop_interface)
984            && self.interface_is_local_client(link_entry.received_interface);
985        let Some((outbound_iface, new_raw)) = route_via_link_table(
986            &ctx.packet,
987            &link_entry,
988            ctx.iface,
989            LocalHopRewrite {
990                local_hops_delta: self.config.local_hops_delta,
991                from_local_client: ctx.from_local_client,
992                skip_local_hops_delta: instance_local_link,
993            },
994        ) else {
995            return;
996        };
997
998        self.packet_hashlist.add(ctx.packet.packet_hash);
999        actions.push(TransportAction::SendOnInterface {
1000            interface: outbound_iface,
1001            raw: new_raw.into(),
1002        });
1003
1004        if let Some(entry) = self.link_table.get_mut(&ctx.packet.destination_hash) {
1005            entry.timestamp = ctx.now;
1006        }
1007    }
1008
1009    fn handle_inbound_announce(
1010        &mut self,
1011        ctx: &InboundPacketCtx,
1012        rng: &mut dyn Rng,
1013        announce_queue: Option<&mut AnnounceVerifyQueue>,
1014        actions: &mut Vec<TransportAction>,
1015    ) {
1016        if ctx.packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE {
1017            return;
1018        }
1019
1020        if let Some(queue) = announce_queue {
1021            self.try_enqueue_announce(ctx, rng, queue, actions);
1022        } else {
1023            let original_raw = ctx
1024                .original_raw
1025                .as_deref()
1026                .expect("announce packets retain original raw bytes");
1027            self.process_inbound_announce(
1028                &ctx.packet,
1029                original_raw,
1030                ctx.iface,
1031                ctx.now,
1032                rng,
1033                actions,
1034            );
1035        }
1036    }
1037
1038    fn handle_inbound_local_delivery(
1039        &self,
1040        ctx: &InboundPacketCtx,
1041        actions: &mut Vec<TransportAction>,
1042    ) {
1043        if (ctx.packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
1044            || ctx.packet.flags.packet_type == constants::PACKET_TYPE_DATA)
1045            && self
1046                .local_destinations
1047                .contains_key(&ctx.packet.destination_hash)
1048        {
1049            let mut delivery_raw = ctx.packet.raw.clone();
1050            // Link responders learn the post-ingress hop metric from the
1051            // authenticated LRRTT packet. Preserve that field when crossing
1052            // the action boundary without changing ordinary callback bytes.
1053            if ctx.packet.context == constants::CONTEXT_LRRTT && delivery_raw.len() >= 2 {
1054                delivery_raw[1] = ctx.packet.hops;
1055            }
1056            actions.push(TransportAction::DeliverLocal {
1057                destination_hash: ctx.packet.destination_hash,
1058                raw: PacketBytes::from(delivery_raw),
1059                packet_hash: ctx.packet.packet_hash,
1060                receiving_interface: ctx.iface,
1061            });
1062        }
1063    }
1064
1065    // =========================================================================
1066    // Inbound announce processing
1067    // =========================================================================
1068
1069    fn process_inbound_announce(
1070        &mut self,
1071        packet: &RawPacket,
1072        original_raw: &[u8],
1073        iface: InterfaceId,
1074        now: f64,
1075        rng: &mut dyn Rng,
1076        actions: &mut Vec<TransportAction>,
1077    ) {
1078        if packet.flags.destination_type != constants::DESTINATION_SINGLE {
1079            return;
1080        }
1081
1082        let has_ratchet = packet.flags.context_flag == constants::FLAG_SET;
1083
1084        // Unpack and validate announce
1085        let announce = match AnnounceData::unpack(&packet.data, has_ratchet) {
1086            Ok(a) => a,
1087            Err(_) => return,
1088        };
1089
1090        if self.should_hold_announce(packet, original_raw, iface, now) {
1091            return;
1092        }
1093
1094        let sig_cache_key =
1095            Self::announce_sig_cache_key(packet.destination_hash, &announce.signature);
1096
1097        let validated = if self.announce_sig_cache.contains(&sig_cache_key) {
1098            announce.to_validated_unchecked()
1099        } else {
1100            match announce.validate(&packet.destination_hash) {
1101                Ok(v) => {
1102                    self.announce_sig_cache.insert(sig_cache_key, now);
1103                    v
1104                }
1105                Err(_) => return,
1106            }
1107        };
1108
1109        let received_from = self.announce_received_from(packet, now);
1110        let random_blob = match extract_random_blob(&packet.data) {
1111            Some(b) => b,
1112            None => return,
1113        };
1114        let announce_emitted = timebase_from_random_blob(&random_blob);
1115
1116        self.process_verified_announce(
1117            VerifiedAnnounceCtx {
1118                packet,
1119                original_raw,
1120                iface,
1121                now,
1122                validated,
1123                received_from,
1124                random_blob,
1125                announce_emitted,
1126            },
1127            rng,
1128            actions,
1129        );
1130    }
1131
1132    fn announce_raw_for_local_clients(&self, packet: &RawPacket) -> PacketBytes {
1133        let Some(identity_hash) = self.config.identity_hash else {
1134            return PacketBytes::from(packet.raw.clone());
1135        };
1136
1137        if packet.raw.len() < 2 {
1138            return PacketBytes::from(packet.raw.clone());
1139        }
1140
1141        let payload_start = if packet.flags.header_type == constants::HEADER_2 {
1142            18usize
1143        } else {
1144            2usize
1145        };
1146        if packet.raw.len() < payload_start {
1147            return PacketBytes::from(packet.raw.clone());
1148        }
1149
1150        let flags = (constants::HEADER_2 << 6)
1151            | (constants::TRANSPORT_TRANSPORT << 4)
1152            | (packet.raw[0] & 0x0F);
1153        let mut raw = Vec::with_capacity(18 + packet.raw.len() - payload_start);
1154        raw.push(flags);
1155        raw.push(packet.hops);
1156        raw.extend_from_slice(&identity_hash);
1157        raw.extend_from_slice(&packet.raw[payload_start..]);
1158        PacketBytes::from(raw)
1159    }
1160
1161    fn announce_sig_cache_key(destination_hash: [u8; 16], signature: &[u8; 64]) -> [u8; 32] {
1162        let mut material = [0u8; 80];
1163        material[..16].copy_from_slice(&destination_hash);
1164        material[16..].copy_from_slice(signature);
1165        hash::full_hash(&material)
1166    }
1167
1168    fn announce_received_from(&mut self, packet: &RawPacket, now: f64) -> [u8; 16] {
1169        if let Some(transport_id) = packet.transport_id {
1170            if self.config.transport_enabled {
1171                if let Some(announce_entry) = self.announce_table.get_mut(&packet.destination_hash)
1172                {
1173                    if packet.hops.checked_sub(1) == Some(announce_entry.hops) {
1174                        announce_entry.local_rebroadcasts += 1;
1175                        if announce_entry.retries > 0
1176                            && announce_entry.local_rebroadcasts
1177                                >= constants::LOCAL_REBROADCASTS_MAX
1178                        {
1179                            self.announce_table.remove(&packet.destination_hash);
1180                        }
1181                    }
1182                    if let Some(announce_entry) = self.announce_table.get(&packet.destination_hash)
1183                    {
1184                        if packet.hops.checked_sub(1) == Some(announce_entry.hops + 1)
1185                            && announce_entry.retries > 0
1186                            && now < announce_entry.retransmit_timeout
1187                        {
1188                            self.announce_table.remove(&packet.destination_hash);
1189                        }
1190                    }
1191                }
1192            }
1193            transport_id
1194        } else {
1195            packet.destination_hash
1196        }
1197    }
1198
1199    fn should_hold_announce(
1200        &mut self,
1201        packet: &RawPacket,
1202        original_raw: &[u8],
1203        iface: InterfaceId,
1204        now: f64,
1205    ) -> bool {
1206        if self.has_path(&packet.destination_hash) {
1207            return false;
1208        }
1209        if self
1210            .discovery_path_requests
1211            .contains_key(&packet.destination_hash)
1212        {
1213            return false;
1214        }
1215        let Some(info) = self.interfaces.get(&iface) else {
1216            return false;
1217        };
1218        if packet.context == constants::CONTEXT_PATH_RESPONSE
1219            || !self.ingress_control.should_ingress_limit(
1220                iface,
1221                &info.ingress_control,
1222                info.ia_freq,
1223                info.started,
1224                now,
1225            )
1226        {
1227            return false;
1228        }
1229        self.ingress_control.hold_announce(
1230            iface,
1231            &info.ingress_control,
1232            packet.destination_hash,
1233            ingress_control::HeldAnnounce {
1234                raw: original_raw.to_vec(),
1235                hops: packet.hops,
1236                receiving_interface: iface,
1237                rx: RxMetadata {
1238                    rssi: packet.rssi,
1239                    snr: packet.snr,
1240                },
1241                timestamp: now,
1242            },
1243        );
1244        true
1245    }
1246
1247    fn try_enqueue_announce(
1248        &mut self,
1249        ctx: &InboundPacketCtx,
1250        rng: &mut dyn Rng,
1251        announce_queue: &mut AnnounceVerifyQueue,
1252        actions: &mut Vec<TransportAction>,
1253    ) {
1254        if ctx.packet.flags.destination_type != constants::DESTINATION_SINGLE {
1255            return;
1256        }
1257
1258        let has_ratchet = ctx.packet.flags.context_flag == constants::FLAG_SET;
1259        let announce = match AnnounceData::unpack(&ctx.packet.data, has_ratchet) {
1260            Ok(a) => a,
1261            Err(_) => return,
1262        };
1263
1264        let received_from = self.announce_received_from(&ctx.packet, ctx.now);
1265
1266        if self
1267            .local_destinations
1268            .contains_key(&ctx.packet.destination_hash)
1269        {
1270            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
1271                "Announce:skipping local destination {:02x}{:02x}{:02x}{:02x}..",
1272                ctx.packet.destination_hash[0],
1273                ctx.packet.destination_hash[1],
1274                ctx.packet.destination_hash[2],
1275                ctx.packet.destination_hash[3],
1276            );
1277            return;
1278        }
1279
1280        let original_raw = ctx
1281            .original_raw
1282            .as_deref()
1283            .expect("announce packets retain original raw bytes");
1284        if self.should_hold_announce(&ctx.packet, original_raw, ctx.iface, ctx.now) {
1285            return;
1286        }
1287
1288        let sig_cache_key =
1289            Self::announce_sig_cache_key(ctx.packet.destination_hash, &announce.signature);
1290        if self.announce_sig_cache.contains(&sig_cache_key) {
1291            let validated = announce.to_validated_unchecked();
1292            let random_blob = match extract_random_blob(&ctx.packet.data) {
1293                Some(b) => b,
1294                None => return,
1295            };
1296            let announce_emitted = timebase_from_random_blob(&random_blob);
1297            self.process_verified_announce(
1298                VerifiedAnnounceCtx {
1299                    packet: &ctx.packet,
1300                    original_raw,
1301                    iface: ctx.iface,
1302                    now: ctx.now,
1303                    validated,
1304                    received_from,
1305                    random_blob,
1306                    announce_emitted,
1307                },
1308                rng,
1309                actions,
1310            );
1311            return;
1312        }
1313
1314        if ctx.packet.context == constants::CONTEXT_PATH_RESPONSE {
1315            let Ok(validated) = announce.validate(&ctx.packet.destination_hash) else {
1316                return;
1317            };
1318            self.announce_sig_cache.insert(sig_cache_key, ctx.now);
1319            let random_blob = match extract_random_blob(&ctx.packet.data) {
1320                Some(b) => b,
1321                None => return,
1322            };
1323            let announce_emitted = timebase_from_random_blob(&random_blob);
1324            self.process_verified_announce(
1325                VerifiedAnnounceCtx {
1326                    packet: &ctx.packet,
1327                    original_raw,
1328                    iface: ctx.iface,
1329                    now: ctx.now,
1330                    validated,
1331                    received_from,
1332                    random_blob,
1333                    announce_emitted,
1334                },
1335                rng,
1336                actions,
1337            );
1338            return;
1339        }
1340
1341        let random_blob = match extract_random_blob(&ctx.packet.data) {
1342            Some(b) => b,
1343            None => return,
1344        };
1345        let announce_emitted = timebase_from_random_blob(&random_blob);
1346        let key = AnnounceVerifyKey {
1347            destination_hash: ctx.packet.destination_hash,
1348            random_blob,
1349            received_from,
1350        };
1351        let pending = PendingAnnounce {
1352            original_raw: original_raw.to_vec(),
1353            packet: ctx.packet.clone(),
1354            interface: ctx.iface,
1355            received_from,
1356            queued_at: ctx.now,
1357            best_hops: ctx.packet.hops,
1358            emission_ts: announce_emitted,
1359            random_blob,
1360        };
1361        let _ = announce_queue.enqueue(key, pending);
1362    }
1363
1364    pub fn complete_verified_announce(
1365        &mut self,
1366        pending: PendingAnnounce,
1367        validated: crate::announce::ValidatedAnnounce,
1368        sig_cache_key: [u8; 32],
1369        now: f64,
1370        rng: &mut dyn Rng,
1371    ) -> Vec<TransportAction> {
1372        self.announce_sig_cache.insert(sig_cache_key, now);
1373        let mut actions = Vec::new();
1374        self.process_verified_announce(
1375            VerifiedAnnounceCtx {
1376                packet: &pending.packet,
1377                original_raw: &pending.original_raw,
1378                iface: pending.interface,
1379                now,
1380                validated,
1381                received_from: pending.received_from,
1382                random_blob: pending.random_blob,
1383                announce_emitted: pending.emission_ts,
1384            },
1385            rng,
1386            &mut actions,
1387        );
1388        actions
1389    }
1390
1391    pub fn clear_failed_verified_announce(&mut self, _sig_cache_key: [u8; 32], _now: f64) {}
1392
1393    fn process_verified_announce(
1394        &mut self,
1395        ctx: VerifiedAnnounceCtx<'_>,
1396        rng: &mut dyn Rng,
1397        actions: &mut Vec<TransportAction>,
1398    ) {
1399        if self.is_blackholed(&ctx.validated.identity_hash, ctx.now) {
1400            return;
1401        }
1402        if ctx.packet.hops > constants::PATHFINDER_M {
1403            return;
1404        }
1405
1406        let existing_set = self.path_table.get(&ctx.packet.destination_hash);
1407        let was_unknown_destination = existing_set.is_none_or(|ps| ps.is_empty());
1408
1409        // Reset stale path state before first-path installation so path-state handling
1410        // cannot race ahead of the path table for previously unknown destinations.
1411        if was_unknown_destination {
1412            self.path_states.remove(&ctx.packet.destination_hash);
1413        }
1414
1415        // Multi-path aware decision
1416        let is_unresponsive = self.path_is_unresponsive(&ctx.packet.destination_hash);
1417
1418        let current_gravity = existing_set
1419            .and_then(|path_set| path_set.primary())
1420            .and_then(|path| self.interfaces.get(&path.receiving_interface))
1421            .map(|interface| interface.gravity);
1422        let announce_gravity = self
1423            .interfaces
1424            .get(&ctx.iface)
1425            .map(|interface| interface.gravity);
1426        let higher_gravity_replacement = existing_set.is_some_and(|path_set| {
1427            pathfinder::is_higher_gravity_replacement(
1428                path_set,
1429                ctx.packet.hops,
1430                ctx.announce_emitted,
1431                current_gravity,
1432                announce_gravity,
1433            )
1434        });
1435        let mp_decision = pathfinder::decide_announce_multipath_with_gravity(
1436            existing_set,
1437            ctx.packet.hops,
1438            ctx.announce_emitted,
1439            &ctx.random_blob,
1440            &ctx.received_from,
1441            is_unresponsive,
1442            ctx.now,
1443            self.config.prefer_shorter_path,
1444            current_gravity,
1445            announce_gravity,
1446        );
1447
1448        if mp_decision == MultiPathDecision::Reject {
1449            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
1450                "Announce:path decision REJECT for dest={:02x}{:02x}{:02x}{:02x}..",
1451                ctx.packet.destination_hash[0],
1452                ctx.packet.destination_hash[1],
1453                ctx.packet.destination_hash[2],
1454                ctx.packet.destination_hash[3],
1455            );
1456            return;
1457        }
1458        if higher_gravity_replacement {
1459            log::log!(
1460                target: crate::logging::PATHING_LOG_TARGET,
1461                crate::logging::GRAVITY_UPDATE_LOG_LEVEL,
1462                "Replacing path table entry for {:02x}{:02x}{:02x}{:02x}.. due to higher gravity ({:?}->{:?})",
1463                ctx.packet.destination_hash[0],
1464                ctx.packet.destination_hash[1],
1465                ctx.packet.destination_hash[2],
1466                ctx.packet.destination_hash[3],
1467                current_gravity,
1468                announce_gravity,
1469            );
1470        }
1471
1472        // Rate limiting
1473        let rate_blocked = if ctx.packet.context != constants::CONTEXT_PATH_RESPONSE {
1474            if let Some(iface_info) = self.interfaces.get(&ctx.iface) {
1475                self.rate_limiter.check_and_update(
1476                    &ctx.packet.destination_hash,
1477                    ctx.now,
1478                    iface_info.announce_rate_target,
1479                    iface_info.announce_rate_grace,
1480                    iface_info.announce_rate_penalty,
1481                )
1482            } else {
1483                false
1484            }
1485        } else {
1486            false
1487        };
1488
1489        // Get interface mode for expiry calculation
1490        let interface_mode = self
1491            .interfaces
1492            .get(&ctx.iface)
1493            .map(|i| i.mode)
1494            .unwrap_or(constants::MODE_FULL);
1495
1496        let expires = compute_path_expires(ctx.now, interface_mode);
1497
1498        // Get existing random blobs from the matching path (same next_hop) or empty
1499        let existing_blobs = self
1500            .path_table
1501            .get(&ctx.packet.destination_hash)
1502            .and_then(|ps| ps.find_by_next_hop(&ctx.received_from))
1503            .map(|e| e.random_blobs.clone())
1504            .unwrap_or_default();
1505
1506        // Generate RNG value for retransmit timeout
1507        let mut rng_bytes = [0u8; 8];
1508        rng.fill_bytes(&mut rng_bytes);
1509        let rng_value = (u64::from_le_bytes(rng_bytes) as f64) / (u64::MAX as f64);
1510
1511        let is_path_response = ctx.packet.context == constants::CONTEXT_PATH_RESPONSE;
1512
1513        let (path_entry, announce_entry) = announce_proc::process_validated_announce(
1514            ctx.packet.destination_hash,
1515            ctx.packet.hops,
1516            &ctx.packet.data,
1517            &ctx.packet.raw,
1518            ctx.packet.packet_hash,
1519            ctx.packet.flags.context_flag,
1520            ctx.received_from,
1521            ctx.iface,
1522            ctx.now,
1523            existing_blobs,
1524            ctx.random_blob,
1525            expires,
1526            rng_value,
1527            self.config.transport_enabled,
1528            is_path_response,
1529            rate_blocked,
1530            Some(ctx.original_raw.to_vec()),
1531        );
1532
1533        // Emit CacheAnnounce for disk caching (pre-hop-increment raw)
1534        actions.push(TransportAction::CacheAnnounce {
1535            packet_hash: ctx.packet.packet_hash,
1536            raw: ctx.original_raw.to_vec().into(),
1537        });
1538
1539        // Store path via upsert into PathSet
1540        match mp_decision {
1541            MultiPathDecision::ReplacePrimary => self.upsert_primary_path_destination(
1542                ctx.packet.destination_hash,
1543                path_entry,
1544                ctx.now,
1545            ),
1546            MultiPathDecision::AddAlternative => {
1547                self.upsert_path_destination(ctx.packet.destination_hash, path_entry, ctx.now)
1548            }
1549            MultiPathDecision::Reject => unreachable!("rejected decisions returned above"),
1550        }
1551
1552        // If receiving interface has a tunnel_id, store path in tunnel table too
1553        if let Some(tunnel_id) = self.interfaces.get(&ctx.iface).and_then(|i| i.tunnel_id) {
1554            let blobs = self
1555                .path_table
1556                .get(&ctx.packet.destination_hash)
1557                .and_then(|ps| ps.find_by_next_hop(&ctx.received_from))
1558                .map(|e| e.random_blobs.clone())
1559                .unwrap_or_default();
1560            self.tunnel_table.store_tunnel_path(
1561                &tunnel_id,
1562                ctx.packet.destination_hash,
1563                tunnel::TunnelPath {
1564                    timestamp: ctx.now,
1565                    received_from: ctx.received_from,
1566                    hops: ctx.packet.hops,
1567                    expires,
1568                    random_blobs: blobs,
1569                    packet_hash: ctx.packet.packet_hash,
1570                },
1571                ctx.now,
1572                self.config.destination_timeout_secs,
1573                self.config.max_tunnel_destinations_total,
1574            );
1575        }
1576
1577        // Re-apply the path-state reset after storing the path entry so any transient
1578        // stale state is also cleared once the destination exists in the path table.
1579        self.path_states.remove(&ctx.packet.destination_hash);
1580
1581        // Store announce for retransmission
1582        if let Some(ann) = announce_entry {
1583            self.insert_announce_entry(ctx.packet.destination_hash, ann, ctx.now);
1584        }
1585
1586        // Emit actions
1587        actions.push(TransportAction::AnnounceReceived {
1588            destination_hash: ctx.packet.destination_hash,
1589            identity_hash: ctx.validated.identity_hash,
1590            public_key: ctx.validated.public_key,
1591            name_hash: ctx.validated.name_hash,
1592            random_hash: ctx.validated.random_hash,
1593            ratchet: ctx.validated.ratchet,
1594            app_data: ctx.validated.app_data,
1595            hops: ctx.packet.hops,
1596            receiving_interface: ctx.iface,
1597            rx: RxMetadata {
1598                rssi: ctx.packet.rssi,
1599                snr: ctx.packet.snr,
1600            },
1601        });
1602
1603        actions.push(TransportAction::PathUpdated {
1604            destination_hash: ctx.packet.destination_hash,
1605            hops: ctx.packet.hops,
1606            next_hop: ctx.received_from,
1607            interface: ctx.iface,
1608        });
1609
1610        // Forward announce to local clients if any are connected
1611        if self.has_local_clients() {
1612            actions.push(TransportAction::ForwardToLocalClients {
1613                raw: self.announce_raw_for_local_clients(ctx.packet),
1614                exclude: Some(ctx.iface),
1615            });
1616        }
1617
1618        // Check for discovery path requests waiting for this announce
1619        if let Some(pr_entry) = self.discovery_path_requests_waiting(&ctx.packet.destination_hash) {
1620            // Build a path response announce and queue it
1621            let entry = AnnounceEntry {
1622                timestamp: ctx.now,
1623                retransmit_timeout: ctx.now,
1624                retries: constants::PATHFINDER_R,
1625                received_from: ctx.received_from,
1626                hops: ctx.packet.hops,
1627                packet_raw: ctx.packet.raw.clone(),
1628                packet_data: ctx.packet.data.clone(),
1629                destination_hash: ctx.packet.destination_hash,
1630                context_flag: ctx.packet.flags.context_flag,
1631                local_rebroadcasts: 0,
1632                block_rebroadcasts: true,
1633                attached_interface: Some(pr_entry),
1634            };
1635            self.insert_announce_entry(ctx.packet.destination_hash, entry, ctx.now);
1636        }
1637    }
1638
1639    pub fn announce_sig_cache_contains(&self, sig_cache_key: &[u8; 32]) -> bool {
1640        self.announce_sig_cache.contains(sig_cache_key)
1641    }
1642
1643    /// Check if there's a waiting discovery path request for a destination.
1644    /// Consumes the request if found (one-shot: the caller queues the announce response).
1645    fn discovery_path_requests_waiting(&mut self, dest_hash: &[u8; 16]) -> Option<InterfaceId> {
1646        self.discovery_path_requests
1647            .remove(dest_hash)
1648            .map(|req| req.requesting_interface)
1649    }
1650
1651    // =========================================================================
1652    // Inbound proof processing
1653    // =========================================================================
1654
1655    fn process_inbound_proof(
1656        &mut self,
1657        ctx: &InboundPacketCtx,
1658        actions: &mut Vec<TransportAction>,
1659    ) {
1660        let packet = &ctx.packet;
1661        if packet.context == constants::CONTEXT_LRPROOF {
1662            // Link request proof routing
1663            if (self.config.transport_enabled)
1664                && self.link_table.contains_key(&packet.destination_hash)
1665            {
1666                let link_entry = self.link_table.get(&packet.destination_hash).cloned();
1667                if let Some(entry) = link_entry {
1668                    let instance_local_link = self
1669                        .interface_is_local_client(entry.next_hop_interface)
1670                        && self.interface_is_local_client(entry.received_interface);
1671                    if let Some((outbound_interface, new_raw)) = route_via_link_table(
1672                        packet,
1673                        &entry,
1674                        ctx.iface,
1675                        LocalHopRewrite {
1676                            local_hops_delta: self.config.local_hops_delta,
1677                            from_local_client: ctx.from_local_client,
1678                            skip_local_hops_delta: instance_local_link,
1679                        },
1680                    ) {
1681                        // Forward the proof (simplified: skip signature validation
1682                        // which requires Identity recall)
1683
1684                        // Mark link as validated
1685                        if let Some(le) = self.link_table.get_mut(&packet.destination_hash) {
1686                            le.validated = true;
1687                        }
1688
1689                        actions.push(TransportAction::LinkEstablished {
1690                            link_id: packet.destination_hash,
1691                            interface: outbound_interface,
1692                        });
1693
1694                        actions.push(TransportAction::SendOnInterface {
1695                            interface: outbound_interface,
1696                            raw: new_raw.into(),
1697                        });
1698                    } else if link_route_hops_match(packet.hops, &entry, ctx.iface) {
1699                        log::debug!(
1700                            "Link request proof received on wrong interface {}, not transporting it (expected {} or {})",
1701                            ctx.iface.0,
1702                            entry.next_hop_interface.0,
1703                            entry.received_interface.0,
1704                        );
1705                    } else {
1706                        log::debug!("{}", lrproof_hop_mismatch_diagnostic(packet.hops, &entry));
1707                    }
1708                }
1709            } else {
1710                // Could be for a local pending link - deliver locally
1711                let mut delivery_raw = packet.raw.clone();
1712                // LinkManager must see the same post-ingress hop metric that
1713                // was authenticated and considered by transport.
1714                if delivery_raw.len() >= 2 {
1715                    delivery_raw[1] = packet.hops;
1716                }
1717                actions.push(TransportAction::DeliverLocal {
1718                    destination_hash: packet.destination_hash,
1719                    raw: PacketBytes::from(delivery_raw),
1720                    packet_hash: packet.packet_hash,
1721                    receiving_interface: ctx.iface,
1722                });
1723            }
1724        } else {
1725            // Regular proof: check reverse table
1726            if self.config.transport_enabled {
1727                if let Some(reverse_entry) = self.reverse_table.remove(&packet.destination_hash) {
1728                    let proof_for_local_client =
1729                        self.interface_is_local_client(reverse_entry.receiving_interface);
1730                    if let Some(action) = route_proof_via_reverse(
1731                        packet,
1732                        &reverse_entry,
1733                        ctx.iface,
1734                        LocalHopRewrite {
1735                            local_hops_delta: self.config.local_hops_delta,
1736                            from_local_client: ctx.from_local_client,
1737                            skip_local_hops_delta: proof_for_local_client,
1738                        },
1739                    ) {
1740                        actions.push(action);
1741                    }
1742                }
1743            }
1744
1745            // Deliver to local receipts
1746            actions.push(TransportAction::DeliverLocal {
1747                destination_hash: packet.destination_hash,
1748                raw: PacketBytes::from(packet.raw.clone()),
1749                packet_hash: packet.packet_hash,
1750                receiving_interface: ctx.iface,
1751            });
1752        }
1753    }
1754
1755    // =========================================================================
1756    // Core API: handle_outbound
1757    // =========================================================================
1758
1759    /// Route an outbound packet.
1760    pub fn handle_outbound(
1761        &mut self,
1762        packet: &RawPacket,
1763        dest_type: u8,
1764        attached_interface: Option<InterfaceId>,
1765        now: f64,
1766    ) -> Vec<TransportAction> {
1767        let actions = route_outbound_with_options(
1768            &self.path_table,
1769            &self.interfaces,
1770            &self.local_destinations,
1771            packet,
1772            dest_type,
1773            attached_interface,
1774            now,
1775            OutboundRouteOptions {
1776                identity_hash: self.config.identity_hash,
1777                local_hops_delta: self.config.local_hops_delta,
1778            },
1779        );
1780
1781        // Add to packet hashlist for outbound packets
1782        self.packet_hashlist.add(packet.packet_hash);
1783
1784        // Gate announces with hops > 0 through the bandwidth queue
1785        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE && packet.hops > 0 {
1786            self.gate_announce_actions(actions, &packet.destination_hash, packet.hops, now)
1787        } else {
1788            actions
1789        }
1790    }
1791
1792    /// Gate announce SendOnInterface actions through per-interface bandwidth queues.
1793    fn gate_announce_actions(
1794        &mut self,
1795        actions: Vec<TransportAction>,
1796        dest_hash: &[u8; 16],
1797        hops: u8,
1798        now: f64,
1799    ) -> Vec<TransportAction> {
1800        let mut result = Vec::new();
1801        for action in actions {
1802            match action {
1803                TransportAction::SendOnInterface { interface, raw } => {
1804                    let (bitrate, airtime_profile, announce_cap) =
1805                        if let Some(info) = self.interfaces.get(&interface) {
1806                            (info.bitrate, info.airtime_profile, info.announce_cap)
1807                        } else {
1808                            (None, None, constants::ANNOUNCE_CAP)
1809                        };
1810                    if let Some(send_action) = self.announce_queues.gate_announce(
1811                        interface,
1812                        raw,
1813                        *dest_hash,
1814                        hops,
1815                        now,
1816                        now,
1817                        bitrate,
1818                        airtime_profile,
1819                        announce_cap,
1820                    ) {
1821                        result.push(send_action);
1822                    }
1823                    // If None, it was queued — no action emitted now
1824                }
1825                other => result.push(other),
1826            }
1827        }
1828        result
1829    }
1830
1831    // =========================================================================
1832    // Core API: tick
1833    // =========================================================================
1834
1835    /// Periodic maintenance. Call regularly (e.g., every 250ms).
1836    pub fn tick(&mut self, now: f64, rng: &mut dyn Rng) -> Vec<TransportAction> {
1837        let mut ctx = TickCtx {
1838            now,
1839            rng,
1840            actions: Vec::new(),
1841        };
1842        self.process_tick_pending_announces(&mut ctx);
1843
1844        let mut queue_actions = self.announce_queues.process_queues(now, &self.interfaces);
1845        ctx.actions.append(&mut queue_actions);
1846
1847        self.process_tick_ingress_release(&mut ctx);
1848        self.cull_tick_tables(&mut ctx);
1849        ctx.actions
1850    }
1851
1852    fn process_tick_pending_announces(&mut self, ctx: &mut TickCtx<'_>) {
1853        if ctx.now <= self.announces_last_checked + constants::ANNOUNCES_CHECK_INTERVAL {
1854            return;
1855        }
1856
1857        self.cull_expired_announce_entries(ctx.now);
1858        self.enforce_announce_retention_cap(ctx.now);
1859        if let Some(identity_hash) = self.config.identity_hash {
1860            let announce_actions = jobs::process_pending_announces(
1861                &mut self.announce_table,
1862                &mut self.held_announces,
1863                &identity_hash,
1864                ctx.now,
1865            );
1866            let gated = self.gate_retransmit_actions(announce_actions, ctx.now);
1867            ctx.actions.extend(gated);
1868        }
1869        self.cull_expired_announce_entries(ctx.now);
1870        self.enforce_announce_retention_cap(ctx.now);
1871        self.announces_last_checked = ctx.now;
1872    }
1873
1874    fn process_tick_ingress_release(&mut self, ctx: &mut TickCtx<'_>) {
1875        let ic_interfaces = self.ingress_control.interfaces_with_held();
1876        for iface_id in ic_interfaces {
1877            let (ia_freq, started, ingress_config) = match self.interfaces.get(&iface_id) {
1878                Some(info) => (info.ia_freq, info.started, info.ingress_control),
1879                None => continue,
1880            };
1881            if !ingress_config.enabled {
1882                continue;
1883            }
1884            if let Some(held) = self.ingress_control.process_held_announces(
1885                iface_id,
1886                &ingress_config,
1887                ia_freq,
1888                started,
1889                ctx.now,
1890            ) {
1891                let released_actions = self.handle_inbound(
1892                    InboundFrame {
1893                        raw: &held.raw,
1894                        iface: held.receiving_interface,
1895                        now: ctx.now,
1896                        rx: held.rx,
1897                    },
1898                    ctx.rng,
1899                );
1900                ctx.actions.extend(released_actions);
1901            }
1902        }
1903    }
1904
1905    fn cull_tick_tables(&mut self, ctx: &mut TickCtx<'_>) {
1906        if ctx.now <= self.tables_last_culled + constants::TABLES_CULL_INTERVAL {
1907            return;
1908        }
1909
1910        jobs::cull_path_table(&mut self.path_table, &self.interfaces, ctx.now);
1911        jobs::cull_reverse_table(&mut self.reverse_table, &self.interfaces, ctx.now);
1912        let (_culled, link_closed_actions) =
1913            jobs::cull_link_table(&mut self.link_table, &self.interfaces, ctx.now);
1914        ctx.actions.extend(link_closed_actions);
1915        jobs::cull_path_states(&mut self.path_states, &self.path_table);
1916        self.cull_blackholed(ctx.now);
1917        self.discovery_path_requests
1918            .retain(|_, req| ctx.now - req.timestamp < constants::DISCOVERY_PATH_REQUEST_TIMEOUT);
1919        self.tunnel_table
1920            .void_missing_interfaces(|id| self.interfaces.contains_key(id));
1921        self.tunnel_table.cull(ctx.now);
1922        self.announce_sig_cache.cull(ctx.now);
1923        self.tables_last_culled = ctx.now;
1924    }
1925
1926    /// Gate retransmitted announce actions through per-interface bandwidth queues.
1927    ///
1928    /// Retransmitted announces always have hops > 0.
1929    /// `BroadcastOnAllInterfaces` is expanded to per-interface sends gated through queues.
1930    fn gate_retransmit_actions(
1931        &mut self,
1932        actions: Vec<TransportAction>,
1933        now: f64,
1934    ) -> Vec<TransportAction> {
1935        let mut result = Vec::new();
1936        for action in actions {
1937            match action {
1938                TransportAction::SendOnInterface { interface, raw } => {
1939                    // Extract dest_hash from raw (bytes 2..18 for H1, 18..34 for H2)
1940                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
1941                    let (bitrate, airtime_profile, announce_cap) =
1942                        if let Some(info) = self.interfaces.get(&interface) {
1943                            (info.bitrate, info.airtime_profile, info.announce_cap)
1944                        } else {
1945                            (None, None, constants::ANNOUNCE_CAP)
1946                        };
1947                    if let Some(send_action) = self.announce_queues.gate_announce(
1948                        interface,
1949                        raw,
1950                        dest_hash,
1951                        hops,
1952                        now,
1953                        now,
1954                        bitrate,
1955                        airtime_profile,
1956                        announce_cap,
1957                    ) {
1958                        result.push(send_action);
1959                    }
1960                }
1961                TransportAction::BroadcastOnAllInterfaces { raw, exclude } => {
1962                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
1963                    // Expand to per-interface sends gated through queues,
1964                    // applying mode filtering (AP blocks non-local announces, etc.)
1965                    let iface_ids: Vec<(
1966                        InterfaceId,
1967                        Option<u64>,
1968                        Option<types::AirtimeProfile>,
1969                        f64,
1970                    )> = self
1971                        .interfaces
1972                        .iter()
1973                        .filter(|(_, info)| info.out_capable)
1974                        .filter(|(id, _)| {
1975                            if let Some(ref ex) = exclude {
1976                                **id != *ex
1977                            } else {
1978                                true
1979                            }
1980                        })
1981                        .filter(|(_, info)| {
1982                            should_transmit_announce(
1983                                info,
1984                                &dest_hash,
1985                                hops,
1986                                &self.local_destinations,
1987                                &self.path_table,
1988                                &self.interfaces,
1989                            )
1990                        })
1991                        .map(|(id, info)| {
1992                            (*id, info.bitrate, info.airtime_profile, info.announce_cap)
1993                        })
1994                        .collect();
1995
1996                    for (iface_id, bitrate, airtime_profile, announce_cap) in iface_ids {
1997                        if let Some(send_action) = self.announce_queues.gate_announce(
1998                            iface_id,
1999                            raw.clone(),
2000                            dest_hash,
2001                            hops,
2002                            now,
2003                            now,
2004                            bitrate,
2005                            airtime_profile,
2006                            announce_cap,
2007                        ) {
2008                            result.push(send_action);
2009                        }
2010                    }
2011                }
2012                other => result.push(other),
2013            }
2014        }
2015        result
2016    }
2017
2018    /// Extract destination hash and hops from raw announce bytes.
2019    fn extract_announce_info(raw: &[u8]) -> ([u8; 16], u8) {
2020        if raw.len() < 18 {
2021            return ([0; 16], 0);
2022        }
2023        let header_type = (raw[0] >> 6) & 0x03;
2024        let hops = raw[1];
2025        if header_type == constants::HEADER_2 && raw.len() >= 34 {
2026            // H2: transport_id at [2..18], dest_hash at [18..34]
2027            let mut dest = [0u8; 16];
2028            dest.copy_from_slice(&raw[18..34]);
2029            (dest, hops)
2030        } else {
2031            // H1: dest_hash at [2..18]
2032            let mut dest = [0u8; 16];
2033            dest.copy_from_slice(&raw[2..18]);
2034            (dest, hops)
2035        }
2036    }
2037
2038    #[cfg(test)]
2039    #[allow(dead_code)]
2040    pub(crate) fn link_table_ref(&self) -> &BTreeMap<[u8; 16], LinkEntry> {
2041        &self.link_table
2042    }
2043}
2044
2045#[cfg(test)]
2046mod tests {
2047    use super::*;
2048    use crate::packet::PacketFlags;
2049
2050    fn make_config(transport_enabled: bool) -> TransportConfig {
2051        TransportConfig {
2052            transport_enabled,
2053            identity_hash: if transport_enabled {
2054                Some([0x42; 16])
2055            } else {
2056                None
2057            },
2058            local_hops_delta: 0,
2059            prefer_shorter_path: false,
2060            max_paths_per_destination: 1,
2061            packet_hashlist_max_entries: constants::HASHLIST_MAXSIZE,
2062            packet_hashlist_allocation: crate::transport::types::PacketHashlistAllocation::Eager,
2063            max_discovery_pr_tags: constants::MAX_PR_TAGS,
2064            max_path_destinations: usize::MAX,
2065            max_tunnel_destinations_total: usize::MAX,
2066            destination_timeout_secs: constants::DESTINATION_TIMEOUT,
2067            announce_table_ttl_secs: constants::ANNOUNCE_TABLE_TTL,
2068            announce_table_max_bytes: constants::ANNOUNCE_TABLE_MAX_BYTES,
2069            announce_sig_cache_enabled: true,
2070            announce_sig_cache_max_entries: constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
2071            announce_sig_cache_ttl_secs: constants::ANNOUNCE_SIG_CACHE_TTL,
2072            announce_queue_max_entries: 256,
2073            announce_queue_max_interfaces: 1024,
2074        }
2075    }
2076
2077    fn make_interface(id: u64, mode: u8) -> InterfaceInfo {
2078        InterfaceInfo {
2079            id: InterfaceId(id),
2080            name: String::from("test"),
2081            mode,
2082            gravity: 0,
2083            recursive_prs: false,
2084            announces_from_internal: true,
2085            announces_to_internal: None,
2086            out_capable: true,
2087            in_capable: true,
2088            bitrate: None,
2089            airtime_profile: None,
2090            announce_rate_target: None,
2091            announce_rate_grace: 0,
2092            announce_rate_penalty: 0.0,
2093            announce_cap: constants::ANNOUNCE_CAP,
2094            is_local_client: false,
2095            wants_tunnel: false,
2096            tunnel_id: None,
2097            mtu: constants::MTU as u32,
2098            ingress_control: crate::transport::types::IngressControlConfig::disabled(),
2099            ia_freq: 0.0,
2100            ip_freq: 0.0,
2101            op_freq: 0.0,
2102            op_samples: 0,
2103            started: 0.0,
2104        }
2105    }
2106
2107    fn make_announce_entry(dest_hash: [u8; 16], timestamp: f64, fill_len: usize) -> AnnounceEntry {
2108        AnnounceEntry {
2109            timestamp,
2110            retransmit_timeout: timestamp,
2111            retries: 0,
2112            received_from: [0xAA; 16],
2113            hops: 2,
2114            packet_raw: vec![0x01; fill_len],
2115            packet_data: vec![0x02; fill_len],
2116            destination_hash: dest_hash,
2117            context_flag: 0,
2118            local_rebroadcasts: 0,
2119            block_rebroadcasts: false,
2120            attached_interface: None,
2121        }
2122    }
2123
2124    fn make_path_entry(
2125        timestamp: f64,
2126        hops: u8,
2127        receiving_interface: InterfaceId,
2128        next_hop: [u8; 16],
2129    ) -> PathEntry {
2130        PathEntry {
2131            timestamp,
2132            next_hop,
2133            hops,
2134            expires: timestamp + 10_000.0,
2135            random_blobs: Vec::new(),
2136            receiving_interface,
2137            packet_hash: [0; 32],
2138            announce_raw: None,
2139        }
2140    }
2141
2142    fn make_unique_tag(dest_hash: [u8; 16], tag: &[u8]) -> [u8; 32] {
2143        let mut unique_tag = [0u8; 32];
2144        let tag_len = tag.len().min(16);
2145        unique_tag[..16].copy_from_slice(&dest_hash);
2146        unique_tag[16..16 + tag_len].copy_from_slice(&tag[..tag_len]);
2147        unique_tag
2148    }
2149
2150    fn make_random_blob(timebase: u64) -> [u8; 10] {
2151        let mut blob = [0u8; 10];
2152        let bytes = timebase.to_be_bytes();
2153        blob[5..10].copy_from_slice(&bytes[3..8]);
2154        blob
2155    }
2156
2157    #[test]
2158    fn test_empty_engine() {
2159        let engine = TransportEngine::new(make_config(false));
2160        assert!(!engine.has_path(&[0; 16]));
2161        assert!(engine.hops_to(&[0; 16]).is_none());
2162        assert!(engine.next_hop(&[0; 16]).is_none());
2163    }
2164
2165    #[test]
2166    fn test_register_deregister_interface() {
2167        let mut engine = TransportEngine::new(make_config(false));
2168        engine.register_interface(make_interface(1, constants::MODE_FULL));
2169        assert!(engine.interfaces.contains_key(&InterfaceId(1)));
2170
2171        engine.deregister_interface(InterfaceId(1));
2172        assert!(!engine.interfaces.contains_key(&InterfaceId(1)));
2173    }
2174
2175    #[test]
2176    fn test_deregister_interface_removes_announce_queue_state() {
2177        let mut engine = TransportEngine::new(make_config(false));
2178        engine.register_interface(make_interface(1, constants::MODE_FULL));
2179
2180        let _ = engine.announce_queues.gate_announce(
2181            InterfaceId(1),
2182            vec![0x01; 100].into(),
2183            [0xAA; 16],
2184            2,
2185            0.0,
2186            0.0,
2187            Some(1000),
2188            None,
2189            constants::ANNOUNCE_CAP,
2190        );
2191        let _ = engine.announce_queues.gate_announce(
2192            InterfaceId(1),
2193            vec![0x02; 100].into(),
2194            [0xBB; 16],
2195            3,
2196            0.0,
2197            0.0,
2198            Some(1000),
2199            None,
2200            constants::ANNOUNCE_CAP,
2201        );
2202        assert_eq!(engine.announce_queue_count(), 1);
2203
2204        engine.deregister_interface(InterfaceId(1));
2205        assert_eq!(engine.announce_queue_count(), 0);
2206    }
2207
2208    #[test]
2209    fn test_deregister_interface_removes_transport_state() {
2210        let mut engine = TransportEngine::new(make_config(true));
2211        engine.register_interface(make_interface(1, constants::MODE_FULL));
2212        engine.register_interface(make_interface(2, constants::MODE_FULL));
2213
2214        let destination_hash = [0x11; 16];
2215        engine.inject_path(
2216            destination_hash,
2217            PathEntry {
2218                timestamp: 1000.0,
2219                next_hop: [0x22; 16],
2220                hops: 2,
2221                expires: 2000.0,
2222                random_blobs: Vec::new(),
2223                receiving_interface: InterfaceId(1),
2224                packet_hash: [0x33; 32],
2225                announce_raw: None,
2226            },
2227        );
2228        engine.reverse_table.insert(
2229            [0x44; 16],
2230            tables::ReverseEntry {
2231                receiving_interface: InterfaceId(2),
2232                outbound_interface: InterfaceId(1),
2233                timestamp: 1000.0,
2234            },
2235        );
2236        engine.register_link(
2237            [0x55; 16],
2238            LinkEntry {
2239                timestamp: 1000.0,
2240                next_hop_transport_id: [0x66; 16],
2241                next_hop_interface: InterfaceId(1),
2242                remaining_hops: 1,
2243                received_interface: InterfaceId(2),
2244                taken_hops: 1,
2245                destination_hash,
2246                validated: true,
2247                proof_timeout: 1100.0,
2248            },
2249        );
2250
2251        assert_eq!(engine.path_table_count(), 1);
2252        assert_eq!(engine.reverse_table_count(), 1);
2253        assert_eq!(engine.link_table_count(), 1);
2254
2255        engine.deregister_interface(InterfaceId(1));
2256
2257        assert_eq!(engine.path_table_count(), 0);
2258        assert_eq!(engine.reverse_table_count(), 0);
2259        assert_eq!(engine.link_table_count(), 0);
2260    }
2261
2262    #[test]
2263    fn test_deregister_interface_preserves_other_announce_queues() {
2264        let mut engine = TransportEngine::new(make_config(false));
2265        engine.register_interface(make_interface(1, constants::MODE_FULL));
2266        engine.register_interface(make_interface(2, constants::MODE_FULL));
2267
2268        let _ = engine.announce_queues.gate_announce(
2269            InterfaceId(1),
2270            vec![0x01; 100].into(),
2271            [0xAA; 16],
2272            2,
2273            0.0,
2274            0.0,
2275            Some(1000),
2276            None,
2277            constants::ANNOUNCE_CAP,
2278        );
2279        let _ = engine.announce_queues.gate_announce(
2280            InterfaceId(1),
2281            vec![0x02; 100].into(),
2282            [0xAB; 16],
2283            3,
2284            0.0,
2285            0.0,
2286            Some(1000),
2287            None,
2288            constants::ANNOUNCE_CAP,
2289        );
2290        let _ = engine.announce_queues.gate_announce(
2291            InterfaceId(2),
2292            vec![0x03; 100].into(),
2293            [0xBA; 16],
2294            2,
2295            0.0,
2296            0.0,
2297            Some(1000),
2298            None,
2299            constants::ANNOUNCE_CAP,
2300        );
2301        let _ = engine.announce_queues.gate_announce(
2302            InterfaceId(2),
2303            vec![0x04; 100].into(),
2304            [0xBB; 16],
2305            3,
2306            0.0,
2307            0.0,
2308            Some(1000),
2309            None,
2310            constants::ANNOUNCE_CAP,
2311        );
2312
2313        engine.deregister_interface(InterfaceId(1));
2314        assert_eq!(engine.announce_queue_count(), 1);
2315        assert_eq!(engine.nonempty_announce_queue_count(), 1);
2316    }
2317
2318    #[test]
2319    fn test_register_deregister_destination() {
2320        let mut engine = TransportEngine::new(make_config(false));
2321        let dest = [0x11; 16];
2322        engine.register_destination(dest, constants::DESTINATION_SINGLE);
2323        assert!(engine.local_destinations.contains_key(&dest));
2324
2325        engine.deregister_destination(&dest);
2326        assert!(!engine.local_destinations.contains_key(&dest));
2327    }
2328
2329    #[test]
2330    fn test_path_state() {
2331        let mut engine = TransportEngine::new(make_config(false));
2332        let dest = [0x22; 16];
2333
2334        assert!(!engine.path_is_unresponsive(&dest));
2335
2336        engine.mark_path_unresponsive(&dest, None);
2337        assert!(engine.path_is_unresponsive(&dest));
2338
2339        engine.mark_path_responsive(&dest);
2340        assert!(!engine.path_is_unresponsive(&dest));
2341    }
2342
2343    #[test]
2344    fn test_announce_clears_stale_path_state_for_unknown_destination() {
2345        use crate::announce::AnnounceData;
2346        use crate::destination::{destination_hash, name_hash};
2347
2348        let mut engine = TransportEngine::new(make_config(false));
2349        engine.register_interface(make_interface(1, constants::MODE_FULL));
2350
2351        let identity =
2352            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x61; 32]));
2353        let dest_hash = destination_hash("pathfix", &["announce"], Some(identity.hash()));
2354        let name_h = name_hash("pathfix", &["announce"]);
2355        let random_hash = [0x24u8; 10];
2356
2357        let (announce_data, _) =
2358            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
2359
2360        let packet = RawPacket::pack(
2361            PacketFlags {
2362                header_type: constants::HEADER_1,
2363                context_flag: constants::FLAG_UNSET,
2364                transport_type: constants::TRANSPORT_BROADCAST,
2365                destination_type: constants::DESTINATION_SINGLE,
2366                packet_type: constants::PACKET_TYPE_ANNOUNCE,
2367            },
2368            0,
2369            &dest_hash,
2370            None,
2371            constants::CONTEXT_NONE,
2372            &announce_data,
2373        )
2374        .unwrap();
2375
2376        engine.mark_path_unresponsive(&dest_hash, None);
2377        assert!(engine.path_is_unresponsive(&dest_hash));
2378        assert!(!engine.has_path(&dest_hash));
2379
2380        let mut rng = rns_crypto::FixedRng::new(&[0x62; 32]);
2381        let actions = engine.handle_inbound(
2382            InboundFrame {
2383                raw: &packet.raw,
2384                iface: InterfaceId(1),
2385                now: 1000.0,
2386                rx: RxMetadata {
2387                    rssi: None,
2388                    snr: None,
2389                },
2390            },
2391            &mut rng,
2392        );
2393
2394        assert!(engine.has_path(&dest_hash));
2395        assert!(
2396            !engine.path_is_unresponsive(&dest_hash),
2397            "stale path state should be cleared for newly installed paths"
2398        );
2399        assert!(actions.iter().any(|action| matches!(
2400            action,
2401            TransportAction::PathUpdated {
2402                destination_hash,
2403                interface,
2404                ..
2405            } if *destination_hash == dest_hash && *interface == InterfaceId(1)
2406        )));
2407    }
2408
2409    #[test]
2410    fn test_duplicate_announce_from_second_interface_uses_existing_path() {
2411        use crate::announce::AnnounceData;
2412        use crate::destination::{destination_hash, name_hash};
2413
2414        let mut engine = TransportEngine::new(make_config(false));
2415        engine.register_interface(make_interface(1, constants::MODE_FULL));
2416        engine.register_interface(make_interface(2, constants::MODE_FULL));
2417
2418        let identity =
2419            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x63; 32]));
2420        let dest_hash = destination_hash("dedup", &["announce"], Some(identity.hash()));
2421        let name_h = name_hash("dedup", &["announce"]);
2422        let random_hash = [0x25u8; 10];
2423
2424        let (announce_data, _) =
2425            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
2426        let packet = RawPacket::pack(
2427            PacketFlags {
2428                header_type: constants::HEADER_1,
2429                context_flag: constants::FLAG_UNSET,
2430                transport_type: constants::TRANSPORT_BROADCAST,
2431                destination_type: constants::DESTINATION_SINGLE,
2432                packet_type: constants::PACKET_TYPE_ANNOUNCE,
2433            },
2434            0,
2435            &dest_hash,
2436            None,
2437            constants::CONTEXT_NONE,
2438            &announce_data,
2439        )
2440        .unwrap();
2441
2442        let mut rng = rns_crypto::FixedRng::new(&[0x64; 32]);
2443        let first_actions = engine.handle_inbound(
2444            InboundFrame {
2445                raw: &packet.raw,
2446                iface: InterfaceId(1),
2447                now: 1000.0,
2448                rx: RxMetadata::default(),
2449            },
2450            &mut rng,
2451        );
2452        assert!(first_actions.iter().any(|action| matches!(
2453            action,
2454            TransportAction::PathUpdated {
2455                destination_hash,
2456                interface,
2457                ..
2458            } if *destination_hash == dest_hash && *interface == InterfaceId(1)
2459        )));
2460
2461        let second_actions = engine.handle_inbound(
2462            InboundFrame {
2463                raw: &packet.raw,
2464                iface: InterfaceId(2),
2465                now: 1000.1,
2466                rx: RxMetadata::default(),
2467            },
2468            &mut rng,
2469        );
2470
2471        assert!(!second_actions.iter().any(|action| matches!(
2472            action,
2473            TransportAction::PathUpdated {
2474                destination_hash,
2475                interface,
2476                ..
2477            } if *destination_hash == dest_hash && *interface == InterfaceId(2)
2478        )));
2479        let path = engine
2480            .path_table
2481            .get(&dest_hash)
2482            .and_then(|set| set.primary())
2483            .expect("first announce should install a path");
2484        assert_eq!(path.receiving_interface, InterfaceId(1));
2485
2486        let mut higher_gravity = make_interface(2, constants::MODE_FULL);
2487        higher_gravity.gravity = 1;
2488        engine.register_interface(higher_gravity);
2489        let third_actions = engine.handle_inbound(
2490            InboundFrame {
2491                raw: &packet.raw,
2492                iface: InterfaceId(2),
2493                now: 1000.2,
2494                rx: RxMetadata::default(),
2495            },
2496            &mut rng,
2497        );
2498
2499        assert!(third_actions.iter().any(|action| matches!(
2500            action,
2501            TransportAction::PathUpdated {
2502                destination_hash,
2503                interface,
2504                ..
2505            } if *destination_hash == dest_hash && *interface == InterfaceId(2)
2506        )));
2507        let path = engine
2508            .path_table
2509            .get(&dest_hash)
2510            .and_then(|set| set.primary())
2511            .expect("higher-gravity reception should become primary");
2512        assert_eq!(path.receiving_interface, InterfaceId(2));
2513    }
2514
2515    #[test]
2516    fn test_boundary_exempts_unresponsive() {
2517        let mut engine = TransportEngine::new(make_config(false));
2518        engine.register_interface(make_interface(1, constants::MODE_BOUNDARY));
2519        let dest = [0xB1; 16];
2520
2521        // Marking via a boundary interface should be skipped
2522        engine.mark_path_unresponsive(&dest, Some(InterfaceId(1)));
2523        assert!(!engine.path_is_unresponsive(&dest));
2524    }
2525
2526    #[test]
2527    fn test_non_boundary_marks_unresponsive() {
2528        let mut engine = TransportEngine::new(make_config(false));
2529        engine.register_interface(make_interface(1, constants::MODE_FULL));
2530        let dest = [0xB2; 16];
2531
2532        // Marking via a non-boundary interface should work
2533        engine.mark_path_unresponsive(&dest, Some(InterfaceId(1)));
2534        assert!(engine.path_is_unresponsive(&dest));
2535    }
2536
2537    #[test]
2538    fn test_expire_path() {
2539        let mut engine = TransportEngine::new(make_config(false));
2540        let dest = [0x33; 16];
2541
2542        engine.path_table.insert(
2543            dest,
2544            PathSet::from_single(
2545                PathEntry {
2546                    timestamp: 1000.0,
2547                    next_hop: [0; 16],
2548                    hops: 2,
2549                    expires: 9999.0,
2550                    random_blobs: Vec::new(),
2551                    receiving_interface: InterfaceId(1),
2552                    packet_hash: [0; 32],
2553                    announce_raw: None,
2554                },
2555                1,
2556            ),
2557        );
2558
2559        assert!(engine.has_path(&dest));
2560        engine.expire_path(&dest);
2561        // Path still exists but expires = 0
2562        assert!(engine.has_path(&dest));
2563        assert_eq!(engine.path_table[&dest].primary().unwrap().expires, 0.0);
2564    }
2565
2566    #[test]
2567    fn test_link_table_operations() {
2568        let mut engine = TransportEngine::new(make_config(false));
2569        let link_id = [0x44; 16];
2570
2571        engine.register_link(
2572            link_id,
2573            LinkEntry {
2574                timestamp: 100.0,
2575                next_hop_transport_id: [0; 16],
2576                next_hop_interface: InterfaceId(1),
2577                remaining_hops: 3,
2578                received_interface: InterfaceId(2),
2579                taken_hops: 2,
2580                destination_hash: [0xAA; 16],
2581                validated: false,
2582                proof_timeout: 200.0,
2583            },
2584        );
2585
2586        assert!(engine.link_table.contains_key(&link_id));
2587        assert!(!engine.link_table[&link_id].validated);
2588
2589        engine.validate_link(&link_id);
2590        assert!(engine.link_table[&link_id].validated);
2591
2592        engine.remove_link(&link_id);
2593        assert!(!engine.link_table.contains_key(&link_id));
2594    }
2595
2596    #[test]
2597    fn test_lrproof_routes_from_originating_side_via_link_table() {
2598        let mut engine = TransportEngine::new(make_config(true));
2599        engine.register_interface(make_interface(1, constants::MODE_FULL));
2600        engine.register_interface(make_interface(2, constants::MODE_FULL));
2601
2602        let link_id = [0x44; 16];
2603        engine.register_link(
2604            link_id,
2605            LinkEntry {
2606                timestamp: 100.0,
2607                next_hop_transport_id: [0xAA; 16],
2608                next_hop_interface: InterfaceId(2),
2609                remaining_hops: 3,
2610                received_interface: InterfaceId(1),
2611                taken_hops: 1,
2612                destination_hash: [0xBB; 16],
2613                validated: false,
2614                proof_timeout: 200.0,
2615            },
2616        );
2617
2618        let flags = PacketFlags {
2619            header_type: constants::HEADER_1,
2620            context_flag: constants::FLAG_UNSET,
2621            transport_type: constants::TRANSPORT_BROADCAST,
2622            destination_type: constants::DESTINATION_LINK,
2623            packet_type: constants::PACKET_TYPE_PROOF,
2624        };
2625        let packet = RawPacket::pack(
2626            flags,
2627            0,
2628            &link_id,
2629            None,
2630            constants::CONTEXT_LRPROOF,
2631            &[0xCC; 64],
2632        )
2633        .unwrap();
2634        let mut rng = rns_crypto::FixedRng::new(&[0x33; 32]);
2635
2636        let actions = engine.handle_inbound(
2637            InboundFrame {
2638                raw: &packet.raw,
2639                iface: InterfaceId(1),
2640                now: 101.0,
2641                rx: RxMetadata {
2642                    rssi: None,
2643                    snr: None,
2644                },
2645            },
2646            &mut rng,
2647        );
2648
2649        assert!(matches!(
2650            engine
2651                .link_table_ref()
2652                .get(&link_id)
2653                .map(|entry| entry.validated),
2654            Some(true)
2655        ));
2656        assert!(actions.iter().any(|action| matches!(
2657            action,
2658            TransportAction::LinkEstablished {
2659                link_id: established,
2660                interface: InterfaceId(2),
2661            } if *established == link_id
2662        )));
2663        assert!(actions.iter().any(|action| matches!(
2664            action,
2665            TransportAction::SendOnInterface {
2666                interface: InterfaceId(2),
2667                ..
2668            }
2669        )));
2670    }
2671
2672    fn lrproof_rebalance_fixture() -> (TransportEngine, [u8; 16], [u8; 16], [u8; 32], Vec<u8>) {
2673        use rns_crypto::ed25519::Ed25519PrivateKey;
2674
2675        let mut engine = TransportEngine::new(make_config(true));
2676        engine.register_interface(make_interface(1, constants::MODE_FULL));
2677        engine.register_interface(make_interface(2, constants::MODE_FULL));
2678        let link_id = [0x44; 16];
2679        let destination_hash = [0xBB; 16];
2680        engine.register_link(
2681            link_id,
2682            LinkEntry {
2683                timestamp: 100.0,
2684                next_hop_transport_id: [0xAA; 16],
2685                next_hop_interface: InterfaceId(2),
2686                remaining_hops: 3,
2687                received_interface: InterfaceId(1),
2688                taken_hops: 1,
2689                destination_hash,
2690                validated: false,
2691                proof_timeout: 200.0,
2692            },
2693        );
2694        engine.inject_path(
2695            destination_hash,
2696            PathEntry {
2697                timestamp: 99.0,
2698                next_hop: [0xAA; 16],
2699                hops: 3,
2700                expires: 999.0,
2701                random_blobs: Vec::new(),
2702                receiving_interface: InterfaceId(2),
2703                packet_hash: [0xCC; 32],
2704                announce_raw: None,
2705            },
2706        );
2707
2708        let mut key_rng = rns_crypto::FixedRng::new(&[0x51; 128]);
2709        let signing_key = Ed25519PrivateKey::generate(&mut key_rng);
2710        let signing_public = signing_key.public_key().public_bytes();
2711        let proof = crate::link::handshake::build_lrproof(
2712            &link_id,
2713            &[0x22; 32],
2714            &signing_public,
2715            &signing_key,
2716            None,
2717            crate::link::LinkMode::Aes256Cbc,
2718        );
2719        (engine, link_id, destination_hash, signing_public, proof)
2720    }
2721
2722    #[test]
2723    fn valid_lrproof_rebalances_relay_link_and_destination_path() {
2724        let (mut engine, link_id, destination_hash, signing_public, proof) =
2725            lrproof_rebalance_fixture();
2726
2727        assert!(engine.rebalance_link_path_from_lrproof(
2728            &link_id,
2729            5,
2730            InterfaceId(2),
2731            &proof,
2732            &signing_public,
2733        ));
2734
2735        assert_eq!(engine.link_table[&link_id].remaining_hops, 5);
2736        assert_eq!(engine.hops_to(&destination_hash), Some(5));
2737
2738        let packet = RawPacket::pack(
2739            PacketFlags {
2740                header_type: constants::HEADER_1,
2741                context_flag: constants::FLAG_UNSET,
2742                transport_type: constants::TRANSPORT_BROADCAST,
2743                destination_type: constants::DESTINATION_LINK,
2744                packet_type: constants::PACKET_TYPE_PROOF,
2745            },
2746            4,
2747            &link_id,
2748            None,
2749            constants::CONTEXT_LRPROOF,
2750            &proof,
2751        )
2752        .unwrap();
2753        let mut inbound_rng = rns_crypto::FixedRng::new(&[0x61; 32]);
2754        let actions = engine.handle_inbound(
2755            InboundFrame {
2756                raw: &packet.raw,
2757                iface: InterfaceId(2),
2758                now: 101.0,
2759                rx: RxMetadata::default(),
2760            },
2761            &mut inbound_rng,
2762        );
2763        assert!(engine.link_table[&link_id].validated);
2764        assert!(actions.iter().any(|action| matches!(
2765            action,
2766            TransportAction::SendOnInterface {
2767                interface: InterfaceId(1),
2768                ..
2769            }
2770        )));
2771    }
2772
2773    #[test]
2774    fn relay_rebalance_rejects_invalid_signature_wrong_interface_and_validated_link() {
2775        let (mut engine, link_id, destination_hash, signing_public, mut proof) =
2776            lrproof_rebalance_fixture();
2777        proof[0] ^= 0x01;
2778        assert!(!engine.rebalance_link_path_from_lrproof(
2779            &link_id,
2780            5,
2781            InterfaceId(2),
2782            &proof,
2783            &signing_public,
2784        ));
2785        assert_eq!(engine.link_table[&link_id].remaining_hops, 3);
2786        assert_eq!(engine.hops_to(&destination_hash), Some(3));
2787
2788        let (_, _, _, _, valid_proof) = lrproof_rebalance_fixture();
2789        assert!(!engine.rebalance_link_path_from_lrproof(
2790            &link_id,
2791            5,
2792            InterfaceId(1),
2793            &valid_proof,
2794            &signing_public,
2795        ));
2796        engine.validate_link(&link_id);
2797        assert!(!engine.rebalance_link_path_from_lrproof(
2798            &link_id,
2799            5,
2800            InterfaceId(2),
2801            &valid_proof,
2802            &signing_public,
2803        ));
2804    }
2805
2806    #[test]
2807    fn relay_rebalance_candidate_rejects_proof_addressed_to_another_transport() {
2808        let (engine, link_id, _, _, proof) = lrproof_rebalance_fixture();
2809        let packet = RawPacket::pack(
2810            PacketFlags {
2811                header_type: constants::HEADER_2,
2812                context_flag: constants::FLAG_UNSET,
2813                transport_type: constants::TRANSPORT_TRANSPORT,
2814                destination_type: constants::DESTINATION_LINK,
2815                packet_type: constants::PACKET_TYPE_PROOF,
2816            },
2817            4,
2818            &link_id,
2819            Some(&[0x43; 16]),
2820            constants::CONTEXT_LRPROOF,
2821            &proof,
2822        )
2823        .unwrap();
2824
2825        assert!(engine
2826            .inbound_lrproof_rebalance_candidate(&packet.raw, InterfaceId(2))
2827            .is_none());
2828    }
2829
2830    #[test]
2831    fn lrproof_hop_mismatch_diagnostic_contains_complete_route_context() {
2832        let entry = LinkEntry {
2833            timestamp: 100.0,
2834            next_hop_transport_id: [0; 16],
2835            next_hop_interface: InterfaceId(17),
2836            remaining_hops: 3,
2837            received_interface: InterfaceId(29),
2838            taken_hops: 5,
2839            destination_hash: [0xAA; 16],
2840            validated: false,
2841            proof_timeout: 200.0,
2842        };
2843
2844        assert_eq!(
2845            lrproof_hop_mismatch_diagnostic(9, &entry),
2846            "Received link request proof with hop mismatch (9/3:17->29), not transporting it"
2847        );
2848    }
2849
2850    #[test]
2851    fn lrproof_hop_mismatch_diagnostic_does_not_confuse_hops_with_interface_ids() {
2852        let entry = LinkEntry {
2853            timestamp: 100.0,
2854            next_hop_transport_id: [0; 16],
2855            next_hop_interface: InterfaceId(u64::MAX - 1),
2856            remaining_hops: u8::MAX,
2857            received_interface: InterfaceId(u64::MAX),
2858            taken_hops: 0,
2859            destination_hash: [0xAA; 16],
2860            validated: false,
2861            proof_timeout: 200.0,
2862        };
2863
2864        assert_eq!(
2865            lrproof_hop_mismatch_diagnostic(0, &entry),
2866            alloc::format!(
2867                "Received link request proof with hop mismatch (0/255:{}->{}), not transporting it",
2868                u64::MAX - 1,
2869                u64::MAX
2870            )
2871        );
2872    }
2873
2874    #[test]
2875    fn test_packet_filter_drops_plain_announce() {
2876        let engine = TransportEngine::new(make_config(false));
2877        let flags = PacketFlags {
2878            header_type: constants::HEADER_1,
2879            context_flag: constants::FLAG_UNSET,
2880            transport_type: constants::TRANSPORT_BROADCAST,
2881            destination_type: constants::DESTINATION_PLAIN,
2882            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2883        };
2884        let packet =
2885            RawPacket::pack(flags, 0, &[0; 16], None, constants::CONTEXT_NONE, b"test").unwrap();
2886        assert!(!engine.packet_filter(&packet));
2887    }
2888
2889    #[test]
2890    fn test_packet_filter_allows_keepalive() {
2891        let engine = TransportEngine::new(make_config(false));
2892        let flags = PacketFlags {
2893            header_type: constants::HEADER_1,
2894            context_flag: constants::FLAG_UNSET,
2895            transport_type: constants::TRANSPORT_BROADCAST,
2896            destination_type: constants::DESTINATION_SINGLE,
2897            packet_type: constants::PACKET_TYPE_DATA,
2898        };
2899        let packet = RawPacket::pack(
2900            flags,
2901            0,
2902            &[0; 16],
2903            None,
2904            constants::CONTEXT_KEEPALIVE,
2905            b"test",
2906        )
2907        .unwrap();
2908        assert!(engine.packet_filter(&packet));
2909    }
2910
2911    #[test]
2912    fn test_packet_filter_drops_high_hop_plain() {
2913        let engine = TransportEngine::new(make_config(false));
2914        let flags = PacketFlags {
2915            header_type: constants::HEADER_1,
2916            context_flag: constants::FLAG_UNSET,
2917            transport_type: constants::TRANSPORT_BROADCAST,
2918            destination_type: constants::DESTINATION_PLAIN,
2919            packet_type: constants::PACKET_TYPE_DATA,
2920        };
2921        let mut packet =
2922            RawPacket::pack(flags, 0, &[0; 16], None, constants::CONTEXT_NONE, b"test").unwrap();
2923        packet.hops = 2;
2924        assert!(!engine.packet_filter(&packet));
2925    }
2926
2927    #[test]
2928    fn test_packet_filter_allows_duplicate_single_announce() {
2929        let mut engine = TransportEngine::new(make_config(false));
2930        let flags = PacketFlags {
2931            header_type: constants::HEADER_1,
2932            context_flag: constants::FLAG_UNSET,
2933            transport_type: constants::TRANSPORT_BROADCAST,
2934            destination_type: constants::DESTINATION_SINGLE,
2935            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2936        };
2937        let packet = RawPacket::pack(
2938            flags,
2939            0,
2940            &[0; 16],
2941            None,
2942            constants::CONTEXT_NONE,
2943            &[0xAA; 64],
2944        )
2945        .unwrap();
2946
2947        // Add to hashlist
2948        engine.packet_hashlist.add(packet.packet_hash);
2949
2950        // Should still pass filter (duplicate announce for SINGLE allowed)
2951        assert!(engine.packet_filter(&packet));
2952    }
2953
2954    #[test]
2955    fn test_packet_filter_fifo_eviction_allows_oldest_hash_again() {
2956        let mut engine = TransportEngine::new(make_config(false));
2957        engine.packet_hashlist = PacketHashlist::new(2);
2958
2959        let make_packet = |seed: u8| {
2960            let flags = PacketFlags {
2961                header_type: constants::HEADER_1,
2962                context_flag: constants::FLAG_UNSET,
2963                transport_type: constants::TRANSPORT_BROADCAST,
2964                destination_type: constants::DESTINATION_SINGLE,
2965                packet_type: constants::PACKET_TYPE_DATA,
2966            };
2967            RawPacket::pack(
2968                flags,
2969                0,
2970                &[seed; 16],
2971                None,
2972                constants::CONTEXT_NONE,
2973                &[seed; 4],
2974            )
2975            .unwrap()
2976        };
2977
2978        let packet1 = make_packet(1);
2979        let packet2 = make_packet(2);
2980        let packet3 = make_packet(3);
2981
2982        engine.packet_hashlist.add(packet1.packet_hash);
2983        engine.packet_hashlist.add(packet2.packet_hash);
2984        assert!(!engine.packet_filter(&packet1));
2985
2986        engine.packet_hashlist.add(packet3.packet_hash);
2987
2988        assert!(engine.packet_filter(&packet1));
2989        assert!(!engine.packet_filter(&packet2));
2990        assert!(!engine.packet_filter(&packet3));
2991    }
2992
2993    #[test]
2994    fn test_packet_filter_duplicate_does_not_refresh_recency() {
2995        let mut engine = TransportEngine::new(make_config(false));
2996        engine.packet_hashlist = PacketHashlist::new(2);
2997
2998        let make_packet = |seed: u8| {
2999            let flags = PacketFlags {
3000                header_type: constants::HEADER_1,
3001                context_flag: constants::FLAG_UNSET,
3002                transport_type: constants::TRANSPORT_BROADCAST,
3003                destination_type: constants::DESTINATION_SINGLE,
3004                packet_type: constants::PACKET_TYPE_DATA,
3005            };
3006            RawPacket::pack(
3007                flags,
3008                0,
3009                &[seed; 16],
3010                None,
3011                constants::CONTEXT_NONE,
3012                &[seed; 4],
3013            )
3014            .unwrap()
3015        };
3016
3017        let packet1 = make_packet(1);
3018        let packet2 = make_packet(2);
3019        let packet3 = make_packet(3);
3020
3021        engine.packet_hashlist.add(packet1.packet_hash);
3022        engine.packet_hashlist.add(packet2.packet_hash);
3023        engine.packet_hashlist.add(packet2.packet_hash);
3024        engine.packet_hashlist.add(packet3.packet_hash);
3025
3026        assert!(engine.packet_filter(&packet1));
3027        assert!(!engine.packet_filter(&packet2));
3028        assert!(!engine.packet_filter(&packet3));
3029    }
3030
3031    #[test]
3032    fn test_tick_retransmits_announce() {
3033        let mut engine = TransportEngine::new(make_config(true));
3034        engine.register_interface(make_interface(1, constants::MODE_FULL));
3035
3036        let dest = [0x55; 16];
3037        engine.register_destination(dest, constants::DESTINATION_SINGLE);
3038        engine.insert_announce_entry(
3039            dest,
3040            AnnounceEntry {
3041                timestamp: 190.0,
3042                retransmit_timeout: 100.0, // ready to retransmit
3043                retries: 0,
3044                received_from: [0xAA; 16],
3045                hops: 2,
3046                packet_raw: vec![0x01, 0x02],
3047                packet_data: vec![0xCC; 10],
3048                destination_hash: dest,
3049                context_flag: 0,
3050                local_rebroadcasts: 0,
3051                block_rebroadcasts: false,
3052                attached_interface: None,
3053            },
3054            190.0,
3055        );
3056
3057        let mut rng = rns_crypto::FixedRng::new(&[0x42; 32]);
3058        let actions = engine.tick(200.0, &mut rng);
3059
3060        // Should have a send action for the retransmit (gated through announce queue,
3061        // expanded from BroadcastOnAllInterfaces to per-interface SendOnInterface)
3062        assert!(!actions.is_empty());
3063        assert!(matches!(
3064            &actions[0],
3065            TransportAction::SendOnInterface { .. }
3066        ));
3067
3068        // Retries should have increased
3069        assert_eq!(engine.announce_table[&dest].retries, 1);
3070    }
3071
3072    #[test]
3073    fn test_gate_retransmit_actions_expands_broadcast_to_matching_interfaces() {
3074        let mut engine = TransportEngine::new(make_config(false));
3075        engine.register_interface(make_interface(1, constants::MODE_FULL));
3076        engine.register_interface(make_interface(2, constants::MODE_FULL));
3077        engine.register_interface(make_interface(3, constants::MODE_ACCESS_POINT));
3078
3079        let dest = [0x56; 16];
3080        engine.register_destination(dest, constants::DESTINATION_SINGLE);
3081        let raw = make_announce_raw(&dest, &[0xAB; 32]);
3082        let actions = engine.gate_retransmit_actions(
3083            vec![TransportAction::BroadcastOnAllInterfaces {
3084                raw: raw.clone().into(),
3085                exclude: None,
3086            }],
3087            1000.0,
3088        );
3089
3090        assert_eq!(actions.len(), 2);
3091        for action in &actions {
3092            match action {
3093                TransportAction::SendOnInterface {
3094                    interface,
3095                    raw: sent,
3096                } => {
3097                    assert!(*interface == InterfaceId(1) || *interface == InterfaceId(2));
3098                    assert_eq!(&**sent, raw.as_slice());
3099                }
3100                other => panic!("expected SendOnInterface, got {:?}", other),
3101            }
3102        }
3103    }
3104
3105    #[test]
3106    fn test_tick_culls_expired_announce_entries() {
3107        let mut config = make_config(true);
3108        config.announce_table_ttl_secs = 10.0;
3109        let mut engine = TransportEngine::new(config);
3110
3111        let dest1 = [0x61; 16];
3112        let dest2 = [0x62; 16];
3113        assert!(engine.insert_announce_entry(dest1, make_announce_entry(dest1, 100.0, 8), 100.0));
3114        assert!(engine.insert_held_announce(dest2, make_announce_entry(dest2, 100.0, 8), 100.0));
3115
3116        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
3117        let _ = engine.tick(111.0, &mut rng);
3118
3119        assert!(!engine.announce_table().contains_key(&dest1));
3120        assert!(!engine.held_announces().contains_key(&dest2));
3121    }
3122
3123    #[test]
3124    fn test_announce_retention_cap_evicts_oldest_and_prefers_held_on_tie() {
3125        let sample_entry = make_announce_entry([0x70; 16], 100.0, 32);
3126        let mut config = make_config(true);
3127        config.announce_table_max_bytes = TransportEngine::announce_entry_size_bytes(&sample_entry)
3128            * 2
3129            + TransportEngine::announce_entry_size_bytes(&sample_entry) / 2;
3130        let max_bytes = config.announce_table_max_bytes;
3131        let mut engine = TransportEngine::new(config);
3132
3133        let held_dest = [0x71; 16];
3134        let active_dest = [0x72; 16];
3135        let newest_dest = [0x73; 16];
3136
3137        assert!(engine.insert_held_announce(
3138            held_dest,
3139            make_announce_entry(held_dest, 100.0, 32),
3140            100.0,
3141        ));
3142        assert!(engine.insert_announce_entry(
3143            active_dest,
3144            make_announce_entry(active_dest, 100.0, 32),
3145            100.0,
3146        ));
3147        assert!(engine.insert_announce_entry(
3148            newest_dest,
3149            make_announce_entry(newest_dest, 101.0, 32),
3150            101.0,
3151        ));
3152
3153        assert!(!engine.held_announces().contains_key(&held_dest));
3154        assert!(engine.announce_table().contains_key(&active_dest));
3155        assert!(engine.announce_table().contains_key(&newest_dest));
3156        assert!(engine.announce_retained_bytes() <= max_bytes);
3157    }
3158
3159    #[test]
3160    fn test_oversized_announce_entry_is_not_retained() {
3161        let mut config = make_config(true);
3162        config.announce_table_max_bytes = 200;
3163        let mut engine = TransportEngine::new(config);
3164        let dest = [0x81; 16];
3165
3166        assert!(!engine.insert_announce_entry(dest, make_announce_entry(dest, 100.0, 256), 100.0));
3167        assert!(!engine.announce_table().contains_key(&dest));
3168        assert_eq!(engine.announce_retained_bytes(), 0);
3169    }
3170
3171    #[test]
3172    fn test_void_queues_clears_shutdown_transients() {
3173        let mut engine = TransportEngine::new(make_config(true));
3174        engine.register_interface(make_interface(1, constants::MODE_FULL));
3175
3176        let active_dest = [0x91; 16];
3177        let held_dest = [0x92; 16];
3178        assert!(engine.insert_announce_entry(
3179            active_dest,
3180            make_announce_entry(active_dest, 100.0, 16),
3181            100.0,
3182        ));
3183        assert!(engine.insert_held_announce(
3184            held_dest,
3185            make_announce_entry(held_dest, 100.0, 16),
3186            100.0,
3187        ));
3188        engine.reverse_table.insert(
3189            [0x93; 16],
3190            tables::ReverseEntry {
3191                receiving_interface: InterfaceId(1),
3192                outbound_interface: InterfaceId(2),
3193                timestamp: 100.0,
3194            },
3195        );
3196        let _ = engine.announce_queues.gate_announce(
3197            InterfaceId(1),
3198            vec![0xAA; 32].into(),
3199            [0x94; 16],
3200            2,
3201            100.0,
3202            100.0,
3203            Some(1000),
3204            None,
3205            constants::ANNOUNCE_CAP,
3206        );
3207        let _ = engine.announce_queues.gate_announce(
3208            InterfaceId(1),
3209            vec![0xBB; 32].into(),
3210            [0x95; 16],
3211            3,
3212            100.0,
3213            100.0,
3214            Some(1000),
3215            None,
3216            constants::ANNOUNCE_CAP,
3217        );
3218
3219        assert_eq!(engine.announce_table_count(), 1);
3220        assert_eq!(engine.held_announces_count(), 1);
3221        assert_eq!(engine.reverse_table_count(), 1);
3222        assert_eq!(engine.queued_announce_count(), 1);
3223
3224        engine.void_queues();
3225
3226        assert_eq!(engine.announce_table_count(), 0);
3227        assert_eq!(engine.held_announces_count(), 0);
3228        assert_eq!(engine.reverse_table_count(), 0);
3229        assert_eq!(engine.queued_announce_count(), 0);
3230        assert_eq!(engine.nonempty_announce_queue_count(), 0);
3231        assert_eq!(engine.announce_retained_bytes(), 0);
3232    }
3233
3234    #[test]
3235    fn test_blackhole_identity() {
3236        let mut engine = TransportEngine::new(make_config(false));
3237        let hash = [0xAA; 16];
3238        let now = 1000.0;
3239
3240        assert!(!engine.is_blackholed(&hash, now));
3241
3242        engine.blackhole_identity(hash, now, None, Some(String::from("test")));
3243        assert!(engine.is_blackholed(&hash, now));
3244        assert!(engine.is_blackholed(&hash, now + 999999.0)); // never expires
3245
3246        assert!(engine.unblackhole_identity(&hash));
3247        assert!(!engine.is_blackholed(&hash, now));
3248        assert!(!engine.unblackhole_identity(&hash)); // already removed
3249    }
3250
3251    #[test]
3252    fn test_blackhole_with_duration() {
3253        let mut engine = TransportEngine::new(make_config(false));
3254        let hash = [0xBB; 16];
3255        let now = 1000.0;
3256
3257        engine.blackhole_identity(hash, now, Some(1.0), None); // 1 hour
3258        assert!(engine.is_blackholed(&hash, now));
3259        assert!(engine.is_blackholed(&hash, now + 3599.0)); // just before expiry
3260        assert!(!engine.is_blackholed(&hash, now + 3601.0)); // after expiry
3261    }
3262
3263    #[test]
3264    fn test_cull_blackholed() {
3265        let mut engine = TransportEngine::new(make_config(false));
3266        let hash1 = [0xCC; 16];
3267        let hash2 = [0xDD; 16];
3268        let now = 1000.0;
3269
3270        engine.blackhole_identity(hash1, now, Some(1.0), None); // 1 hour
3271        engine.blackhole_identity(hash2, now, None, None); // never expires
3272
3273        engine.cull_blackholed(now + 4000.0); // past hash1 expiry
3274
3275        assert!(!engine.blackholed_identities.contains_key(&hash1));
3276        assert!(engine.blackholed_identities.contains_key(&hash2));
3277    }
3278
3279    #[test]
3280    fn test_blackhole_blocks_announce() {
3281        use crate::announce::AnnounceData;
3282        use crate::destination::{destination_hash, name_hash};
3283
3284        let mut engine = TransportEngine::new(make_config(false));
3285        engine.register_interface(make_interface(1, constants::MODE_FULL));
3286
3287        let identity =
3288            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x55; 32]));
3289        let dest_hash = destination_hash("test", &["app"], Some(identity.hash()));
3290        let name_h = name_hash("test", &["app"]);
3291        let random_hash = [0x42u8; 10];
3292
3293        let (announce_data, _) =
3294            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
3295
3296        let flags = PacketFlags {
3297            header_type: constants::HEADER_1,
3298            context_flag: constants::FLAG_UNSET,
3299            transport_type: constants::TRANSPORT_BROADCAST,
3300            destination_type: constants::DESTINATION_SINGLE,
3301            packet_type: constants::PACKET_TYPE_ANNOUNCE,
3302        };
3303        let packet = RawPacket::pack(
3304            flags,
3305            0,
3306            &dest_hash,
3307            None,
3308            constants::CONTEXT_NONE,
3309            &announce_data,
3310        )
3311        .unwrap();
3312
3313        // Blackhole the identity
3314        let now = 1000.0;
3315        engine.blackhole_identity(*identity.hash(), now, None, None);
3316
3317        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
3318        let actions = engine.handle_inbound(
3319            InboundFrame {
3320                raw: &packet.raw,
3321                iface: InterfaceId(1),
3322                now,
3323                rx: RxMetadata {
3324                    rssi: None,
3325                    snr: None,
3326                },
3327            },
3328            &mut rng,
3329        );
3330
3331        // Should produce no AnnounceReceived or PathUpdated actions
3332        assert!(actions
3333            .iter()
3334            .all(|a| !matches!(a, TransportAction::AnnounceReceived { .. })));
3335        assert!(actions
3336            .iter()
3337            .all(|a| !matches!(a, TransportAction::PathUpdated { .. })));
3338    }
3339
3340    #[test]
3341    fn test_async_announce_retransmit_cleanup_happens_before_queueing() {
3342        use crate::announce::AnnounceData;
3343        use crate::destination::{destination_hash, name_hash};
3344        use crate::transport::announce_verify_queue::AnnounceVerifyQueue;
3345
3346        let mut engine = TransportEngine::new(make_config(true));
3347        engine.register_interface(make_interface(1, constants::MODE_FULL));
3348
3349        let identity =
3350            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x31; 32]));
3351        let dest_hash = destination_hash("async", &["announce"], Some(identity.hash()));
3352        let name_h = name_hash("async", &["announce"]);
3353        let random_hash = [0x44u8; 10];
3354        let (announce_data, _) =
3355            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
3356
3357        let packet = RawPacket::pack(
3358            PacketFlags {
3359                header_type: constants::HEADER_2,
3360                context_flag: constants::FLAG_UNSET,
3361                transport_type: constants::TRANSPORT_TRANSPORT,
3362                destination_type: constants::DESTINATION_SINGLE,
3363                packet_type: constants::PACKET_TYPE_ANNOUNCE,
3364            },
3365            3,
3366            &dest_hash,
3367            Some(&[0xBB; 16]),
3368            constants::CONTEXT_NONE,
3369            &announce_data,
3370        )
3371        .unwrap();
3372
3373        engine.announce_table.insert(
3374            dest_hash,
3375            AnnounceEntry {
3376                timestamp: 1000.0,
3377                retransmit_timeout: 2000.0,
3378                retries: constants::PATHFINDER_R,
3379                received_from: [0xBB; 16],
3380                hops: 2,
3381                packet_raw: packet.raw.clone(),
3382                packet_data: packet.data.clone(),
3383                destination_hash: dest_hash,
3384                context_flag: constants::FLAG_UNSET,
3385                local_rebroadcasts: 0,
3386                block_rebroadcasts: false,
3387                attached_interface: None,
3388            },
3389        );
3390
3391        let mut queue = AnnounceVerifyQueue::new(8);
3392        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
3393        let actions = engine.handle_inbound_with_announce_queue(
3394            InboundFrame {
3395                raw: &packet.raw,
3396                iface: InterfaceId(1),
3397                now: 1000.0,
3398                rx: RxMetadata {
3399                    rssi: None,
3400                    snr: None,
3401                },
3402            },
3403            &mut rng,
3404            Some(&mut queue),
3405        );
3406
3407        assert!(actions.is_empty());
3408        assert_eq!(queue.len(), 1);
3409        assert!(
3410            !engine.announce_table.contains_key(&dest_hash),
3411            "retransmit completion should clear announce_table before queueing"
3412        );
3413    }
3414
3415    #[test]
3416    fn test_async_announce_completion_inserts_sig_cache_and_prevents_requeue() {
3417        use crate::announce::AnnounceData;
3418        use crate::destination::{destination_hash, name_hash};
3419        use crate::transport::announce_verify_queue::AnnounceVerifyQueue;
3420
3421        let mut engine = TransportEngine::new(make_config(false));
3422        engine.register_interface(make_interface(1, constants::MODE_FULL));
3423
3424        let identity =
3425            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x52; 32]));
3426        let dest_hash = destination_hash("async", &["cache"], Some(identity.hash()));
3427        let name_h = name_hash("async", &["cache"]);
3428        let random_hash = [0x55u8; 10];
3429        let (announce_data, _) =
3430            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
3431
3432        let packet = RawPacket::pack(
3433            PacketFlags {
3434                header_type: constants::HEADER_1,
3435                context_flag: constants::FLAG_UNSET,
3436                transport_type: constants::TRANSPORT_BROADCAST,
3437                destination_type: constants::DESTINATION_SINGLE,
3438                packet_type: constants::PACKET_TYPE_ANNOUNCE,
3439            },
3440            0,
3441            &dest_hash,
3442            None,
3443            constants::CONTEXT_NONE,
3444            &announce_data,
3445        )
3446        .unwrap();
3447
3448        let mut queue = AnnounceVerifyQueue::new(8);
3449        let mut rng = rns_crypto::FixedRng::new(&[0x77; 32]);
3450        let actions = engine.handle_inbound_with_announce_queue(
3451            InboundFrame {
3452                raw: &packet.raw,
3453                iface: InterfaceId(1),
3454                now: 1000.0,
3455                rx: RxMetadata {
3456                    rssi: None,
3457                    snr: None,
3458                },
3459            },
3460            &mut rng,
3461            Some(&mut queue),
3462        );
3463        assert!(actions.is_empty());
3464        assert_eq!(queue.len(), 1);
3465
3466        let mut batch = queue.take_pending(1000.0);
3467        assert_eq!(batch.len(), 1);
3468        let (key, pending) = batch.pop().unwrap();
3469
3470        let announce = AnnounceData::unpack(&pending.packet.data, false).unwrap();
3471        let validated = announce.validate(&pending.packet.destination_hash).unwrap();
3472        let mut material = [0u8; 80];
3473        material[..16].copy_from_slice(&pending.packet.destination_hash);
3474        material[16..].copy_from_slice(&announce.signature);
3475        let sig_cache_key = hash::full_hash(&material);
3476
3477        let pending = queue.complete_success(&key).unwrap();
3478        let actions =
3479            engine.complete_verified_announce(pending, validated, sig_cache_key, 1000.0, &mut rng);
3480        assert!(actions
3481            .iter()
3482            .any(|action| matches!(action, TransportAction::AnnounceReceived { .. })));
3483        assert!(engine.announce_sig_cache_contains(&sig_cache_key));
3484
3485        let actions = engine.handle_inbound_with_announce_queue(
3486            InboundFrame {
3487                raw: &packet.raw,
3488                iface: InterfaceId(1),
3489                now: 1001.0,
3490                rx: RxMetadata {
3491                    rssi: None,
3492                    snr: None,
3493                },
3494            },
3495            &mut rng,
3496            Some(&mut queue),
3497        );
3498        assert!(actions.is_empty());
3499        assert_eq!(queue.len(), 0);
3500    }
3501
3502    #[test]
3503    fn test_tick_culls_expired_path() {
3504        let mut engine = TransportEngine::new(make_config(false));
3505        engine.register_interface(make_interface(1, constants::MODE_FULL));
3506
3507        let dest = [0x66; 16];
3508        engine.path_table.insert(
3509            dest,
3510            PathSet::from_single(
3511                PathEntry {
3512                    timestamp: 100.0,
3513                    next_hop: [0; 16],
3514                    hops: 2,
3515                    expires: 200.0,
3516                    random_blobs: Vec::new(),
3517                    receiving_interface: InterfaceId(1),
3518                    packet_hash: [0; 32],
3519                    announce_raw: None,
3520                },
3521                1,
3522            ),
3523        );
3524
3525        assert!(engine.has_path(&dest));
3526
3527        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3528        // Advance past cull interval and path expiry
3529        engine.tick(300.0, &mut rng);
3530
3531        assert!(!engine.has_path(&dest));
3532    }
3533
3534    // =========================================================================
3535    // Phase 7b: Local client transport tests
3536    // =========================================================================
3537
3538    fn make_local_client_interface(id: u64) -> InterfaceInfo {
3539        InterfaceInfo {
3540            id: InterfaceId(id),
3541            name: String::from("local_client"),
3542            mode: constants::MODE_FULL,
3543            gravity: 0,
3544            recursive_prs: false,
3545            announces_from_internal: true,
3546            announces_to_internal: None,
3547            out_capable: true,
3548            in_capable: true,
3549            bitrate: None,
3550            airtime_profile: None,
3551            announce_rate_target: None,
3552            announce_rate_grace: 0,
3553            announce_rate_penalty: 0.0,
3554            announce_cap: constants::ANNOUNCE_CAP,
3555            is_local_client: true,
3556            wants_tunnel: false,
3557            tunnel_id: None,
3558            mtu: constants::MTU as u32,
3559            ingress_control: crate::transport::types::IngressControlConfig::disabled(),
3560            ia_freq: 0.0,
3561            ip_freq: 0.0,
3562            op_freq: 0.0,
3563            op_samples: 0,
3564            started: 0.0,
3565        }
3566    }
3567
3568    #[test]
3569    fn test_has_local_clients() {
3570        let mut engine = TransportEngine::new(make_config(false));
3571        assert!(!engine.has_local_clients());
3572
3573        engine.register_interface(make_interface(1, constants::MODE_FULL));
3574        assert!(!engine.has_local_clients());
3575
3576        engine.register_interface(make_local_client_interface(2));
3577        assert!(engine.has_local_clients());
3578
3579        engine.deregister_interface(InterfaceId(2));
3580        assert!(!engine.has_local_clients());
3581    }
3582
3583    #[test]
3584    fn test_local_client_hop_decrement() {
3585        // Packets from local clients should have their hops decremented
3586        // to cancel the standard +1 (net zero change)
3587        let mut engine = TransportEngine::new(make_config(false));
3588        engine.register_interface(make_local_client_interface(1));
3589        engine.register_interface(make_interface(2, constants::MODE_FULL));
3590
3591        // Register destination so we get a DeliverLocal action
3592        let dest = [0xAA; 16];
3593        engine.register_destination(dest, constants::DESTINATION_PLAIN);
3594
3595        let flags = PacketFlags {
3596            header_type: constants::HEADER_1,
3597            context_flag: constants::FLAG_UNSET,
3598            transport_type: constants::TRANSPORT_BROADCAST,
3599            destination_type: constants::DESTINATION_PLAIN,
3600            packet_type: constants::PACKET_TYPE_DATA,
3601        };
3602        // Pack with hops=0
3603        let packet =
3604            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
3605
3606        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3607        let actions = engine.handle_inbound(
3608            InboundFrame {
3609                raw: &packet.raw,
3610                iface: InterfaceId(1),
3611                now: 1000.0,
3612                rx: RxMetadata {
3613                    rssi: None,
3614                    snr: None,
3615                },
3616            },
3617            &mut rng,
3618        );
3619
3620        // Should have local delivery; hops should still be 0 (not 1)
3621        // because the local client decrement cancels the increment
3622        let deliver = actions
3623            .iter()
3624            .find(|a| matches!(a, TransportAction::DeliverLocal { .. }));
3625        assert!(deliver.is_some(), "Should deliver locally");
3626    }
3627
3628    #[test]
3629    fn lrrtt_local_delivery_preserves_post_ingress_hops() {
3630        let mut engine = TransportEngine::new(make_config(false));
3631        engine.register_interface(make_interface(1, constants::MODE_FULL));
3632        let link_id = [0x4c; 16];
3633        engine.register_destination(link_id, constants::DESTINATION_LINK);
3634        let packet = RawPacket::pack(
3635            PacketFlags {
3636                header_type: constants::HEADER_1,
3637                context_flag: constants::FLAG_UNSET,
3638                transport_type: constants::TRANSPORT_BROADCAST,
3639                destination_type: constants::DESTINATION_LINK,
3640                packet_type: constants::PACKET_TYPE_DATA,
3641            },
3642            0,
3643            &link_id,
3644            None,
3645            constants::CONTEXT_LRRTT,
3646            b"authenticated ciphertext",
3647        )
3648        .unwrap();
3649        let actions = engine.handle_inbound(
3650            InboundFrame {
3651                raw: &packet.raw,
3652                iface: InterfaceId(1),
3653                now: 1.0,
3654                rx: RxMetadata::default(),
3655            },
3656            &mut rns_crypto::FixedRng::new(&[0; 32]),
3657        );
3658        let raw = actions
3659            .iter()
3660            .find_map(|action| match action {
3661                TransportAction::DeliverLocal { raw, .. } => Some(raw),
3662                _ => None,
3663            })
3664            .expect("LRRTT should be delivered locally");
3665        assert_eq!(RawPacket::unpack(raw).unwrap().hops, 1);
3666    }
3667
3668    #[test]
3669    fn test_prepare_inbound_packet_only_retains_original_raw_for_announces() {
3670        let engine = TransportEngine::new(make_config(false));
3671        let dest = [0xAB; 16];
3672        let flags = PacketFlags {
3673            header_type: constants::HEADER_1,
3674            context_flag: constants::FLAG_UNSET,
3675            transport_type: constants::TRANSPORT_BROADCAST,
3676            destination_type: constants::DESTINATION_SINGLE,
3677            packet_type: constants::PACKET_TYPE_DATA,
3678        };
3679        let packet =
3680            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
3681
3682        let ctx = engine
3683            .prepare_inbound_packet(InboundFrame {
3684                raw: &packet.raw,
3685                iface: InterfaceId(9),
3686                now: 1000.0,
3687                rx: RxMetadata {
3688                    rssi: None,
3689                    snr: None,
3690                },
3691            })
3692            .expect("packet should parse and pass filter");
3693
3694        assert!(ctx.original_raw.is_none());
3695        assert_eq!(ctx.packet.raw, packet.raw);
3696        assert_eq!(ctx.packet.hops, 1);
3697        assert_eq!(ctx.iface, InterfaceId(9));
3698
3699        let announce_flags = PacketFlags {
3700            packet_type: constants::PACKET_TYPE_ANNOUNCE,
3701            ..flags
3702        };
3703        let announce = RawPacket::pack(
3704            announce_flags,
3705            0,
3706            &dest,
3707            None,
3708            constants::CONTEXT_NONE,
3709            &[0u8; 91],
3710        )
3711        .unwrap();
3712        let announce_ctx = engine
3713            .prepare_inbound_packet(InboundFrame {
3714                raw: &announce.raw,
3715                iface: InterfaceId(9),
3716                now: 1000.0,
3717                rx: RxMetadata {
3718                    rssi: None,
3719                    snr: None,
3720                },
3721            })
3722            .expect("announce should parse and pass filter");
3723        assert_eq!(
3724            announce_ctx.original_raw.as_deref(),
3725            Some(announce.raw.as_slice())
3726        );
3727    }
3728
3729    #[test]
3730    fn test_deliver_local_preserves_original_raw_and_metadata() {
3731        let mut engine = TransportEngine::new(make_config(false));
3732        engine.register_interface(make_interface(1, constants::MODE_FULL));
3733
3734        let dest = [0xAC; 16];
3735        engine.register_destination(dest, constants::DESTINATION_SINGLE);
3736
3737        let flags = PacketFlags {
3738            header_type: constants::HEADER_1,
3739            context_flag: constants::FLAG_UNSET,
3740            transport_type: constants::TRANSPORT_BROADCAST,
3741            destination_type: constants::DESTINATION_SINGLE,
3742            packet_type: constants::PACKET_TYPE_DATA,
3743        };
3744        let packet =
3745            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"deliver").unwrap();
3746
3747        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3748        let actions = engine.handle_inbound(
3749            InboundFrame {
3750                raw: &packet.raw,
3751                iface: InterfaceId(1),
3752                now: 1000.0,
3753                rx: RxMetadata {
3754                    rssi: None,
3755                    snr: None,
3756                },
3757            },
3758            &mut rng,
3759        );
3760
3761        let deliver = actions
3762            .iter()
3763            .find_map(|action| match action {
3764                TransportAction::DeliverLocal {
3765                    destination_hash,
3766                    raw,
3767                    packet_hash,
3768                    receiving_interface,
3769                } => Some((destination_hash, raw, packet_hash, receiving_interface)),
3770                _ => None,
3771            })
3772            .expect("should produce DeliverLocal");
3773
3774        assert_eq!(*deliver.0, dest);
3775        assert_eq!(&**deliver.1, packet.raw.as_slice());
3776        assert_eq!(*deliver.2, packet.packet_hash);
3777        assert_eq!(*deliver.3, InterfaceId(1));
3778    }
3779
3780    #[test]
3781    fn local_lrproof_delivery_carries_post_ingress_hops() {
3782        let mut engine = TransportEngine::new(make_config(false));
3783        engine.register_interface(make_interface(1, constants::MODE_FULL));
3784        let link_id = [0x4C; 16];
3785        let packet = RawPacket::pack(
3786            PacketFlags {
3787                header_type: constants::HEADER_1,
3788                context_flag: constants::FLAG_UNSET,
3789                transport_type: constants::TRANSPORT_BROADCAST,
3790                destination_type: constants::DESTINATION_LINK,
3791                packet_type: constants::PACKET_TYPE_PROOF,
3792            },
3793            4,
3794            &link_id,
3795            None,
3796            constants::CONTEXT_LRPROOF,
3797            &[0xAA; 96],
3798        )
3799        .unwrap();
3800        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
3801
3802        let actions = engine.handle_inbound(
3803            InboundFrame {
3804                raw: &packet.raw,
3805                iface: InterfaceId(1),
3806                now: 1000.0,
3807                rx: RxMetadata::default(),
3808            },
3809            &mut rng,
3810        );
3811        let delivered_raw = actions
3812            .iter()
3813            .find_map(|action| match action {
3814                TransportAction::DeliverLocal { raw, .. } => Some(&**raw),
3815                _ => None,
3816            })
3817            .expect("LRPROOF should be delivered to the pending link manager");
3818        assert_eq!(RawPacket::unpack(delivered_raw).unwrap().hops, 5);
3819    }
3820
3821    #[test]
3822    fn test_plain_broadcast_from_local_client() {
3823        // PLAIN broadcast from local client should forward to external interfaces
3824        let mut engine = TransportEngine::new(make_config(false));
3825        engine.register_interface(make_local_client_interface(1));
3826        engine.register_interface(make_interface(2, constants::MODE_FULL));
3827
3828        let dest = [0xBB; 16];
3829        let flags = PacketFlags {
3830            header_type: constants::HEADER_1,
3831            context_flag: constants::FLAG_UNSET,
3832            transport_type: constants::TRANSPORT_BROADCAST,
3833            destination_type: constants::DESTINATION_PLAIN,
3834            packet_type: constants::PACKET_TYPE_DATA,
3835        };
3836        let packet =
3837            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
3838
3839        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3840        let actions = engine.handle_inbound(
3841            InboundFrame {
3842                raw: &packet.raw,
3843                iface: InterfaceId(1),
3844                now: 1000.0,
3845                rx: RxMetadata {
3846                    rssi: None,
3847                    snr: None,
3848                },
3849            },
3850            &mut rng,
3851        );
3852
3853        // Should have ForwardPlainBroadcast to external (to_local=false)
3854        let forward = actions.iter().find(|a| {
3855            matches!(
3856                a,
3857                TransportAction::ForwardPlainBroadcast {
3858                    to_local: false,
3859                    ..
3860                }
3861            )
3862        });
3863        assert!(forward.is_some(), "Should forward to external interfaces");
3864    }
3865
3866    #[test]
3867    fn test_plain_broadcast_from_external() {
3868        // PLAIN broadcast from external should forward to local clients
3869        let mut engine = TransportEngine::new(make_config(false));
3870        engine.register_interface(make_local_client_interface(1));
3871        engine.register_interface(make_interface(2, constants::MODE_FULL));
3872
3873        let dest = [0xCC; 16];
3874        let flags = PacketFlags {
3875            header_type: constants::HEADER_1,
3876            context_flag: constants::FLAG_UNSET,
3877            transport_type: constants::TRANSPORT_BROADCAST,
3878            destination_type: constants::DESTINATION_PLAIN,
3879            packet_type: constants::PACKET_TYPE_DATA,
3880        };
3881        let packet =
3882            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
3883
3884        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3885        let actions = engine.handle_inbound(
3886            InboundFrame {
3887                raw: &packet.raw,
3888                iface: InterfaceId(2),
3889                now: 1000.0,
3890                rx: RxMetadata {
3891                    rssi: None,
3892                    snr: None,
3893                },
3894            },
3895            &mut rng,
3896        );
3897
3898        // Should have ForwardPlainBroadcast to local clients (to_local=true)
3899        let forward = actions.iter().find(|a| {
3900            matches!(
3901                a,
3902                TransportAction::ForwardPlainBroadcast { to_local: true, .. }
3903            )
3904        });
3905        assert!(forward.is_some(), "Should forward to local clients");
3906    }
3907
3908    #[test]
3909    fn test_no_plain_broadcast_bridging_without_local_clients() {
3910        // Without local clients, no bridging should happen
3911        let mut engine = TransportEngine::new(make_config(false));
3912        engine.register_interface(make_interface(1, constants::MODE_FULL));
3913        engine.register_interface(make_interface(2, constants::MODE_FULL));
3914
3915        let dest = [0xDD; 16];
3916        let flags = PacketFlags {
3917            header_type: constants::HEADER_1,
3918            context_flag: constants::FLAG_UNSET,
3919            transport_type: constants::TRANSPORT_BROADCAST,
3920            destination_type: constants::DESTINATION_PLAIN,
3921            packet_type: constants::PACKET_TYPE_DATA,
3922        };
3923        let packet =
3924            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
3925
3926        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3927        let actions = engine.handle_inbound(
3928            InboundFrame {
3929                raw: &packet.raw,
3930                iface: InterfaceId(1),
3931                now: 1000.0,
3932                rx: RxMetadata {
3933                    rssi: None,
3934                    snr: None,
3935                },
3936            },
3937            &mut rng,
3938        );
3939
3940        // No ForwardPlainBroadcast should be emitted
3941        let has_forward = actions
3942            .iter()
3943            .any(|a| matches!(a, TransportAction::ForwardPlainBroadcast { .. }));
3944        assert!(!has_forward, "No bridging without local clients");
3945    }
3946
3947    #[test]
3948    fn test_announce_forwarded_to_local_clients() {
3949        use crate::announce::AnnounceData;
3950        use crate::destination::{destination_hash, name_hash};
3951
3952        let mut engine = TransportEngine::new(make_config(true));
3953        engine.register_interface(make_interface(1, constants::MODE_FULL));
3954        engine.register_interface(make_local_client_interface(2));
3955
3956        let identity =
3957            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x77; 32]));
3958        let dest_hash = destination_hash("test", &["fwd"], Some(identity.hash()));
3959        let name_h = name_hash("test", &["fwd"]);
3960        let random_hash = [0x42u8; 10];
3961
3962        let (announce_data, _) =
3963            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
3964
3965        let flags = PacketFlags {
3966            header_type: constants::HEADER_1,
3967            context_flag: constants::FLAG_UNSET,
3968            transport_type: constants::TRANSPORT_BROADCAST,
3969            destination_type: constants::DESTINATION_SINGLE,
3970            packet_type: constants::PACKET_TYPE_ANNOUNCE,
3971        };
3972        let packet = RawPacket::pack(
3973            flags,
3974            0,
3975            &dest_hash,
3976            None,
3977            constants::CONTEXT_NONE,
3978            &announce_data,
3979        )
3980        .unwrap();
3981
3982        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
3983        let actions = engine.handle_inbound(
3984            InboundFrame {
3985                raw: &packet.raw,
3986                iface: InterfaceId(1),
3987                now: 1000.0,
3988                rx: RxMetadata {
3989                    rssi: None,
3990                    snr: None,
3991                },
3992            },
3993            &mut rng,
3994        );
3995
3996        // Should have ForwardToLocalClients since we have local clients
3997        let forward = actions
3998            .iter()
3999            .find(|a| matches!(a, TransportAction::ForwardToLocalClients { .. }));
4000        assert!(
4001            forward.is_some(),
4002            "Should forward announce to local clients"
4003        );
4004
4005        // The exclude should be the receiving interface
4006        match forward.unwrap() {
4007            TransportAction::ForwardToLocalClients { exclude, raw } => {
4008                assert_eq!(*exclude, Some(InterfaceId(1)));
4009                let flags = PacketFlags::unpack(raw[0]);
4010                assert_eq!(flags.header_type, constants::HEADER_2);
4011                assert_eq!(flags.transport_type, constants::TRANSPORT_TRANSPORT);
4012                assert_eq!(&raw[2..18], &[0x42; 16]);
4013            }
4014            _ => unreachable!(),
4015        }
4016    }
4017
4018    #[test]
4019    fn test_no_announce_forward_without_local_clients() {
4020        use crate::announce::AnnounceData;
4021        use crate::destination::{destination_hash, name_hash};
4022
4023        let mut engine = TransportEngine::new(make_config(false));
4024        engine.register_interface(make_interface(1, constants::MODE_FULL));
4025
4026        let identity =
4027            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x88; 32]));
4028        let dest_hash = destination_hash("test", &["nofwd"], Some(identity.hash()));
4029        let name_h = name_hash("test", &["nofwd"]);
4030        let random_hash = [0x42u8; 10];
4031
4032        let (announce_data, _) =
4033            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
4034
4035        let flags = PacketFlags {
4036            header_type: constants::HEADER_1,
4037            context_flag: constants::FLAG_UNSET,
4038            transport_type: constants::TRANSPORT_BROADCAST,
4039            destination_type: constants::DESTINATION_SINGLE,
4040            packet_type: constants::PACKET_TYPE_ANNOUNCE,
4041        };
4042        let packet = RawPacket::pack(
4043            flags,
4044            0,
4045            &dest_hash,
4046            None,
4047            constants::CONTEXT_NONE,
4048            &announce_data,
4049        )
4050        .unwrap();
4051
4052        let mut rng = rns_crypto::FixedRng::new(&[0x22; 32]);
4053        let actions = engine.handle_inbound(
4054            InboundFrame {
4055                raw: &packet.raw,
4056                iface: InterfaceId(1),
4057                now: 1000.0,
4058                rx: RxMetadata {
4059                    rssi: None,
4060                    snr: None,
4061                },
4062            },
4063            &mut rng,
4064        );
4065
4066        // No ForwardToLocalClients should be emitted
4067        let has_forward = actions
4068            .iter()
4069            .any(|a| matches!(a, TransportAction::ForwardToLocalClients { .. }));
4070        assert!(!has_forward, "No forward without local clients");
4071    }
4072
4073    #[test]
4074    fn test_local_client_exclude_from_forward() {
4075        use crate::announce::AnnounceData;
4076        use crate::destination::{destination_hash, name_hash};
4077
4078        let mut engine = TransportEngine::new(make_config(false));
4079        engine.register_interface(make_local_client_interface(1));
4080        engine.register_interface(make_local_client_interface(2));
4081
4082        let identity =
4083            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
4084        let dest_hash = destination_hash("test", &["excl"], Some(identity.hash()));
4085        let name_h = name_hash("test", &["excl"]);
4086        let random_hash = [0x42u8; 10];
4087
4088        let (announce_data, _) =
4089            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
4090
4091        let flags = PacketFlags {
4092            header_type: constants::HEADER_1,
4093            context_flag: constants::FLAG_UNSET,
4094            transport_type: constants::TRANSPORT_BROADCAST,
4095            destination_type: constants::DESTINATION_SINGLE,
4096            packet_type: constants::PACKET_TYPE_ANNOUNCE,
4097        };
4098        let packet = RawPacket::pack(
4099            flags,
4100            0,
4101            &dest_hash,
4102            None,
4103            constants::CONTEXT_NONE,
4104            &announce_data,
4105        )
4106        .unwrap();
4107
4108        let mut rng = rns_crypto::FixedRng::new(&[0x33; 32]);
4109        // Feed announce from local client 1
4110        let actions = engine.handle_inbound(
4111            InboundFrame {
4112                raw: &packet.raw,
4113                iface: InterfaceId(1),
4114                now: 1000.0,
4115                rx: RxMetadata {
4116                    rssi: None,
4117                    snr: None,
4118                },
4119            },
4120            &mut rng,
4121        );
4122
4123        // Should forward to local clients, excluding interface 1 (the sender)
4124        let forward = actions
4125            .iter()
4126            .find(|a| matches!(a, TransportAction::ForwardToLocalClients { .. }));
4127        assert!(forward.is_some());
4128        match forward.unwrap() {
4129            TransportAction::ForwardToLocalClients { exclude, .. } => {
4130                assert_eq!(*exclude, Some(InterfaceId(1)));
4131            }
4132            _ => unreachable!(),
4133        }
4134    }
4135
4136    // =========================================================================
4137    // Phase 7d: Tunnel tests
4138    // =========================================================================
4139
4140    fn make_tunnel_interface(id: u64) -> InterfaceInfo {
4141        InterfaceInfo {
4142            id: InterfaceId(id),
4143            name: String::from("tunnel_iface"),
4144            mode: constants::MODE_FULL,
4145            gravity: 0,
4146            recursive_prs: false,
4147            announces_from_internal: true,
4148            announces_to_internal: None,
4149            out_capable: true,
4150            in_capable: true,
4151            bitrate: None,
4152            airtime_profile: None,
4153            announce_rate_target: None,
4154            announce_rate_grace: 0,
4155            announce_rate_penalty: 0.0,
4156            announce_cap: constants::ANNOUNCE_CAP,
4157            is_local_client: false,
4158            wants_tunnel: true,
4159            tunnel_id: None,
4160            mtu: constants::MTU as u32,
4161            ingress_control: crate::transport::types::IngressControlConfig::disabled(),
4162            ia_freq: 0.0,
4163            ip_freq: 0.0,
4164            op_freq: 0.0,
4165            op_samples: 0,
4166            started: 0.0,
4167        }
4168    }
4169
4170    #[test]
4171    fn test_handle_tunnel_new() {
4172        let mut engine = TransportEngine::new(make_config(true));
4173        engine.register_interface(make_tunnel_interface(1));
4174
4175        let tunnel_id = [0xAA; 32];
4176        let actions = engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
4177
4178        // Should emit TunnelEstablished
4179        assert!(actions
4180            .iter()
4181            .any(|a| matches!(a, TransportAction::TunnelEstablished { .. })));
4182
4183        // Interface should now have tunnel_id set
4184        let info = engine.interface_info(&InterfaceId(1)).unwrap();
4185        assert_eq!(info.tunnel_id, Some(tunnel_id));
4186
4187        // Tunnel table should have the entry
4188        assert_eq!(engine.tunnel_table().len(), 1);
4189    }
4190
4191    #[test]
4192    fn test_announce_stores_tunnel_path() {
4193        use crate::announce::AnnounceData;
4194        use crate::destination::{destination_hash, name_hash};
4195
4196        let mut engine = TransportEngine::new(make_config(false));
4197        let mut iface = make_tunnel_interface(1);
4198        let tunnel_id = [0xBB; 32];
4199        iface.tunnel_id = Some(tunnel_id);
4200        engine.register_interface(iface);
4201
4202        // Create tunnel entry
4203        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
4204
4205        // Create and send an announce
4206        let identity =
4207            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xCC; 32]));
4208        let dest_hash = destination_hash("test", &["tunnel"], Some(identity.hash()));
4209        let name_h = name_hash("test", &["tunnel"]);
4210        let random_hash = [0x42u8; 10];
4211
4212        let (announce_data, _) =
4213            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
4214
4215        let flags = PacketFlags {
4216            header_type: constants::HEADER_1,
4217            context_flag: constants::FLAG_UNSET,
4218            transport_type: constants::TRANSPORT_BROADCAST,
4219            destination_type: constants::DESTINATION_SINGLE,
4220            packet_type: constants::PACKET_TYPE_ANNOUNCE,
4221        };
4222        let packet = RawPacket::pack(
4223            flags,
4224            0,
4225            &dest_hash,
4226            None,
4227            constants::CONTEXT_NONE,
4228            &announce_data,
4229        )
4230        .unwrap();
4231
4232        let mut rng = rns_crypto::FixedRng::new(&[0xDD; 32]);
4233        engine.handle_inbound(
4234            InboundFrame {
4235                raw: &packet.raw,
4236                iface: InterfaceId(1),
4237                now: 1000.0,
4238                rx: RxMetadata {
4239                    rssi: None,
4240                    snr: None,
4241                },
4242            },
4243            &mut rng,
4244        );
4245
4246        // Path should be in path table
4247        assert!(engine.has_path(&dest_hash));
4248
4249        // Path should also be in tunnel table
4250        let tunnel = engine.tunnel_table().get(&tunnel_id).unwrap();
4251        assert_eq!(tunnel.paths.len(), 1);
4252        assert!(tunnel.paths.contains_key(&dest_hash));
4253    }
4254
4255    #[test]
4256    fn test_tunnel_reattach_restores_paths() {
4257        let mut engine = TransportEngine::new(make_config(true));
4258        engine.register_interface(make_tunnel_interface(1));
4259
4260        let tunnel_id = [0xCC; 32];
4261        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
4262
4263        // Manually add a path to the tunnel
4264        let dest = [0xDD; 16];
4265        engine.tunnel_table.store_tunnel_path(
4266            &tunnel_id,
4267            dest,
4268            tunnel::TunnelPath {
4269                timestamp: 1000.0,
4270                received_from: [0xEE; 16],
4271                hops: 3,
4272                expires: 1000.0 + constants::DESTINATION_TIMEOUT,
4273                random_blobs: Vec::new(),
4274                packet_hash: [0xFF; 32],
4275            },
4276            1000.0,
4277            constants::DESTINATION_TIMEOUT,
4278            usize::MAX,
4279        );
4280
4281        // Void the tunnel interface (disconnect)
4282        engine.void_tunnel_interface(&tunnel_id);
4283
4284        // Remove path from path table to simulate it expiring
4285        engine.path_table.remove(&dest);
4286        assert!(!engine.has_path(&dest));
4287
4288        // Reattach tunnel on new interface
4289        engine.register_interface(make_interface(2, constants::MODE_FULL));
4290        let actions = engine.handle_tunnel(tunnel_id, InterfaceId(2), 2000.0);
4291
4292        // Should restore the path
4293        assert!(engine.has_path(&dest));
4294        let path = engine.path_table.get(&dest).unwrap().primary().unwrap();
4295        assert_eq!(path.hops, 3);
4296        assert_eq!(path.receiving_interface, InterfaceId(2));
4297
4298        // Should emit TunnelEstablished
4299        assert!(actions
4300            .iter()
4301            .any(|a| matches!(a, TransportAction::TunnelEstablished { .. })));
4302    }
4303
4304    #[test]
4305    fn test_active_packet_hashes_include_detached_tunnel_paths() {
4306        let mut engine = TransportEngine::new(make_config(true));
4307        engine.register_interface(make_tunnel_interface(1));
4308
4309        let tunnel_id = [0xCA; 32];
4310        let destination_hash = [0xDB; 16];
4311        let packet_hash = [0xEC; 32];
4312        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
4313        engine.tunnel_table.store_tunnel_path(
4314            &tunnel_id,
4315            destination_hash,
4316            tunnel::TunnelPath {
4317                timestamp: 1000.0,
4318                received_from: [0xFE; 16],
4319                hops: 2,
4320                expires: 1000.0 + constants::DESTINATION_TIMEOUT,
4321                random_blobs: Vec::new(),
4322                packet_hash,
4323            },
4324            1000.0,
4325            constants::DESTINATION_TIMEOUT,
4326            usize::MAX,
4327        );
4328        engine.void_tunnel_interface(&tunnel_id);
4329        engine.path_table.remove(&destination_hash);
4330
4331        let active_hashes = engine.active_packet_hashes();
4332        assert_eq!(active_hashes, vec![packet_hash]);
4333    }
4334
4335    #[test]
4336    fn test_tunnel_reattach_does_not_overwrite_newer_path() {
4337        let mut engine = TransportEngine::new(make_config(true));
4338        engine.register_interface(make_tunnel_interface(1));
4339
4340        let tunnel_id = [0xCD; 32];
4341        let dest = [0xDE; 16];
4342        let older_blob = make_random_blob(100);
4343        let newer_blob = make_random_blob(200);
4344
4345        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
4346        engine.tunnel_table.store_tunnel_path(
4347            &tunnel_id,
4348            dest,
4349            tunnel::TunnelPath {
4350                timestamp: 1000.0,
4351                received_from: [0xEE; 16],
4352                hops: 2,
4353                expires: 1000.0 + constants::DESTINATION_TIMEOUT,
4354                random_blobs: vec![older_blob],
4355                packet_hash: [0x11; 32],
4356            },
4357            1000.0,
4358            constants::DESTINATION_TIMEOUT,
4359            usize::MAX,
4360        );
4361        engine.void_tunnel_interface(&tunnel_id);
4362
4363        engine.path_table.insert(
4364            dest,
4365            PathSet::from_single(
4366                PathEntry {
4367                    timestamp: 1500.0,
4368                    next_hop: [0xAB; 16],
4369                    hops: 3,
4370                    expires: 1500.0 + constants::DESTINATION_TIMEOUT,
4371                    random_blobs: vec![newer_blob],
4372                    receiving_interface: InterfaceId(3),
4373                    packet_hash: [0x22; 32],
4374                    announce_raw: None,
4375                },
4376                1,
4377            ),
4378        );
4379
4380        engine.register_interface(make_interface(2, constants::MODE_FULL));
4381        engine.handle_tunnel(tunnel_id, InterfaceId(2), 2000.0);
4382
4383        let path = engine.path_table.get(&dest).unwrap().primary().unwrap();
4384        assert_eq!(path.next_hop, [0xAB; 16]);
4385        assert_eq!(path.hops, 3);
4386        assert_eq!(path.receiving_interface, InterfaceId(3));
4387        assert_eq!(path.random_blobs, vec![newer_blob]);
4388    }
4389
4390    #[test]
4391    fn test_void_tunnel_interface() {
4392        let mut engine = TransportEngine::new(make_config(true));
4393        engine.register_interface(make_tunnel_interface(1));
4394
4395        let tunnel_id = [0xDD; 32];
4396        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
4397
4398        // Verify tunnel has interface
4399        assert_eq!(
4400            engine.tunnel_table().get(&tunnel_id).unwrap().interface,
4401            Some(InterfaceId(1))
4402        );
4403
4404        engine.void_tunnel_interface(&tunnel_id);
4405
4406        // Interface voided, but tunnel still exists
4407        assert_eq!(engine.tunnel_table().len(), 1);
4408        assert_eq!(
4409            engine.tunnel_table().get(&tunnel_id).unwrap().interface,
4410            None
4411        );
4412    }
4413
4414    #[test]
4415    fn test_tick_culls_tunnels() {
4416        let mut engine = TransportEngine::new(make_config(true));
4417        engine.register_interface(make_tunnel_interface(1));
4418
4419        let tunnel_id = [0xEE; 32];
4420        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
4421        assert_eq!(engine.tunnel_table().len(), 1);
4422
4423        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
4424
4425        // Tick past DESTINATION_TIMEOUT + TABLES_CULL_INTERVAL
4426        engine.tick(
4427            1000.0 + constants::DESTINATION_TIMEOUT + constants::TABLES_CULL_INTERVAL + 1.0,
4428            &mut rng,
4429        );
4430
4431        assert_eq!(engine.tunnel_table().len(), 0);
4432    }
4433
4434    #[test]
4435    fn test_synthesize_tunnel() {
4436        let mut engine = TransportEngine::new(make_config(true));
4437        engine.register_interface(make_tunnel_interface(1));
4438
4439        let identity =
4440            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xFF; 32]));
4441        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
4442
4443        let actions = engine.synthesize_tunnel(&identity, InterfaceId(1), &mut rng);
4444
4445        // Should produce a TunnelSynthesize action
4446        assert_eq!(actions.len(), 1);
4447        match &actions[0] {
4448            TransportAction::TunnelSynthesize {
4449                interface,
4450                data,
4451                dest_hash,
4452            } => {
4453                assert_eq!(*interface, InterfaceId(1));
4454                assert_eq!(data.len(), tunnel::TUNNEL_SYNTH_LENGTH);
4455                // dest_hash should be the tunnel.synthesize plain destination
4456                let expected_dest = crate::destination::destination_hash(
4457                    "rnstransport",
4458                    &["tunnel", "synthesize"],
4459                    None,
4460                );
4461                assert_eq!(*dest_hash, expected_dest);
4462            }
4463            _ => panic!("Expected TunnelSynthesize"),
4464        }
4465    }
4466
4467    fn synthesized_interface_hash(actions: &[TransportAction]) -> [u8; 32] {
4468        let data = actions
4469            .iter()
4470            .find_map(|action| match action {
4471                TransportAction::TunnelSynthesize { data, .. } => Some(data),
4472                _ => None,
4473            })
4474            .expect("tunnel synthesis action");
4475        let mut interface_hash = [0u8; 32];
4476        interface_hash.copy_from_slice(&data[64..96]);
4477        interface_hash
4478    }
4479
4480    #[test]
4481    fn tunnel_synthesis_uses_hash_cached_at_interface_registration() {
4482        let mut engine = TransportEngine::new(make_config(true));
4483        let mut interface = make_tunnel_interface(1);
4484        interface.name = String::from("registered-name");
4485        engine.register_interface(interface);
4486
4487        // Interface display names are immutable in normal operation. Mutating
4488        // the stored metadata here distinguishes a registration-time cache
4489        // from a hash recalculated by every synthesis.
4490        engine.interfaces.get_mut(&InterfaceId(1)).unwrap().name = String::from("later-name");
4491
4492        let identity =
4493            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xFF; 32]));
4494        let actions = engine.synthesize_tunnel(
4495            &identity,
4496            InterfaceId(1),
4497            &mut rns_crypto::FixedRng::new(&[0x11; 32]),
4498        );
4499
4500        assert_eq!(
4501            synthesized_interface_hash(&actions),
4502            hash::full_hash(b"registered-name")
4503        );
4504    }
4505
4506    #[test]
4507    fn replacing_interface_id_refreshes_cached_tunnel_hash() {
4508        let mut engine = TransportEngine::new(make_config(true));
4509        let mut first = make_tunnel_interface(1);
4510        first.name = String::from("first-name");
4511        engine.register_interface(first);
4512        let mut replacement = make_tunnel_interface(1);
4513        replacement.name = String::from("replacement-name");
4514        engine.register_interface(replacement);
4515
4516        let identity =
4517            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xFF; 32]));
4518        let actions = engine.synthesize_tunnel(
4519            &identity,
4520            InterfaceId(1),
4521            &mut rns_crypto::FixedRng::new(&[0x11; 32]),
4522        );
4523
4524        assert_eq!(
4525            synthesized_interface_hash(&actions),
4526            hash::full_hash(b"replacement-name")
4527        );
4528        assert_ne!(
4529            synthesized_interface_hash(&actions),
4530            hash::full_hash(b"first-name")
4531        );
4532    }
4533
4534    #[test]
4535    fn test_synthesize_tunnel_missing_interface_is_dropped() {
4536        let engine = TransportEngine::new(make_config(true));
4537
4538        let identity =
4539            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xFF; 32]));
4540        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
4541
4542        let actions = engine.synthesize_tunnel(&identity, InterfaceId(99), &mut rng);
4543
4544        assert!(actions.is_empty());
4545    }
4546
4547    #[test]
4548    fn test_synthesize_tunnel_public_only_identity_is_dropped() {
4549        let mut engine = TransportEngine::new(make_config(true));
4550        engine.register_interface(make_tunnel_interface(1));
4551
4552        let identity =
4553            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xFF; 32]));
4554        let public_key = identity.get_public_key().unwrap();
4555        let public_only_identity = rns_crypto::identity::Identity::from_public_key(&public_key);
4556        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
4557
4558        let actions = engine.synthesize_tunnel(&public_only_identity, InterfaceId(1), &mut rng);
4559
4560        assert!(actions.is_empty());
4561    }
4562
4563    // =========================================================================
4564    // DISCOVER_PATHS_FOR tests
4565    // =========================================================================
4566
4567    fn make_path_request_data(dest_hash: &[u8; 16], tag: &[u8]) -> Vec<u8> {
4568        let mut data = Vec::new();
4569        data.extend_from_slice(dest_hash);
4570        data.extend_from_slice(tag);
4571        data
4572    }
4573
4574    fn make_transport_path_request_data(
4575        dest_hash: &[u8; 16],
4576        requestor_transport_id: &[u8; 16],
4577        tag: &[u8],
4578    ) -> Vec<u8> {
4579        let mut data = Vec::new();
4580        data.extend_from_slice(dest_hash);
4581        data.extend_from_slice(requestor_transport_id);
4582        data.extend_from_slice(tag);
4583        data
4584    }
4585
4586    fn assert_recursive_path_request_packet(raw: &[u8], dest: &[u8; 16], tag: &[u8]) {
4587        let packet = RawPacket::unpack(raw).expect("recursive path request packet");
4588        let path_request_dest =
4589            crate::destination::destination_hash("rnstransport", &["path", "request"], None);
4590
4591        assert_eq!(packet.flags.header_type, constants::HEADER_1);
4592        assert_eq!(packet.flags.transport_type, constants::TRANSPORT_BROADCAST);
4593        assert_eq!(packet.flags.destination_type, constants::DESTINATION_PLAIN);
4594        assert_eq!(packet.flags.packet_type, constants::PACKET_TYPE_DATA);
4595        assert_eq!(packet.hops, 0);
4596        assert_eq!(packet.context, constants::CONTEXT_NONE);
4597        assert_eq!(packet.destination_hash, path_request_dest);
4598
4599        let mut expected_data = Vec::new();
4600        expected_data.extend_from_slice(dest);
4601        expected_data.extend_from_slice(&[0x42; 16]);
4602        expected_data.extend_from_slice(tag);
4603        assert_eq!(packet.data, expected_data);
4604    }
4605
4606    #[test]
4607    fn test_path_request_forwarded_on_ap() {
4608        let mut engine = TransportEngine::new(make_config(true));
4609        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
4610        engine.register_interface(make_interface(2, constants::MODE_FULL));
4611
4612        let dest = [0xD1; 16];
4613        let tag = [0x01; 16];
4614        let data = make_path_request_data(&dest, &tag);
4615
4616        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4617
4618        // Should forward the path request on interface 2 (the other OUT interface)
4619        assert_eq!(actions.len(), 1);
4620        match &actions[0] {
4621            TransportAction::SendOnInterface { interface, .. } => {
4622                assert_eq!(*interface, InterfaceId(2));
4623            }
4624            _ => panic!("Expected SendOnInterface for forwarded path request"),
4625        }
4626        // Should have stored a discovery path request
4627        assert!(engine.discovery_path_requests.contains_key(&dest));
4628    }
4629
4630    #[test]
4631    fn test_recursive_path_request_rebuilds_transport_payload() {
4632        let mut engine = TransportEngine::new(make_config(true));
4633        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
4634        engine.register_interface(make_interface(2, constants::MODE_FULL));
4635
4636        let dest = [0xD8; 16];
4637        let original_requestor_transport_id = [0x99; 16];
4638        let tag = [0x08; 16];
4639        let data = make_transport_path_request_data(&dest, &original_requestor_transport_id, &tag);
4640
4641        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4642
4643        assert_eq!(actions.len(), 1);
4644        match &actions[0] {
4645            TransportAction::SendOnInterface { interface, raw } => {
4646                assert_eq!(*interface, InterfaceId(2));
4647                assert_recursive_path_request_packet(raw.as_ref(), &dest, &tag);
4648            }
4649            _ => panic!("expected SendOnInterface for recursive path request"),
4650        }
4651        assert!(engine.discovery_path_requests.contains_key(&dest));
4652    }
4653
4654    #[test]
4655    fn test_path_request_forwarded_on_internal() {
4656        let mut engine = TransportEngine::new(make_config(true));
4657        engine.register_interface(make_interface(1, constants::MODE_INTERNAL));
4658        engine.register_interface(make_interface(2, constants::MODE_FULL));
4659
4660        let dest = [0xDB; 16];
4661        let tag = [0x0B; 16];
4662        let data = make_path_request_data(&dest, &tag);
4663
4664        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4665
4666        assert_eq!(actions.len(), 1);
4667        match &actions[0] {
4668            TransportAction::SendOnInterface { interface, raw } => {
4669                assert_eq!(*interface, InterfaceId(2));
4670                assert_recursive_path_request_packet(raw.as_ref(), &dest, &tag);
4671            }
4672            _ => panic!("expected SendOnInterface for recursive path request"),
4673        }
4674        assert!(engine.discovery_path_requests.contains_key(&dest));
4675    }
4676
4677    #[test]
4678    fn boundary_path_request_searches_only_boundary_and_gateway_interfaces() {
4679        let mut engine = TransportEngine::new(make_config(true));
4680        engine.register_interface(make_interface(1, constants::MODE_BOUNDARY));
4681        engine.register_interface(make_interface(2, constants::MODE_BOUNDARY));
4682        engine.register_interface(make_interface(3, constants::MODE_GATEWAY));
4683        engine.register_interface(make_interface(4, constants::MODE_FULL));
4684        engine.register_interface(make_interface(5, constants::MODE_ACCESS_POINT));
4685        engine.register_interface(make_interface(6, constants::MODE_INTERNAL));
4686
4687        let dest = [0xBC; 16];
4688        let tag = [0x21; 16];
4689        let data = make_path_request_data(&dest, &tag);
4690
4691        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4692
4693        let interfaces: Vec<_> = actions
4694            .iter()
4695            .filter_map(|action| match action {
4696                TransportAction::SendOnInterface { interface, raw } => {
4697                    assert_recursive_path_request_packet(raw.as_ref(), &dest, &tag);
4698                    Some(*interface)
4699                }
4700                _ => None,
4701            })
4702            .collect();
4703        assert_eq!(interfaces, vec![InterfaceId(2), InterfaceId(3)]);
4704        assert!(engine.discovery_path_requests.contains_key(&dest));
4705    }
4706
4707    #[test]
4708    fn recursive_prs_on_boundary_keeps_unfiltered_egress_behavior() {
4709        let mut engine = TransportEngine::new(make_config(true));
4710        let mut ingress = make_interface(1, constants::MODE_BOUNDARY);
4711        ingress.recursive_prs = true;
4712        engine.register_interface(ingress);
4713        engine.register_interface(make_interface(2, constants::MODE_FULL));
4714
4715        let dest = [0xBD; 16];
4716        let tag = [0x22; 16];
4717        let data = make_path_request_data(&dest, &tag);
4718
4719        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4720
4721        assert_eq!(actions.len(), 1);
4722        match &actions[0] {
4723            TransportAction::SendOnInterface { interface, raw } => {
4724                assert_eq!(*interface, InterfaceId(2));
4725                assert_recursive_path_request_packet(raw.as_ref(), &dest, &tag);
4726            }
4727            _ => panic!("expected SendOnInterface for recursive path request"),
4728        }
4729        assert!(engine.discovery_path_requests.contains_key(&dest));
4730    }
4731
4732    #[test]
4733    fn test_path_request_not_forwarded_on_full() {
4734        let mut engine = TransportEngine::new(make_config(true));
4735        engine.register_interface(make_interface(1, constants::MODE_FULL));
4736        engine.register_interface(make_interface(2, constants::MODE_FULL));
4737
4738        let dest = [0xD2; 16];
4739        let tag = [0x02; 16];
4740        let data = make_path_request_data(&dest, &tag);
4741
4742        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4743
4744        // MODE_FULL is not in DISCOVER_PATHS_FOR, so no forwarding
4745        assert!(actions.is_empty());
4746        assert!(!engine.discovery_path_requests.contains_key(&dest));
4747    }
4748
4749    #[test]
4750    fn test_path_request_forwarded_on_full_with_recursive_prs() {
4751        let mut engine = TransportEngine::new(make_config(true));
4752        let mut ingress = make_interface(1, constants::MODE_FULL);
4753        ingress.recursive_prs = true;
4754        engine.register_interface(ingress);
4755        engine.register_interface(make_interface(2, constants::MODE_FULL));
4756
4757        let dest = [0xD9; 16];
4758        let tag = [0x09; 16];
4759        let data = make_path_request_data(&dest, &tag);
4760
4761        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4762
4763        assert_eq!(actions.len(), 1);
4764        match &actions[0] {
4765            TransportAction::SendOnInterface { interface, raw } => {
4766                assert_eq!(*interface, InterfaceId(2));
4767                assert_recursive_path_request_packet(raw.as_ref(), &dest, &tag);
4768            }
4769            _ => panic!("expected SendOnInterface for recursive path request"),
4770        }
4771        assert!(engine.discovery_path_requests.contains_key(&dest));
4772    }
4773
4774    #[test]
4775    fn test_recursive_prs_still_obeys_ingress_control() {
4776        let mut engine = TransportEngine::new(make_config(true));
4777        let mut ingress = make_interface(1, constants::MODE_FULL);
4778        ingress.recursive_prs = true;
4779        let ingress_config = crate::transport::types::IngressControlConfig::enabled();
4780        ingress.ip_freq = ingress_config.pr_burst_freq_new + 1.0;
4781        ingress.ingress_control = ingress_config;
4782        ingress.started = 1000.0;
4783        engine.register_interface(ingress);
4784        engine.register_interface(make_interface(2, constants::MODE_FULL));
4785
4786        let dest = [0xDA; 16];
4787        let tag = [0x0A; 16];
4788        let data = make_path_request_data(&dest, &tag);
4789
4790        let actions = engine.handle_path_request(&data, InterfaceId(1), 1001.0);
4791
4792        assert!(actions.is_empty());
4793        assert!(!engine.discovery_path_requests.contains_key(&dest));
4794    }
4795
4796    #[test]
4797    fn test_duplicate_discovery_path_request_is_suppressed() {
4798        let mut engine = TransportEngine::new(make_config(true));
4799        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
4800        engine.register_interface(make_interface(2, constants::MODE_FULL));
4801
4802        let dest = [0xD7; 16];
4803        let tag = [0x07; 16];
4804        let data = make_path_request_data(&dest, &tag);
4805
4806        let first = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4807        let second = engine.handle_path_request(&data, InterfaceId(1), 1001.0);
4808
4809        assert_eq!(first.len(), 1);
4810        assert!(
4811            second.is_empty(),
4812            "duplicate discovery request should be dropped"
4813        );
4814        assert_eq!(engine.discovery_pr_tags_count(), 1);
4815    }
4816
4817    #[test]
4818    fn test_path_request_ingress_burst_suppresses_recursive_discovery() {
4819        let mut engine = TransportEngine::new(make_config(true));
4820        let mut ingress = make_interface(1, constants::MODE_ACCESS_POINT);
4821        ingress.ingress_control.enabled = true;
4822        ingress.ip_freq = constants::IC_PR_BURST_FREQ + 1.0;
4823        engine.register_interface(ingress);
4824        engine.register_interface(make_interface(2, constants::MODE_FULL));
4825
4826        let dest = [0xE1; 16];
4827        let tag = [0x11; 16];
4828        let data = make_path_request_data(&dest, &tag);
4829
4830        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4831
4832        assert!(actions.is_empty());
4833        assert!(!engine.discovery_path_requests.contains_key(&dest));
4834    }
4835
4836    #[test]
4837    fn test_path_request_egress_limit_skips_only_limited_interface() {
4838        let mut engine = TransportEngine::new(make_config(true));
4839        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
4840
4841        let mut limited = make_interface(2, constants::MODE_FULL);
4842        limited.ingress_control.egress_enabled = true;
4843        limited.op_freq = constants::EC_PR_FREQ + 1.0;
4844        limited.op_samples = constants::IC_BURST_MIN_SAMPLES;
4845        engine.register_interface(limited);
4846
4847        let mut allowed = make_interface(3, constants::MODE_FULL);
4848        allowed.ingress_control.egress_enabled = true;
4849        allowed.op_freq = constants::EC_PR_FREQ - 1.0;
4850        allowed.op_samples = constants::IC_BURST_MIN_SAMPLES;
4851        engine.register_interface(allowed);
4852
4853        let dest = [0xE2; 16];
4854        let tag = [0x12; 16];
4855        let data = make_path_request_data(&dest, &tag);
4856
4857        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4858
4859        assert_eq!(actions.len(), 1);
4860        match &actions[0] {
4861            TransportAction::SendOnInterface { interface, .. } => {
4862                assert_eq!(*interface, InterfaceId(3))
4863            }
4864            _ => panic!("expected SendOnInterface for the unlimited egress interface"),
4865        }
4866        assert!(engine.discovery_path_requests.contains_key(&dest));
4867    }
4868
4869    #[test]
4870    fn test_recursive_path_request_skips_interface_with_queued_announces() {
4871        let mut engine = TransportEngine::new(make_config(true));
4872        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
4873        let mut blocked = make_interface(2, constants::MODE_FULL);
4874        blocked.bitrate = Some(1_000);
4875        engine.register_interface(blocked);
4876        engine.register_interface(make_interface(3, constants::MODE_FULL));
4877
4878        let _ = engine.announce_queues.gate_announce(
4879            InterfaceId(2),
4880            vec![0xAA; 100].into(),
4881            [0xA0; 16],
4882            1,
4883            900.0,
4884            900.0,
4885            Some(1_000),
4886            None,
4887            constants::ANNOUNCE_CAP,
4888        );
4889        let _ = engine.announce_queues.gate_announce(
4890            InterfaceId(2),
4891            vec![0xBB; 100].into(),
4892            [0xB0; 16],
4893            1,
4894            901.0,
4895            901.0,
4896            Some(1_000),
4897            None,
4898            constants::ANNOUNCE_CAP,
4899        );
4900
4901        let dest = [0xE3; 16];
4902        let tag = [0x13; 16];
4903        let data = make_path_request_data(&dest, &tag);
4904        let actions = engine.handle_path_request(&data, InterfaceId(1), 902.0);
4905
4906        assert_eq!(actions.len(), 1);
4907        match &actions[0] {
4908            TransportAction::SendOnInterface { interface, .. } => {
4909                assert_eq!(*interface, InterfaceId(3));
4910            }
4911            _ => panic!("expected SendOnInterface for the unqueued egress interface"),
4912        }
4913        assert!(engine.discovery_path_requests.contains_key(&dest));
4914    }
4915
4916    #[test]
4917    fn test_recursive_path_request_skips_interface_with_active_announce_cap() {
4918        let mut engine = TransportEngine::new(make_config(true));
4919        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
4920        let mut blocked = make_interface(2, constants::MODE_FULL);
4921        blocked.bitrate = Some(1_000);
4922        engine.register_interface(blocked);
4923
4924        let _ = engine.announce_queues.gate_announce(
4925            InterfaceId(2),
4926            vec![0xAA; 100].into(),
4927            [0xA0; 16],
4928            1,
4929            900.0,
4930            900.0,
4931            Some(1_000),
4932            None,
4933            constants::ANNOUNCE_CAP,
4934        );
4935
4936        let dest = [0xE4; 16];
4937        let tag = [0x14; 16];
4938        let data = make_path_request_data(&dest, &tag);
4939        let actions = engine.handle_path_request(&data, InterfaceId(1), 901.0);
4940
4941        assert!(actions.is_empty());
4942        assert!(!engine.discovery_path_requests.contains_key(&dest));
4943    }
4944
4945    #[test]
4946    fn test_recursive_path_request_reserves_announce_cap_on_sent_interface() {
4947        let mut engine = TransportEngine::new(make_config(true));
4948        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
4949        let mut egress = make_interface(2, constants::MODE_FULL);
4950        egress.bitrate = Some(1_000);
4951        engine.register_interface(egress);
4952
4953        let dest = [0xE5; 16];
4954        let tag = [0x15; 16];
4955        let data = make_path_request_data(&dest, &tag);
4956        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
4957
4958        assert_eq!(actions.len(), 1);
4959        let queue = engine
4960            .announce_queues
4961            .queue_for(&InterfaceId(2))
4962            .expect("sent recursive PR should create announce-cap state");
4963        assert!(
4964            queue.announce_allowed_at > 1000.0,
4965            "recursive PR should reserve announce-cap airtime"
4966        );
4967        assert!(queue.entries.is_empty());
4968    }
4969
4970    #[test]
4971    fn test_discovery_pr_tags_fifo_eviction() {
4972        let mut config = make_config(true);
4973        config.max_discovery_pr_tags = 2;
4974        let mut engine = TransportEngine::new(config);
4975
4976        let dest1 = [0xA1; 16];
4977        let dest2 = [0xA2; 16];
4978        let dest3 = [0xA3; 16];
4979        let tag1 = [0x01; 16];
4980        let tag2 = [0x02; 16];
4981        let tag3 = [0x03; 16];
4982
4983        engine.handle_path_request(
4984            &make_path_request_data(&dest1, &tag1),
4985            InterfaceId(1),
4986            1000.0,
4987        );
4988        engine.handle_path_request(
4989            &make_path_request_data(&dest2, &tag2),
4990            InterfaceId(1),
4991            1001.0,
4992        );
4993        assert_eq!(engine.discovery_pr_tags_count(), 2);
4994
4995        let unique1 = make_unique_tag(dest1, &tag1);
4996        let unique2 = make_unique_tag(dest2, &tag2);
4997        assert!(engine.has_discovery_pr_tag(&unique1));
4998        assert!(engine.has_discovery_pr_tag(&unique2));
4999
5000        engine.handle_path_request(
5001            &make_path_request_data(&dest3, &tag3),
5002            InterfaceId(1),
5003            1002.0,
5004        );
5005        assert_eq!(engine.discovery_pr_tags_count(), 2);
5006        assert!(!engine.has_discovery_pr_tag(&unique1));
5007        assert!(engine.has_discovery_pr_tag(&unique2));
5008
5009        engine.handle_path_request(
5010            &make_path_request_data(&dest1, &tag1),
5011            InterfaceId(1),
5012            1003.0,
5013        );
5014        assert_eq!(engine.discovery_pr_tags_count(), 2);
5015        assert!(engine.has_discovery_pr_tag(&unique1));
5016    }
5017
5018    #[test]
5019    fn test_path_destination_cap_evicts_oldest_and_clears_state() {
5020        let mut config = make_config(false);
5021        config.max_path_destinations = 2;
5022        let mut engine = TransportEngine::new(config);
5023        engine.register_interface(make_interface(1, constants::MODE_FULL));
5024
5025        let dest1 = [0xB1; 16];
5026        let dest2 = [0xB2; 16];
5027        let dest3 = [0xB3; 16];
5028
5029        engine.upsert_path_destination(
5030            dest1,
5031            make_path_entry(1000.0, 1, InterfaceId(1), [0x11; 16]),
5032            1000.0,
5033        );
5034        engine.upsert_path_destination(
5035            dest2,
5036            make_path_entry(1001.0, 1, InterfaceId(1), [0x22; 16]),
5037            1001.0,
5038        );
5039        engine
5040            .path_states
5041            .insert(dest1, constants::STATE_UNRESPONSIVE);
5042
5043        engine.upsert_path_destination(
5044            dest3,
5045            make_path_entry(1002.0, 1, InterfaceId(1), [0x33; 16]),
5046            1002.0,
5047        );
5048
5049        assert_eq!(engine.path_table_count(), 2);
5050        assert!(!engine.has_path(&dest1));
5051        assert!(engine.has_path(&dest2));
5052        assert!(engine.has_path(&dest3));
5053        assert!(!engine.path_states.contains_key(&dest1));
5054        assert_eq!(engine.path_destination_cap_evict_count(), 1);
5055    }
5056
5057    #[test]
5058    fn test_existing_path_destination_update_does_not_trigger_cap_eviction() {
5059        let mut config = make_config(false);
5060        config.max_path_destinations = 2;
5061        config.max_paths_per_destination = 2;
5062        let mut engine = TransportEngine::new(config);
5063        engine.register_interface(make_interface(1, constants::MODE_FULL));
5064
5065        let dest1 = [0xC1; 16];
5066        let dest2 = [0xC2; 16];
5067
5068        engine.upsert_path_destination(
5069            dest1,
5070            make_path_entry(1000.0, 2, InterfaceId(1), [0x11; 16]),
5071            1000.0,
5072        );
5073        engine.upsert_path_destination(
5074            dest2,
5075            make_path_entry(1001.0, 2, InterfaceId(1), [0x22; 16]),
5076            1001.0,
5077        );
5078
5079        engine.upsert_path_destination(
5080            dest2,
5081            make_path_entry(1002.0, 1, InterfaceId(1), [0x23; 16]),
5082            1002.0,
5083        );
5084
5085        assert_eq!(engine.path_table_count(), 2);
5086        assert!(engine.has_path(&dest1));
5087        assert!(engine.has_path(&dest2));
5088    }
5089
5090    #[test]
5091    fn test_roaming_loop_prevention() {
5092        let mut engine = TransportEngine::new(make_config(true));
5093        engine.register_interface(make_interface(1, constants::MODE_ROAMING));
5094
5095        let dest = [0xD3; 16];
5096        // Path is known and routes through the same interface (1)
5097        engine.path_table.insert(
5098            dest,
5099            PathSet::from_single(
5100                PathEntry {
5101                    timestamp: 900.0,
5102                    next_hop: [0xAA; 16],
5103                    hops: 2,
5104                    expires: 9999.0,
5105                    random_blobs: Vec::new(),
5106                    receiving_interface: InterfaceId(1),
5107                    packet_hash: [0; 32],
5108                    announce_raw: None,
5109                },
5110                1,
5111            ),
5112        );
5113
5114        let tag = [0x03; 16];
5115        let data = make_path_request_data(&dest, &tag);
5116
5117        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
5118
5119        // ROAMING interface, path next-hop on same interface → loop prevention, no action
5120        assert!(actions.is_empty());
5121        assert!(!engine.announce_table.contains_key(&dest));
5122    }
5123
5124    /// Build a minimal HEADER_1 announce raw packet for testing.
5125    fn make_announce_raw(dest_hash: &[u8; 16], payload: &[u8]) -> Vec<u8> {
5126        // HEADER_1: [flags:1][hops:1][dest:16][context:1][data:*]
5127        // flags: HEADER_1(0) << 6 | context_flag(0) << 5 | TRANSPORT_BROADCAST(0) << 4 | SINGLE(0) << 2 | ANNOUNCE(1)
5128        let flags: u8 = 0x01; // HEADER_1, no context, broadcast, single, announce
5129        let mut raw = Vec::new();
5130        raw.push(flags);
5131        raw.push(0x02); // hops
5132        raw.extend_from_slice(dest_hash);
5133        raw.push(constants::CONTEXT_NONE);
5134        raw.extend_from_slice(payload);
5135        raw
5136    }
5137
5138    #[test]
5139    fn test_path_request_populates_announce_entry_from_raw() {
5140        let mut engine = TransportEngine::new(make_config(true));
5141        engine.register_interface(make_interface(1, constants::MODE_FULL));
5142        engine.register_interface(make_interface(2, constants::MODE_FULL));
5143
5144        let dest = [0xD5; 16];
5145        let payload = vec![0xAB; 32]; // simulated announce data (pubkey, sig, etc.)
5146        let announce_raw = make_announce_raw(&dest, &payload);
5147
5148        engine.path_table.insert(
5149            dest,
5150            PathSet::from_single(
5151                PathEntry {
5152                    timestamp: 900.0,
5153                    next_hop: [0xBB; 16],
5154                    hops: 2,
5155                    expires: 9999.0,
5156                    random_blobs: Vec::new(),
5157                    receiving_interface: InterfaceId(2),
5158                    packet_hash: [0; 32],
5159                    announce_raw: Some(announce_raw.clone()),
5160                },
5161                1,
5162            ),
5163        );
5164
5165        let tag = [0x05; 16];
5166        let data = make_path_request_data(&dest, &tag);
5167        let _actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
5168
5169        // The announce table should now have an entry with populated packet_raw/packet_data
5170        let entry = engine
5171            .announce_table
5172            .get(&dest)
5173            .expect("announce entry must exist");
5174        assert_eq!(entry.packet_raw, announce_raw);
5175        assert_eq!(entry.packet_data, payload);
5176        assert!(entry.block_rebroadcasts);
5177    }
5178
5179    #[test]
5180    fn test_path_request_discovers_when_known_path_has_no_announce_raw() {
5181        let mut engine = TransportEngine::new(make_config(true));
5182        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
5183        engine.register_interface(make_interface(2, constants::MODE_FULL));
5184
5185        let dest = [0xD6; 16];
5186
5187        engine.path_table.insert(
5188            dest,
5189            PathSet::from_single(
5190                PathEntry {
5191                    timestamp: 900.0,
5192                    next_hop: [0xCC; 16],
5193                    hops: 1,
5194                    expires: 9999.0,
5195                    random_blobs: Vec::new(),
5196                    receiving_interface: InterfaceId(2),
5197                    packet_hash: [0; 32],
5198                    announce_raw: None, // no raw data available
5199                },
5200                1,
5201            ),
5202        );
5203
5204        let tag = [0x06; 16];
5205        let data = make_path_request_data(&dest, &tag);
5206        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
5207
5208        assert!(!engine.announce_table.contains_key(&dest));
5209        assert_eq!(actions.len(), 1);
5210        match &actions[0] {
5211            TransportAction::SendOnInterface { interface, raw } => {
5212                assert_eq!(*interface, InterfaceId(2));
5213                assert_recursive_path_request_packet(raw.as_ref(), &dest, &tag);
5214            }
5215            _ => panic!("expected SendOnInterface for recursive path request"),
5216        }
5217        assert!(engine.discovery_path_requests.contains_key(&dest));
5218    }
5219
5220    #[test]
5221    fn test_discovery_request_consumed_on_announce() {
5222        let mut engine = TransportEngine::new(make_config(true));
5223        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
5224
5225        let dest = [0xD4; 16];
5226
5227        // Simulate a waiting discovery request
5228        engine.discovery_path_requests.insert(
5229            dest,
5230            DiscoveryPathRequest {
5231                timestamp: 900.0,
5232                requesting_interface: InterfaceId(1),
5233            },
5234        );
5235
5236        // Consume it
5237        let iface = engine.discovery_path_requests_waiting(&dest);
5238        assert_eq!(iface, Some(InterfaceId(1)));
5239
5240        // Should be gone now
5241        assert!(!engine.discovery_path_requests.contains_key(&dest));
5242        assert_eq!(engine.discovery_path_requests_waiting(&dest), None);
5243    }
5244
5245    #[test]
5246    fn test_pending_path_request_announce_bypasses_ingress_control() {
5247        let mut engine = TransportEngine::new(make_config(true));
5248        let mut inbound = make_interface(1, constants::MODE_FULL);
5249        inbound.ingress_control = crate::transport::types::IngressControlConfig::enabled();
5250        inbound.ia_freq = 10_000.0;
5251        inbound.started = 0.0;
5252        engine.register_interface(inbound);
5253        engine.register_interface(make_interface(2, constants::MODE_ACCESS_POINT));
5254
5255        let identity =
5256            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
5257        let dest_hash = crate::destination::destination_hash(
5258            "ingress",
5259            &["path-request"],
5260            Some(identity.hash()),
5261        );
5262        let name_hash = crate::destination::name_hash("ingress", &["path-request"]);
5263        let announce_raw = build_announce_for_issue4(&dest_hash, &name_hash);
5264
5265        engine.discovery_path_requests.insert(
5266            dest_hash,
5267            DiscoveryPathRequest {
5268                timestamp: 999.0,
5269                requesting_interface: InterfaceId(2),
5270            },
5271        );
5272
5273        let mut rng = rns_crypto::FixedRng::new(&[0x88; 32]);
5274        let actions = engine.handle_inbound(
5275            InboundFrame {
5276                raw: &announce_raw,
5277                iface: InterfaceId(1),
5278                now: 1000.0,
5279                rx: RxMetadata {
5280                    rssi: None,
5281                    snr: None,
5282                },
5283            },
5284            &mut rng,
5285        );
5286
5287        assert_eq!(engine.held_announce_count(&InterfaceId(1)), 0);
5288        assert!(engine.has_path(&dest_hash));
5289        assert!(!engine.discovery_path_requests.contains_key(&dest_hash));
5290        assert!(actions.iter().any(|a| {
5291            matches!(
5292                a,
5293                TransportAction::AnnounceReceived {
5294                    destination_hash,
5295                    receiving_interface: InterfaceId(1),
5296                    ..
5297                } if *destination_hash == dest_hash
5298            )
5299        }));
5300
5301        let entry = engine
5302            .announce_table
5303            .get(&dest_hash)
5304            .expect("path response announce should be queued");
5305        assert!(entry.block_rebroadcasts);
5306        assert_eq!(entry.attached_interface, Some(InterfaceId(2)));
5307    }
5308
5309    // =========================================================================
5310    // Issue #4: Shared instance client 1-hop transport injection
5311    // =========================================================================
5312
5313    /// Helper: build a valid announce packet for use in issue #4 tests.
5314    fn build_announce_for_issue4(dest_hash: &[u8; 16], name_hash: &[u8; 10]) -> Vec<u8> {
5315        let identity =
5316            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
5317        let random_hash = [0x42u8; 10];
5318        let (announce_data, _) = crate::announce::AnnounceData::pack(
5319            &identity,
5320            dest_hash,
5321            name_hash,
5322            &random_hash,
5323            None,
5324            None,
5325        )
5326        .unwrap();
5327        let flags = PacketFlags {
5328            header_type: constants::HEADER_1,
5329            context_flag: constants::FLAG_UNSET,
5330            transport_type: constants::TRANSPORT_BROADCAST,
5331            destination_type: constants::DESTINATION_SINGLE,
5332            packet_type: constants::PACKET_TYPE_ANNOUNCE,
5333        };
5334        RawPacket::pack(
5335            flags,
5336            0,
5337            dest_hash,
5338            None,
5339            constants::CONTEXT_NONE,
5340            &announce_data,
5341        )
5342        .unwrap()
5343        .raw
5344    }
5345
5346    #[test]
5347    fn test_ingress_held_announce_preserves_rx_metadata_on_release() {
5348        let mut engine = TransportEngine::new(make_config(true));
5349        let mut inbound = make_interface(1, constants::MODE_FULL);
5350        inbound.ingress_control = crate::transport::types::IngressControlConfig::enabled();
5351        inbound.ia_freq = constants::IC_BURST_FREQ + 1.0;
5352        inbound.started = 0.0;
5353        engine.register_interface(inbound);
5354
5355        let identity =
5356            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
5357        let dest_hash =
5358            crate::destination::destination_hash("ingress", &["rx"], Some(identity.hash()));
5359        let name_hash = crate::destination::name_hash("ingress", &["rx"]);
5360        let announce_raw = build_announce_for_issue4(&dest_hash, &name_hash);
5361        let rx = RxMetadata {
5362            rssi: Some(-91),
5363            snr: Some(5.5),
5364        };
5365
5366        let mut rng = rns_crypto::FixedRng::new(&[0x88; 32]);
5367        let held_actions = engine.handle_inbound(
5368            InboundFrame::new(&announce_raw, InterfaceId(1), 10000.0).with_rx(rx),
5369            &mut rng,
5370        );
5371
5372        assert!(held_actions.is_empty());
5373        assert_eq!(engine.held_announce_count(&InterfaceId(1)), 1);
5374        assert!(!engine.has_path(&dest_hash));
5375
5376        engine
5377            .interfaces
5378            .get_mut(&InterfaceId(1))
5379            .expect("interface must exist")
5380            .ia_freq = 0.0;
5381
5382        let released_actions = engine.tick(10000.0 + constants::IC_BURST_PENALTY + 1.0, &mut rng);
5383
5384        let released_rx = released_actions.iter().find_map(|action| match action {
5385            TransportAction::AnnounceReceived {
5386                destination_hash,
5387                rx: action_rx,
5388                ..
5389            } if *destination_hash == dest_hash => Some(*action_rx),
5390            _ => None,
5391        });
5392
5393        assert_eq!(released_rx, Some(rx));
5394        assert_eq!(engine.held_announce_count(&InterfaceId(1)), 0);
5395        assert!(engine.has_path(&dest_hash));
5396    }
5397
5398    #[test]
5399    fn test_issue4_local_client_single_data_to_1hop_rewrites_on_outbound() {
5400        // Shared clients learn remote paths via their local shared-instance
5401        // interface and must inject transport headers on outbound when the
5402        // destination is exactly 1 hop away behind the daemon.
5403
5404        let mut engine = TransportEngine::new(make_config(false));
5405        engine.register_interface(make_local_client_interface(1));
5406
5407        let identity =
5408            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
5409        let dest_hash =
5410            crate::destination::destination_hash("issue4", &["test"], Some(identity.hash()));
5411        let name_hash = crate::destination::name_hash("issue4", &["test"]);
5412        let announce_raw = build_announce_for_issue4(&dest_hash, &name_hash);
5413
5414        // Model the announce as already forwarded by the shared daemon to
5415        // the local client. The raw hop count is 1 so that after the local
5416        // client hop compensation the learned path remains 1 hop away.
5417        let mut announce_packet = RawPacket::unpack(&announce_raw).unwrap();
5418        announce_packet.raw[1] = 1;
5419        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
5420        engine.handle_inbound(
5421            InboundFrame {
5422                raw: &announce_packet.raw,
5423                iface: InterfaceId(1),
5424                now: 1000.0,
5425                rx: RxMetadata {
5426                    rssi: None,
5427                    snr: None,
5428                },
5429            },
5430            &mut rng,
5431        );
5432        assert!(engine.has_path(&dest_hash));
5433        assert_eq!(engine.hops_to(&dest_hash), Some(1));
5434
5435        // Build DATA from the shared client to the 1-hop destination.
5436        let data_flags = PacketFlags {
5437            header_type: constants::HEADER_1,
5438            context_flag: constants::FLAG_UNSET,
5439            transport_type: constants::TRANSPORT_BROADCAST,
5440            destination_type: constants::DESTINATION_SINGLE,
5441            packet_type: constants::PACKET_TYPE_DATA,
5442        };
5443        let data_packet = RawPacket::pack(
5444            data_flags,
5445            0,
5446            &dest_hash,
5447            None,
5448            constants::CONTEXT_NONE,
5449            b"hello",
5450        )
5451        .unwrap();
5452
5453        let actions =
5454            engine.handle_outbound(&data_packet, constants::DESTINATION_SINGLE, None, 1001.0);
5455
5456        let send = actions.iter().find_map(|a| match a {
5457            TransportAction::SendOnInterface { interface, raw } => Some((interface, raw)),
5458            _ => None,
5459        });
5460        let (interface, raw) = send.expect("shared client should emit a transport-injected packet");
5461        assert_eq!(*interface, InterfaceId(1));
5462        let flags = PacketFlags::unpack(raw[0]);
5463        assert_eq!(flags.header_type, constants::HEADER_2);
5464        assert_eq!(flags.transport_type, constants::TRANSPORT_TRANSPORT);
5465    }
5466
5467    #[test]
5468    fn test_local_client_forward_to_external_applies_local_hops_delta() {
5469        let daemon_id = [0x42; 16];
5470        let mut config = make_config(true);
5471        config.local_hops_delta = 5;
5472        let mut engine = TransportEngine::new(config);
5473        engine.register_interface(make_local_client_interface(1));
5474        engine.register_interface(make_interface(2, constants::MODE_FULL));
5475
5476        let dest_hash = [0xB7; 16];
5477        engine.upsert_path_destination(
5478            dest_hash,
5479            make_path_entry(1000.0, 1, InterfaceId(2), dest_hash),
5480            1000.0,
5481        );
5482
5483        let flags = PacketFlags {
5484            header_type: constants::HEADER_2,
5485            context_flag: constants::FLAG_UNSET,
5486            transport_type: constants::TRANSPORT_TRANSPORT,
5487            destination_type: constants::DESTINATION_SINGLE,
5488            packet_type: constants::PACKET_TYPE_DATA,
5489        };
5490        let packet = RawPacket::pack(
5491            flags,
5492            0,
5493            &dest_hash,
5494            Some(&daemon_id),
5495            constants::CONTEXT_NONE,
5496            b"from local client",
5497        )
5498        .unwrap();
5499
5500        let mut rng = rns_crypto::FixedRng::new(&[0x44; 32]);
5501        let actions = engine.handle_inbound(
5502            InboundFrame::new(&packet.raw, InterfaceId(1), 1001.0),
5503            &mut rng,
5504        );
5505
5506        let raw = actions.iter().find_map(|action| match action {
5507            TransportAction::SendOnInterface { interface, raw } if *interface == InterfaceId(2) => {
5508                Some(raw)
5509            }
5510            _ => None,
5511        });
5512        let raw = raw.expect("local-client DATA should be forwarded externally");
5513        assert_eq!(raw[1], 5);
5514        let forwarded_flags = PacketFlags::unpack(raw[0]);
5515        assert_eq!(forwarded_flags.header_type, constants::HEADER_1);
5516        assert_eq!(&raw[2..18], &dest_hash);
5517    }
5518
5519    #[test]
5520    fn test_local_client_link_routing_to_external_applies_local_hops_delta() {
5521        let mut config = make_config(true);
5522        config.local_hops_delta = 5;
5523        let mut engine = TransportEngine::new(config);
5524        engine.register_interface(make_local_client_interface(1));
5525        engine.register_interface(make_interface(2, constants::MODE_FULL));
5526
5527        let link_id = [0x4C; 16];
5528        engine.register_link(
5529            link_id,
5530            LinkEntry {
5531                timestamp: 1000.0,
5532                next_hop_transport_id: [0xAA; 16],
5533                next_hop_interface: InterfaceId(2),
5534                remaining_hops: 3,
5535                received_interface: InterfaceId(1),
5536                taken_hops: 0,
5537                destination_hash: [0xBB; 16],
5538                validated: true,
5539                proof_timeout: 1100.0,
5540            },
5541        );
5542
5543        let packet = RawPacket::pack(
5544            PacketFlags {
5545                header_type: constants::HEADER_1,
5546                context_flag: constants::FLAG_UNSET,
5547                transport_type: constants::TRANSPORT_BROADCAST,
5548                destination_type: constants::DESTINATION_LINK,
5549                packet_type: constants::PACKET_TYPE_DATA,
5550            },
5551            0,
5552            &link_id,
5553            None,
5554            constants::CONTEXT_CHANNEL,
5555            b"link data",
5556        )
5557        .unwrap();
5558
5559        let mut rng = rns_crypto::FixedRng::new(&[0x45; 32]);
5560        let actions = engine.handle_inbound(
5561            InboundFrame::new(&packet.raw, InterfaceId(1), 1001.0),
5562            &mut rng,
5563        );
5564
5565        let raw = actions.iter().find_map(|action| match action {
5566            TransportAction::SendOnInterface { interface, raw } if *interface == InterfaceId(2) => {
5567                Some(raw)
5568            }
5569            _ => None,
5570        });
5571        let raw = raw.expect("local-client link packet should be forwarded externally");
5572        assert_eq!(raw[1], 5);
5573    }
5574
5575    #[test]
5576    fn test_instance_local_link_routing_preserves_hops() {
5577        let mut config = make_config(true);
5578        config.local_hops_delta = 5;
5579        let mut engine = TransportEngine::new(config);
5580        engine.register_interface(make_local_client_interface(1));
5581        engine.register_interface(make_local_client_interface(2));
5582
5583        let link_id = [0x4D; 16];
5584        engine.register_link(
5585            link_id,
5586            LinkEntry {
5587                timestamp: 1000.0,
5588                next_hop_transport_id: [0xAA; 16],
5589                next_hop_interface: InterfaceId(2),
5590                remaining_hops: 0,
5591                received_interface: InterfaceId(1),
5592                taken_hops: 0,
5593                destination_hash: [0xBB; 16],
5594                validated: true,
5595                proof_timeout: 1100.0,
5596            },
5597        );
5598
5599        let packet = RawPacket::pack(
5600            PacketFlags {
5601                header_type: constants::HEADER_1,
5602                context_flag: constants::FLAG_UNSET,
5603                transport_type: constants::TRANSPORT_BROADCAST,
5604                destination_type: constants::DESTINATION_LINK,
5605                packet_type: constants::PACKET_TYPE_DATA,
5606            },
5607            0,
5608            &link_id,
5609            None,
5610            constants::CONTEXT_CHANNEL,
5611            b"local link data",
5612        )
5613        .unwrap();
5614
5615        let mut rng = rns_crypto::FixedRng::new(&[0x46; 32]);
5616        let actions = engine.handle_inbound(
5617            InboundFrame::new(&packet.raw, InterfaceId(1), 1001.0),
5618            &mut rng,
5619        );
5620
5621        let raw = actions.iter().find_map(|action| match action {
5622            TransportAction::SendOnInterface { interface, raw } if *interface == InterfaceId(2) => {
5623                Some(raw)
5624            }
5625            _ => None,
5626        });
5627        let raw = raw.expect("instance-local link packet should be forwarded");
5628        assert_eq!(raw[1], 0);
5629    }
5630
5631    #[test]
5632    fn test_local_client_proof_to_external_applies_local_hops_delta() {
5633        let mut config = make_config(true);
5634        config.local_hops_delta = 5;
5635        let mut engine = TransportEngine::new(config);
5636        engine.register_interface(make_local_client_interface(1));
5637        engine.register_interface(make_interface(2, constants::MODE_FULL));
5638
5639        let proof_dest = [0xA5; 16];
5640        engine.reverse_table.insert(
5641            proof_dest,
5642            tables::ReverseEntry {
5643                receiving_interface: InterfaceId(2),
5644                outbound_interface: InterfaceId(1),
5645                timestamp: 1000.0,
5646            },
5647        );
5648
5649        let packet = RawPacket::pack(
5650            PacketFlags {
5651                header_type: constants::HEADER_1,
5652                context_flag: constants::FLAG_UNSET,
5653                transport_type: constants::TRANSPORT_BROADCAST,
5654                destination_type: constants::DESTINATION_SINGLE,
5655                packet_type: constants::PACKET_TYPE_PROOF,
5656            },
5657            0,
5658            &proof_dest,
5659            None,
5660            constants::CONTEXT_NONE,
5661            &[0xCC; 32],
5662        )
5663        .unwrap();
5664
5665        let mut rng = rns_crypto::FixedRng::new(&[0x47; 32]);
5666        let actions = engine.handle_inbound(
5667            InboundFrame::new(&packet.raw, InterfaceId(1), 1001.0),
5668            &mut rng,
5669        );
5670
5671        let raw = actions.iter().find_map(|action| match action {
5672            TransportAction::SendOnInterface { interface, raw } if *interface == InterfaceId(2) => {
5673                Some(raw)
5674            }
5675            _ => None,
5676        });
5677        let raw = raw.expect("local-client proof should be forwarded externally");
5678        assert_eq!(raw[1], 5);
5679    }
5680
5681    #[test]
5682    fn test_proof_for_local_client_preserves_hops() {
5683        let mut config = make_config(true);
5684        config.local_hops_delta = 5;
5685        let mut engine = TransportEngine::new(config);
5686        engine.register_interface(make_local_client_interface(1));
5687        engine.register_interface(make_local_client_interface(2));
5688
5689        let proof_dest = [0xA6; 16];
5690        engine.reverse_table.insert(
5691            proof_dest,
5692            tables::ReverseEntry {
5693                receiving_interface: InterfaceId(2),
5694                outbound_interface: InterfaceId(1),
5695                timestamp: 1000.0,
5696            },
5697        );
5698
5699        let packet = RawPacket::pack(
5700            PacketFlags {
5701                header_type: constants::HEADER_1,
5702                context_flag: constants::FLAG_UNSET,
5703                transport_type: constants::TRANSPORT_BROADCAST,
5704                destination_type: constants::DESTINATION_SINGLE,
5705                packet_type: constants::PACKET_TYPE_PROOF,
5706            },
5707            0,
5708            &proof_dest,
5709            None,
5710            constants::CONTEXT_NONE,
5711            &[0xCD; 32],
5712        )
5713        .unwrap();
5714
5715        let mut rng = rns_crypto::FixedRng::new(&[0x48; 32]);
5716        let actions = engine.handle_inbound(
5717            InboundFrame::new(&packet.raw, InterfaceId(1), 1001.0),
5718            &mut rng,
5719        );
5720
5721        let raw = actions.iter().find_map(|action| match action {
5722            TransportAction::SendOnInterface { interface, raw } if *interface == InterfaceId(2) => {
5723                Some(raw)
5724            }
5725            _ => None,
5726        });
5727        let raw = raw.expect("proof for local client should be forwarded");
5728        assert_eq!(raw[1], 0);
5729    }
5730
5731    #[test]
5732    fn test_issue4_external_data_to_shared_client_strips_transport_header() {
5733        let daemon_id = [0x42; 16];
5734        let mut engine = TransportEngine::new(make_config(true));
5735        engine.register_interface(make_interface(1, constants::MODE_FULL));
5736        engine.register_interface(make_local_client_interface(2));
5737
5738        let dest_hash = [0x99; 16];
5739        engine.upsert_path_destination(
5740            dest_hash,
5741            make_path_entry(1000.0, 1, InterfaceId(2), daemon_id),
5742            1000.0,
5743        );
5744
5745        let h2_flags = PacketFlags {
5746            header_type: constants::HEADER_2,
5747            context_flag: constants::FLAG_UNSET,
5748            transport_type: constants::TRANSPORT_TRANSPORT,
5749            destination_type: constants::DESTINATION_SINGLE,
5750            packet_type: constants::PACKET_TYPE_DATA,
5751        };
5752        let mut h2_raw = Vec::new();
5753        h2_raw.push(h2_flags.pack());
5754        h2_raw.push(0);
5755        h2_raw.extend_from_slice(&daemon_id);
5756        h2_raw.extend_from_slice(&dest_hash);
5757        h2_raw.push(constants::CONTEXT_NONE);
5758        h2_raw.extend_from_slice(b"hello shared client");
5759
5760        let mut rng = rns_crypto::FixedRng::new(&[0x22; 32]);
5761        let actions = engine.handle_inbound(
5762            InboundFrame {
5763                raw: &h2_raw,
5764                iface: InterfaceId(1),
5765                now: 1001.0,
5766                rx: RxMetadata {
5767                    rssi: None,
5768                    snr: None,
5769                },
5770            },
5771            &mut rng,
5772        );
5773
5774        let raw = actions.iter().find_map(|a| match a {
5775            TransportAction::SendOnInterface { interface, raw } if *interface == InterfaceId(2) => {
5776                Some(raw)
5777            }
5778            _ => None,
5779        });
5780        let raw = raw.expect("daemon should forward external DATA to shared client");
5781        let flags = PacketFlags::unpack(raw[0]);
5782        assert_eq!(flags.header_type, constants::HEADER_1);
5783        assert_eq!(flags.transport_type, constants::TRANSPORT_BROADCAST);
5784        assert_eq!(&raw[2..18], &dest_hash);
5785        assert_eq!(&raw[19..], b"hello shared client");
5786    }
5787
5788    #[test]
5789    fn test_issue4_external_data_to_1hop_via_transport_works() {
5790        // Control test: when a DATA packet arrives from an external interface
5791        // with HEADER_2 and the daemon's transport_id, the daemon correctly
5792        // forwards it via step 5.  This proves the multi-hop path works;
5793        // it's only the 1-hop shared-client case that's broken.
5794
5795        let daemon_id = [0x42; 16];
5796        let mut engine = TransportEngine::new(TransportConfig {
5797            transport_enabled: true,
5798            identity_hash: Some(daemon_id),
5799            local_hops_delta: 0,
5800            prefer_shorter_path: false,
5801            max_paths_per_destination: 1,
5802            packet_hashlist_max_entries: constants::HASHLIST_MAXSIZE,
5803            packet_hashlist_allocation: crate::transport::types::PacketHashlistAllocation::Eager,
5804            max_discovery_pr_tags: constants::MAX_PR_TAGS,
5805            max_path_destinations: usize::MAX,
5806            max_tunnel_destinations_total: usize::MAX,
5807            destination_timeout_secs: constants::DESTINATION_TIMEOUT,
5808            announce_table_ttl_secs: constants::ANNOUNCE_TABLE_TTL,
5809            announce_table_max_bytes: constants::ANNOUNCE_TABLE_MAX_BYTES,
5810            announce_sig_cache_enabled: true,
5811            announce_sig_cache_max_entries: constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
5812            announce_sig_cache_ttl_secs: constants::ANNOUNCE_SIG_CACHE_TTL,
5813            announce_queue_max_entries: 256,
5814            announce_queue_max_interfaces: 1024,
5815        });
5816        engine.register_interface(make_interface(1, constants::MODE_FULL)); // inbound
5817        engine.register_interface(make_interface(2, constants::MODE_FULL)); // outbound to Bob
5818
5819        let identity =
5820            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
5821        let dest_hash =
5822            crate::destination::destination_hash("issue4", &["ctrl"], Some(identity.hash()));
5823        let name_hash = crate::destination::name_hash("issue4", &["ctrl"]);
5824        let announce_raw = build_announce_for_issue4(&dest_hash, &name_hash);
5825
5826        // Feed announce from interface 2 (Bob's side), hops=0 → stored as hops=1
5827        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
5828        engine.handle_inbound(
5829            InboundFrame {
5830                raw: &announce_raw,
5831                iface: InterfaceId(2),
5832                now: 1000.0,
5833                rx: RxMetadata {
5834                    rssi: None,
5835                    snr: None,
5836                },
5837            },
5838            &mut rng,
5839        );
5840        assert_eq!(engine.hops_to(&dest_hash), Some(1));
5841
5842        // Now send a HEADER_2 transport packet addressed to the daemon
5843        // (simulating what Alice would send in a multi-hop scenario)
5844        let h2_flags = PacketFlags {
5845            header_type: constants::HEADER_2,
5846            context_flag: constants::FLAG_UNSET,
5847            transport_type: constants::TRANSPORT_TRANSPORT,
5848            destination_type: constants::DESTINATION_SINGLE,
5849            packet_type: constants::PACKET_TYPE_DATA,
5850        };
5851        // Build HEADER_2 manually: [flags, hops, transport_id(16), dest_hash(16), context, data...]
5852        let mut h2_raw = Vec::new();
5853        h2_raw.push(h2_flags.pack());
5854        h2_raw.push(0); // hops
5855        h2_raw.extend_from_slice(&daemon_id); // transport_id = daemon
5856        h2_raw.extend_from_slice(&dest_hash);
5857        h2_raw.push(constants::CONTEXT_NONE);
5858        h2_raw.extend_from_slice(b"hello via transport");
5859
5860        let mut rng2 = rns_crypto::FixedRng::new(&[0x22; 32]);
5861        let actions = engine.handle_inbound(
5862            InboundFrame {
5863                raw: &h2_raw,
5864                iface: InterfaceId(1),
5865                now: 1001.0,
5866                rx: RxMetadata {
5867                    rssi: None,
5868                    snr: None,
5869                },
5870            },
5871            &mut rng2,
5872        );
5873
5874        // This SHOULD forward via step 5 (transport forwarding)
5875        let has_send = actions.iter().any(|a| {
5876            matches!(
5877                a,
5878                TransportAction::SendOnInterface { interface, .. } if *interface == InterfaceId(2)
5879            )
5880        });
5881        assert!(
5882            has_send,
5883            "HEADER_2 transport packet should be forwarded (control test)"
5884        );
5885    }
5886}