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