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