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