Skip to main content

rns_core/transport/
mod.rs

1pub mod announce_proc;
2pub mod announce_queue;
3pub mod dedup;
4pub mod inbound;
5pub mod ingress_control;
6pub mod jobs;
7pub mod outbound;
8pub mod pathfinder;
9pub mod rate_limit;
10pub mod tables;
11pub mod tunnel;
12pub mod types;
13
14use alloc::collections::BTreeMap;
15use alloc::string::String;
16use alloc::vec::Vec;
17
18use rns_crypto::Rng;
19
20use crate::announce::AnnounceData;
21use crate::constants;
22use crate::hash;
23use crate::packet::RawPacket;
24
25use self::announce_proc::compute_path_expires;
26use self::announce_queue::AnnounceQueues;
27use self::dedup::PacketHashlist;
28use self::inbound::{
29    create_link_entry, create_reverse_entry, forward_transport_packet, route_proof_via_reverse,
30    route_via_link_table,
31};
32use self::ingress_control::IngressControl;
33use self::outbound::{route_outbound, should_transmit_announce};
34use self::pathfinder::{
35    decide_announce_multipath, extract_random_blob, timebase_from_random_blob, MultiPathDecision,
36};
37use self::rate_limit::AnnounceRateLimiter;
38use self::tables::{AnnounceEntry, DiscoveryPathRequest, LinkEntry, PathEntry, PathSet};
39use self::tunnel::TunnelTable;
40use self::types::{BlackholeEntry, InterfaceId, InterfaceInfo, TransportAction, TransportConfig};
41
42pub type PathTableRow = ([u8; 16], f64, [u8; 16], u8, f64, String);
43pub type RateTableRow = ([u8; 16], f64, u32, f64, Vec<f64>);
44
45/// The core transport/routing engine.
46///
47/// Maintains routing tables and processes packets without performing any I/O.
48/// Returns `Vec<TransportAction>` that the caller must execute.
49pub struct TransportEngine {
50    config: TransportConfig,
51    path_table: BTreeMap<[u8; 16], PathSet>,
52    announce_table: BTreeMap<[u8; 16], AnnounceEntry>,
53    reverse_table: BTreeMap<[u8; 16], tables::ReverseEntry>,
54    link_table: BTreeMap<[u8; 16], LinkEntry>,
55    held_announces: BTreeMap<[u8; 16], AnnounceEntry>,
56    packet_hashlist: PacketHashlist,
57    rate_limiter: AnnounceRateLimiter,
58    path_states: BTreeMap<[u8; 16], u8>,
59    interfaces: BTreeMap<InterfaceId, InterfaceInfo>,
60    local_destinations: BTreeMap<[u8; 16], u8>,
61    blackholed_identities: BTreeMap<[u8; 16], BlackholeEntry>,
62    announce_queues: AnnounceQueues,
63    ingress_control: IngressControl,
64    tunnel_table: TunnelTable,
65    discovery_pr_tags: Vec<[u8; 32]>,
66    discovery_path_requests: BTreeMap<[u8; 16], DiscoveryPathRequest>,
67    // Job timing
68    announces_last_checked: f64,
69    tables_last_culled: f64,
70}
71
72impl TransportEngine {
73    pub fn new(config: TransportConfig) -> Self {
74        let packet_hashlist_max_entries = config.packet_hashlist_max_entries;
75        TransportEngine {
76            config,
77            path_table: BTreeMap::new(),
78            announce_table: BTreeMap::new(),
79            reverse_table: BTreeMap::new(),
80            link_table: BTreeMap::new(),
81            held_announces: BTreeMap::new(),
82            packet_hashlist: PacketHashlist::new(packet_hashlist_max_entries),
83            rate_limiter: AnnounceRateLimiter::new(),
84            path_states: BTreeMap::new(),
85            interfaces: BTreeMap::new(),
86            local_destinations: BTreeMap::new(),
87            blackholed_identities: BTreeMap::new(),
88            announce_queues: AnnounceQueues::new(),
89            ingress_control: IngressControl::new(),
90            tunnel_table: TunnelTable::new(),
91            discovery_pr_tags: Vec::new(),
92            discovery_path_requests: BTreeMap::new(),
93            announces_last_checked: 0.0,
94            tables_last_culled: 0.0,
95        }
96    }
97
98    // =========================================================================
99    // Interface management
100    // =========================================================================
101
102    pub fn register_interface(&mut self, info: InterfaceInfo) {
103        self.interfaces.insert(info.id, info);
104    }
105
106    pub fn deregister_interface(&mut self, id: InterfaceId) {
107        self.interfaces.remove(&id);
108        self.ingress_control.remove_interface(&id);
109    }
110
111    // =========================================================================
112    // Destination management
113    // =========================================================================
114
115    pub fn register_destination(&mut self, dest_hash: [u8; 16], dest_type: u8) {
116        self.local_destinations.insert(dest_hash, dest_type);
117    }
118
119    pub fn deregister_destination(&mut self, dest_hash: &[u8; 16]) {
120        self.local_destinations.remove(dest_hash);
121    }
122
123    // =========================================================================
124    // Path queries
125    // =========================================================================
126
127    pub fn has_path(&self, dest_hash: &[u8; 16]) -> bool {
128        self.path_table
129            .get(dest_hash)
130            .is_some_and(|ps| !ps.is_empty())
131    }
132
133    pub fn hops_to(&self, dest_hash: &[u8; 16]) -> Option<u8> {
134        self.path_table
135            .get(dest_hash)
136            .and_then(|ps| ps.primary())
137            .map(|e| e.hops)
138    }
139
140    pub fn next_hop(&self, dest_hash: &[u8; 16]) -> Option<[u8; 16]> {
141        self.path_table
142            .get(dest_hash)
143            .and_then(|ps| ps.primary())
144            .map(|e| e.next_hop)
145    }
146
147    pub fn next_hop_interface(&self, dest_hash: &[u8; 16]) -> Option<InterfaceId> {
148        self.path_table
149            .get(dest_hash)
150            .and_then(|ps| ps.primary())
151            .map(|e| e.receiving_interface)
152    }
153
154    // =========================================================================
155    // Path state
156    // =========================================================================
157
158    /// Mark a path as unresponsive.
159    ///
160    /// If `receiving_interface` is provided and points to a MODE_BOUNDARY interface,
161    /// the marking is skipped — boundary interfaces must not poison path tables.
162    /// (Python Transport.py: mark_path_unknown/unresponsive boundary exemption)
163    pub fn mark_path_unresponsive(
164        &mut self,
165        dest_hash: &[u8; 16],
166        receiving_interface: Option<InterfaceId>,
167    ) {
168        if let Some(iface_id) = receiving_interface {
169            if let Some(info) = self.interfaces.get(&iface_id) {
170                if info.mode == constants::MODE_BOUNDARY {
171                    return;
172                }
173            }
174        }
175
176        // Failover: if we have alternative paths, promote the next one
177        if let Some(ps) = self.path_table.get_mut(dest_hash) {
178            if ps.len() > 1 {
179                ps.failover(false); // demote old primary to back
180                                    // Clear unresponsive state since we promoted a fresh primary
181                self.path_states.remove(dest_hash);
182                return;
183            }
184        }
185
186        self.path_states
187            .insert(*dest_hash, constants::STATE_UNRESPONSIVE);
188    }
189
190    pub fn mark_path_responsive(&mut self, dest_hash: &[u8; 16]) {
191        self.path_states
192            .insert(*dest_hash, constants::STATE_RESPONSIVE);
193    }
194
195    pub fn path_is_unresponsive(&self, dest_hash: &[u8; 16]) -> bool {
196        self.path_states.get(dest_hash) == Some(&constants::STATE_UNRESPONSIVE)
197    }
198
199    pub fn expire_path(&mut self, dest_hash: &[u8; 16]) {
200        if let Some(ps) = self.path_table.get_mut(dest_hash) {
201            ps.expire_all();
202        }
203    }
204
205    // =========================================================================
206    // Link table
207    // =========================================================================
208
209    pub fn register_link(&mut self, link_id: [u8; 16], entry: LinkEntry) {
210        self.link_table.insert(link_id, entry);
211    }
212
213    pub fn validate_link(&mut self, link_id: &[u8; 16]) {
214        if let Some(entry) = self.link_table.get_mut(link_id) {
215            entry.validated = true;
216        }
217    }
218
219    pub fn remove_link(&mut self, link_id: &[u8; 16]) {
220        self.link_table.remove(link_id);
221    }
222
223    // =========================================================================
224    // Blackhole management
225    // =========================================================================
226
227    /// Add an identity hash to the blackhole list.
228    pub fn blackhole_identity(
229        &mut self,
230        identity_hash: [u8; 16],
231        now: f64,
232        duration_hours: Option<f64>,
233        reason: Option<String>,
234    ) {
235        let expires = match duration_hours {
236            Some(h) if h > 0.0 => now + h * 3600.0,
237            _ => 0.0, // never expires
238        };
239        self.blackholed_identities.insert(
240            identity_hash,
241            BlackholeEntry {
242                created: now,
243                expires,
244                reason,
245            },
246        );
247    }
248
249    /// Remove an identity hash from the blackhole list.
250    pub fn unblackhole_identity(&mut self, identity_hash: &[u8; 16]) -> bool {
251        self.blackholed_identities.remove(identity_hash).is_some()
252    }
253
254    /// Check if an identity hash is blackholed (and not expired).
255    pub fn is_blackholed(&self, identity_hash: &[u8; 16], now: f64) -> bool {
256        if let Some(entry) = self.blackholed_identities.get(identity_hash) {
257            if entry.expires == 0.0 || entry.expires > now {
258                return true;
259            }
260        }
261        false
262    }
263
264    /// Get all blackhole entries (for queries).
265    pub fn blackholed_entries(&self) -> impl Iterator<Item = (&[u8; 16], &BlackholeEntry)> {
266        self.blackholed_identities.iter()
267    }
268
269    /// Cull expired blackhole entries.
270    fn cull_blackholed(&mut self, now: f64) {
271        self.blackholed_identities
272            .retain(|_, entry| entry.expires == 0.0 || entry.expires > now);
273    }
274
275    // =========================================================================
276    // Tunnel management
277    // =========================================================================
278
279    /// Handle a validated tunnel synthesis — create new or reattach.
280    ///
281    /// Returns actions for any restored paths.
282    pub fn handle_tunnel(
283        &mut self,
284        tunnel_id: [u8; 32],
285        interface: InterfaceId,
286        now: f64,
287    ) -> Vec<TransportAction> {
288        let mut actions = Vec::new();
289
290        // Set tunnel_id on the interface
291        if let Some(info) = self.interfaces.get_mut(&interface) {
292            info.tunnel_id = Some(tunnel_id);
293        }
294
295        let restored_paths = self.tunnel_table.handle_tunnel(tunnel_id, interface, now);
296
297        // Restore paths to path table if they're better than existing
298        let max_paths = self.config.max_paths_per_destination;
299        for (dest_hash, tunnel_path) in &restored_paths {
300            let should_restore = match self.path_table.get(dest_hash).and_then(|ps| ps.primary()) {
301                Some(existing) => {
302                    // Restore if fewer hops or existing expired
303                    tunnel_path.hops <= existing.hops || existing.expires < now
304                }
305                None => true,
306            };
307
308            if should_restore {
309                let entry = PathEntry {
310                    timestamp: tunnel_path.timestamp,
311                    next_hop: tunnel_path.received_from,
312                    hops: tunnel_path.hops,
313                    expires: tunnel_path.expires,
314                    random_blobs: tunnel_path.random_blobs.clone(),
315                    receiving_interface: interface,
316                    packet_hash: tunnel_path.packet_hash,
317                    announce_raw: None,
318                };
319                self.path_table
320                    .insert(*dest_hash, PathSet::from_single(entry, max_paths));
321            }
322        }
323
324        actions.push(TransportAction::TunnelEstablished {
325            tunnel_id,
326            interface,
327        });
328
329        actions
330    }
331
332    /// Synthesize a tunnel on an interface.
333    ///
334    /// `identity`: the transport identity (must have private key for signing)
335    /// `interface_id`: which interface to send the synthesis on
336    /// `rng`: random number generator
337    ///
338    /// Returns TunnelSynthesize action to send the synthesis packet.
339    pub fn synthesize_tunnel(
340        &self,
341        identity: &rns_crypto::identity::Identity,
342        interface_id: InterfaceId,
343        rng: &mut dyn Rng,
344    ) -> Vec<TransportAction> {
345        let mut actions = Vec::new();
346
347        // Compute interface hash from the interface name
348        let interface_hash = if let Some(info) = self.interfaces.get(&interface_id) {
349            hash::full_hash(info.name.as_bytes())
350        } else {
351            return actions;
352        };
353
354        match tunnel::build_tunnel_synthesize_data(identity, &interface_hash, rng) {
355            Ok((data, _tunnel_id)) => {
356                let dest_hash = crate::destination::destination_hash(
357                    "rnstransport",
358                    &["tunnel", "synthesize"],
359                    None,
360                );
361                actions.push(TransportAction::TunnelSynthesize {
362                    interface: interface_id,
363                    data,
364                    dest_hash,
365                });
366            }
367            Err(e) => {
368                // Can't synthesize — no private key or other error
369                let _ = e;
370            }
371        }
372
373        actions
374    }
375
376    /// Void a tunnel's interface connection (tunnel disconnected).
377    pub fn void_tunnel_interface(&mut self, tunnel_id: &[u8; 32]) {
378        self.tunnel_table.void_tunnel_interface(tunnel_id);
379    }
380
381    /// Access the tunnel table for queries.
382    pub fn tunnel_table(&self) -> &TunnelTable {
383        &self.tunnel_table
384    }
385
386    // =========================================================================
387    // Packet filter
388    // =========================================================================
389
390    /// Check if any local client interfaces are registered.
391    fn has_local_clients(&self) -> bool {
392        self.interfaces.values().any(|i| i.is_local_client)
393    }
394
395    /// Packet filter: dedup + basic validity.
396    ///
397    /// Transport.py:1187-1238
398    fn packet_filter(&self, packet: &RawPacket) -> bool {
399        // Filter packets for other transport instances
400        if packet.transport_id.is_some()
401            && packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
402        {
403            if let Some(ref identity_hash) = self.config.identity_hash {
404                if packet.transport_id.as_ref() != Some(identity_hash) {
405                    return false;
406                }
407            }
408        }
409
410        // Allow certain contexts unconditionally
411        match packet.context {
412            constants::CONTEXT_KEEPALIVE
413            | constants::CONTEXT_RESOURCE_REQ
414            | constants::CONTEXT_RESOURCE_PRF
415            | constants::CONTEXT_RESOURCE
416            | constants::CONTEXT_CACHE_REQUEST
417            | constants::CONTEXT_CHANNEL => return true,
418            _ => {}
419        }
420
421        // PLAIN/GROUP checks
422        if packet.flags.destination_type == constants::DESTINATION_PLAIN
423            || packet.flags.destination_type == constants::DESTINATION_GROUP
424        {
425            if packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE {
426                return packet.hops <= 1;
427            } else {
428                // PLAIN/GROUP ANNOUNCE is invalid
429                return false;
430            }
431        }
432
433        // Deduplication
434        if !self.packet_hashlist.is_duplicate(&packet.packet_hash) {
435            return true;
436        }
437
438        // Duplicate announce for SINGLE dest is allowed (path update)
439        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
440            && packet.flags.destination_type == constants::DESTINATION_SINGLE
441        {
442            return true;
443        }
444
445        false
446    }
447
448    // =========================================================================
449    // Core API: handle_inbound
450    // =========================================================================
451
452    /// Process an inbound raw packet from a network interface.
453    ///
454    /// Returns a list of actions for the caller to execute.
455    pub fn handle_inbound(
456        &mut self,
457        raw: &[u8],
458        iface: InterfaceId,
459        now: f64,
460        rng: &mut dyn Rng,
461    ) -> Vec<TransportAction> {
462        let mut actions = Vec::new();
463
464        // 1. Unpack
465        let mut packet = match RawPacket::unpack(raw) {
466            Ok(p) => p,
467            Err(_) => return actions, // silent drop
468        };
469
470        // Save original raw (pre-hop-increment) for announce caching
471        let original_raw = raw.to_vec();
472
473        // 2. Increment hops
474        packet.hops += 1;
475
476        // 2a. If from a local client, decrement hops to cancel the +1
477        // (local clients are attached via shared instance, not a real hop)
478        let from_local_client = self
479            .interfaces
480            .get(&iface)
481            .map(|i| i.is_local_client)
482            .unwrap_or(false);
483        if from_local_client {
484            packet.hops = packet.hops.saturating_sub(1);
485        }
486
487        // 3. Packet filter
488        if !self.packet_filter(&packet) {
489            return actions;
490        }
491
492        // 4. Determine whether to add to hashlist now or defer
493        let mut remember_hash = true;
494
495        if self.link_table.contains_key(&packet.destination_hash) {
496            remember_hash = false;
497        }
498        if packet.flags.packet_type == constants::PACKET_TYPE_PROOF
499            && packet.context == constants::CONTEXT_LRPROOF
500        {
501            remember_hash = false;
502        }
503
504        if remember_hash {
505            self.packet_hashlist.add(packet.packet_hash);
506        }
507
508        // 4a. PLAIN broadcast bridging between local clients and external interfaces
509        if packet.flags.destination_type == constants::DESTINATION_PLAIN
510            && packet.flags.transport_type == constants::TRANSPORT_BROADCAST
511            && self.has_local_clients()
512        {
513            if from_local_client {
514                // From local client → forward to all external interfaces
515                actions.push(TransportAction::ForwardPlainBroadcast {
516                    raw: packet.raw.clone(),
517                    to_local: false,
518                    exclude: Some(iface),
519                });
520            } else {
521                // From external → forward to all local clients
522                actions.push(TransportAction::ForwardPlainBroadcast {
523                    raw: packet.raw.clone(),
524                    to_local: true,
525                    exclude: None,
526                });
527            }
528        }
529
530        // 5. Transport forwarding: if we are the designated next hop
531        if self.config.transport_enabled || self.config.identity_hash.is_some() {
532            if packet.transport_id.is_some()
533                && packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
534            {
535                if let Some(ref identity_hash) = self.config.identity_hash {
536                    if packet.transport_id.as_ref() == Some(identity_hash) {
537                        if let Some(path_entry) = self
538                            .path_table
539                            .get(&packet.destination_hash)
540                            .and_then(|ps| ps.primary())
541                        {
542                            let next_hop = path_entry.next_hop;
543                            let remaining_hops = path_entry.hops;
544                            let outbound_interface = path_entry.receiving_interface;
545
546                            let new_raw = forward_transport_packet(
547                                &packet,
548                                next_hop,
549                                remaining_hops,
550                                outbound_interface,
551                            );
552
553                            // Create link table or reverse table entry
554                            if packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST {
555                                let proof_timeout = now
556                                    + constants::LINK_ESTABLISHMENT_TIMEOUT_PER_HOP
557                                        * (remaining_hops.max(1) as f64);
558
559                                let (link_id, link_entry) = create_link_entry(
560                                    &packet,
561                                    next_hop,
562                                    outbound_interface,
563                                    remaining_hops,
564                                    iface,
565                                    now,
566                                    proof_timeout,
567                                );
568                                self.link_table.insert(link_id, link_entry);
569                                actions.push(TransportAction::LinkRequestReceived {
570                                    link_id,
571                                    destination_hash: packet.destination_hash,
572                                    receiving_interface: iface,
573                                });
574                            } else {
575                                let (trunc_hash, reverse_entry) =
576                                    create_reverse_entry(&packet, outbound_interface, iface, now);
577                                self.reverse_table.insert(trunc_hash, reverse_entry);
578                            }
579
580                            actions.push(TransportAction::SendOnInterface {
581                                interface: outbound_interface,
582                                raw: new_raw,
583                            });
584
585                            // Update path timestamp
586                            if let Some(entry) = self
587                                .path_table
588                                .get_mut(&packet.destination_hash)
589                                .and_then(|ps| ps.primary_mut())
590                            {
591                                entry.timestamp = now;
592                            }
593                        }
594                    }
595                }
596            }
597
598            // 6. Link table routing for non-announce, non-linkrequest, non-lrproof
599            if packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
600                && packet.flags.packet_type != constants::PACKET_TYPE_LINKREQUEST
601                && packet.context != constants::CONTEXT_LRPROOF
602            {
603                if let Some(link_entry) = self.link_table.get(&packet.destination_hash).cloned() {
604                    if let Some((outbound_iface, new_raw)) =
605                        route_via_link_table(&packet, &link_entry, iface)
606                    {
607                        // Add to hashlist now that we know it's for us
608                        self.packet_hashlist.add(packet.packet_hash);
609
610                        actions.push(TransportAction::SendOnInterface {
611                            interface: outbound_iface,
612                            raw: new_raw,
613                        });
614
615                        // Update link timestamp
616                        if let Some(entry) = self.link_table.get_mut(&packet.destination_hash) {
617                            entry.timestamp = now;
618                        }
619                    }
620                }
621            }
622        }
623
624        // 7. Announce handling
625        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE {
626            self.process_inbound_announce(&packet, &original_raw, iface, now, rng, &mut actions);
627        }
628
629        // 8. Proof handling
630        if packet.flags.packet_type == constants::PACKET_TYPE_PROOF {
631            self.process_inbound_proof(&packet, iface, now, &mut actions);
632        }
633
634        // 9. Local delivery for LINKREQUEST and DATA
635        if (packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
636            || packet.flags.packet_type == constants::PACKET_TYPE_DATA)
637            && self.local_destinations.contains_key(&packet.destination_hash)
638        {
639            actions.push(TransportAction::DeliverLocal {
640                destination_hash: packet.destination_hash,
641                raw: packet.raw.clone(),
642                packet_hash: packet.packet_hash,
643                receiving_interface: iface,
644            });
645        }
646
647        actions
648    }
649
650    // =========================================================================
651    // Inbound announce processing
652    // =========================================================================
653
654    fn process_inbound_announce(
655        &mut self,
656        packet: &RawPacket,
657        original_raw: &[u8],
658        iface: InterfaceId,
659        now: f64,
660        rng: &mut dyn Rng,
661        actions: &mut Vec<TransportAction>,
662    ) {
663        if packet.flags.destination_type != constants::DESTINATION_SINGLE {
664            return;
665        }
666
667        let has_ratchet = packet.flags.context_flag == constants::FLAG_SET;
668
669        // Unpack and validate announce
670        let announce = match AnnounceData::unpack(&packet.data, has_ratchet) {
671            Ok(a) => a,
672            Err(_) => return,
673        };
674
675        let validated = match announce.validate(&packet.destination_hash) {
676            Ok(v) => v,
677            Err(_) => return,
678        };
679
680        // Skip blackholed identities
681        if self.is_blackholed(&validated.identity_hash, now) {
682            return;
683        }
684
685        // Skip local destinations
686        if self
687            .local_destinations
688            .contains_key(&packet.destination_hash)
689        {
690            log::debug!(
691                "Announce:skipping local destination {:02x}{:02x}{:02x}{:02x}..",
692                packet.destination_hash[0],
693                packet.destination_hash[1],
694                packet.destination_hash[2],
695                packet.destination_hash[3],
696            );
697            return;
698        }
699
700        // Ingress control: hold announces from unknown destinations during bursts
701        if !self.has_path(&packet.destination_hash) {
702            if let Some(info) = self.interfaces.get(&iface) {
703                if info.ingress_control
704                    && self.ingress_control.should_ingress_limit(
705                        iface,
706                        info.ia_freq,
707                        info.started,
708                        now,
709                    )
710                {
711                    self.ingress_control.hold_announce(
712                        iface,
713                        packet.destination_hash,
714                        ingress_control::HeldAnnounce {
715                            raw: original_raw.to_vec(),
716                            hops: packet.hops,
717                            receiving_interface: iface,
718                            timestamp: now,
719                        },
720                    );
721                    return;
722                }
723            }
724        }
725
726        // Detect retransmit completion
727        let received_from = if let Some(transport_id) = packet.transport_id {
728            // Check if this is a retransmit we can stop
729            if self.config.transport_enabled {
730                if let Some(announce_entry) = self.announce_table.get_mut(&packet.destination_hash)
731                {
732                    if packet.hops.checked_sub(1) == Some(announce_entry.hops) {
733                        announce_entry.local_rebroadcasts += 1;
734                        if announce_entry.retries > 0
735                            && announce_entry.local_rebroadcasts
736                                >= constants::LOCAL_REBROADCASTS_MAX
737                        {
738                            self.announce_table.remove(&packet.destination_hash);
739                        }
740                    }
741                    // Check if our retransmit was passed on
742                    if let Some(announce_entry) = self.announce_table.get(&packet.destination_hash)
743                    {
744                        if packet.hops.checked_sub(1) == Some(announce_entry.hops + 1)
745                            && announce_entry.retries > 0
746                            && now < announce_entry.retransmit_timeout
747                        {
748                            self.announce_table.remove(&packet.destination_hash);
749                        }
750                    }
751                }
752            }
753            transport_id
754        } else {
755            packet.destination_hash
756        };
757
758        // Extract random blob
759        let random_blob = match extract_random_blob(&packet.data) {
760            Some(b) => b,
761            None => return,
762        };
763
764        // Check hop limit
765        if packet.hops > constants::PATHFINDER_M {
766            return;
767        }
768
769        let announce_emitted = timebase_from_random_blob(&random_blob);
770
771        // Multi-path aware decision
772        let existing_set = self.path_table.get(&packet.destination_hash);
773        let is_unresponsive = self.path_is_unresponsive(&packet.destination_hash);
774
775        let mp_decision = decide_announce_multipath(
776            existing_set,
777            packet.hops,
778            announce_emitted,
779            &random_blob,
780            &received_from,
781            is_unresponsive,
782            now,
783            self.config.prefer_shorter_path,
784        );
785
786        if mp_decision == MultiPathDecision::Reject {
787            log::debug!(
788                "Announce:path decision REJECT for dest={:02x}{:02x}{:02x}{:02x}..",
789                packet.destination_hash[0],
790                packet.destination_hash[1],
791                packet.destination_hash[2],
792                packet.destination_hash[3],
793            );
794            return;
795        }
796
797        // Rate limiting
798        let rate_blocked = if packet.context != constants::CONTEXT_PATH_RESPONSE {
799            if let Some(iface_info) = self.interfaces.get(&iface) {
800                self.rate_limiter.check_and_update(
801                    &packet.destination_hash,
802                    now,
803                    iface_info.announce_rate_target,
804                    iface_info.announce_rate_grace,
805                    iface_info.announce_rate_penalty,
806                )
807            } else {
808                false
809            }
810        } else {
811            false
812        };
813
814        // Get interface mode for expiry calculation
815        let interface_mode = self
816            .interfaces
817            .get(&iface)
818            .map(|i| i.mode)
819            .unwrap_or(constants::MODE_FULL);
820
821        let expires = compute_path_expires(now, interface_mode);
822
823        // Get existing random blobs from the matching path (same next_hop) or empty
824        let existing_blobs = self
825            .path_table
826            .get(&packet.destination_hash)
827            .and_then(|ps| ps.find_by_next_hop(&received_from))
828            .map(|e| e.random_blobs.clone())
829            .unwrap_or_default();
830
831        // Generate RNG value for retransmit timeout
832        let mut rng_bytes = [0u8; 8];
833        rng.fill_bytes(&mut rng_bytes);
834        let rng_value = (u64::from_le_bytes(rng_bytes) as f64) / (u64::MAX as f64);
835
836        let is_path_response = packet.context == constants::CONTEXT_PATH_RESPONSE;
837
838        let (path_entry, announce_entry) = announce_proc::process_validated_announce(
839            packet.destination_hash,
840            packet.hops,
841            &packet.data,
842            &packet.raw,
843            packet.packet_hash,
844            packet.flags.context_flag,
845            received_from,
846            iface,
847            now,
848            existing_blobs,
849            random_blob,
850            expires,
851            rng_value,
852            self.config.transport_enabled,
853            is_path_response,
854            rate_blocked,
855            Some(original_raw.to_vec()),
856        );
857
858        // Emit CacheAnnounce for disk caching (pre-hop-increment raw)
859        actions.push(TransportAction::CacheAnnounce {
860            packet_hash: packet.packet_hash,
861            raw: original_raw.to_vec(),
862        });
863
864        // Store path via upsert into PathSet
865        let max_paths = self.config.max_paths_per_destination;
866        if let Some(ps) = self.path_table.get_mut(&packet.destination_hash) {
867            ps.upsert(path_entry);
868        } else {
869            self.path_table.insert(
870                packet.destination_hash,
871                PathSet::from_single(path_entry, max_paths),
872            );
873        }
874
875        // If receiving interface has a tunnel_id, store path in tunnel table too
876        if let Some(tunnel_id) = self.interfaces.get(&iface).and_then(|i| i.tunnel_id) {
877            let blobs = self
878                .path_table
879                .get(&packet.destination_hash)
880                .and_then(|ps| ps.find_by_next_hop(&received_from))
881                .map(|e| e.random_blobs.clone())
882                .unwrap_or_default();
883            self.tunnel_table.store_tunnel_path(
884                &tunnel_id,
885                packet.destination_hash,
886                tunnel::TunnelPath {
887                    timestamp: now,
888                    received_from,
889                    hops: packet.hops,
890                    expires,
891                    random_blobs: blobs,
892                    packet_hash: packet.packet_hash,
893                },
894                now,
895            );
896        }
897
898        // Mark path as unknown state on update
899        self.path_states.remove(&packet.destination_hash);
900
901        // Store announce for retransmission
902        if let Some(ann) = announce_entry {
903            self.announce_table.insert(packet.destination_hash, ann);
904        }
905
906        // Emit actions
907        actions.push(TransportAction::AnnounceReceived {
908            destination_hash: packet.destination_hash,
909            identity_hash: validated.identity_hash,
910            public_key: validated.public_key,
911            name_hash: validated.name_hash,
912            random_hash: validated.random_hash,
913            app_data: validated.app_data,
914            hops: packet.hops,
915            receiving_interface: iface,
916        });
917
918        actions.push(TransportAction::PathUpdated {
919            destination_hash: packet.destination_hash,
920            hops: packet.hops,
921            next_hop: received_from,
922            interface: iface,
923        });
924
925        // Forward announce to local clients if any are connected
926        if self.has_local_clients() {
927            actions.push(TransportAction::ForwardToLocalClients {
928                raw: packet.raw.clone(),
929                exclude: Some(iface),
930            });
931        }
932
933        // Check for discovery path requests waiting for this announce
934        if let Some(pr_entry) = self.discovery_path_requests_waiting(&packet.destination_hash) {
935            // Build a path response announce and queue it
936            let entry = AnnounceEntry {
937                timestamp: now,
938                retransmit_timeout: now,
939                retries: constants::PATHFINDER_R,
940                received_from,
941                hops: packet.hops,
942                packet_raw: packet.raw.clone(),
943                packet_data: packet.data.clone(),
944                destination_hash: packet.destination_hash,
945                context_flag: packet.flags.context_flag,
946                local_rebroadcasts: 0,
947                block_rebroadcasts: true,
948                attached_interface: Some(pr_entry),
949            };
950            self.announce_table.insert(packet.destination_hash, entry);
951        }
952    }
953
954    /// Check if there's a waiting discovery path request for a destination.
955    /// Consumes the request if found (one-shot: the caller queues the announce response).
956    fn discovery_path_requests_waiting(&mut self, dest_hash: &[u8; 16]) -> Option<InterfaceId> {
957        self.discovery_path_requests
958            .remove(dest_hash)
959            .map(|req| req.requesting_interface)
960    }
961
962    // =========================================================================
963    // Inbound proof processing
964    // =========================================================================
965
966    fn process_inbound_proof(
967        &mut self,
968        packet: &RawPacket,
969        iface: InterfaceId,
970        _now: f64,
971        actions: &mut Vec<TransportAction>,
972    ) {
973        if packet.context == constants::CONTEXT_LRPROOF {
974            // Link request proof routing
975            if (self.config.transport_enabled)
976                && self.link_table.contains_key(&packet.destination_hash)
977            {
978                let link_entry = self.link_table.get(&packet.destination_hash).cloned();
979                if let Some(entry) = link_entry {
980                    if packet.hops == entry.remaining_hops && iface == entry.next_hop_interface {
981                        // Forward the proof (simplified: skip signature validation
982                        // which requires Identity recall)
983                        let mut new_raw = Vec::new();
984                        new_raw.push(packet.raw[0]);
985                        new_raw.push(packet.hops);
986                        new_raw.extend_from_slice(&packet.raw[2..]);
987
988                        // Mark link as validated
989                        if let Some(le) = self.link_table.get_mut(&packet.destination_hash) {
990                            le.validated = true;
991                        }
992
993                        actions.push(TransportAction::LinkEstablished {
994                            link_id: packet.destination_hash,
995                            interface: entry.received_interface,
996                        });
997
998                        actions.push(TransportAction::SendOnInterface {
999                            interface: entry.received_interface,
1000                            raw: new_raw,
1001                        });
1002                    }
1003                }
1004            } else {
1005                // Could be for a local pending link - deliver locally
1006                actions.push(TransportAction::DeliverLocal {
1007                    destination_hash: packet.destination_hash,
1008                    raw: packet.raw.clone(),
1009                    packet_hash: packet.packet_hash,
1010                    receiving_interface: iface,
1011                });
1012            }
1013        } else {
1014            // Regular proof: check reverse table
1015            if self.config.transport_enabled {
1016                if let Some(reverse_entry) = self.reverse_table.remove(&packet.destination_hash) {
1017                    if let Some(action) = route_proof_via_reverse(packet, &reverse_entry, iface) {
1018                        actions.push(action);
1019                    }
1020                }
1021            }
1022
1023            // Deliver to local receipts
1024            actions.push(TransportAction::DeliverLocal {
1025                destination_hash: packet.destination_hash,
1026                raw: packet.raw.clone(),
1027                packet_hash: packet.packet_hash,
1028                receiving_interface: iface,
1029            });
1030        }
1031    }
1032
1033    // =========================================================================
1034    // Core API: handle_outbound
1035    // =========================================================================
1036
1037    /// Route an outbound packet.
1038    pub fn handle_outbound(
1039        &mut self,
1040        packet: &RawPacket,
1041        dest_type: u8,
1042        attached_interface: Option<InterfaceId>,
1043        now: f64,
1044    ) -> Vec<TransportAction> {
1045        let actions = route_outbound(
1046            &self.path_table,
1047            &self.interfaces,
1048            &self.local_destinations,
1049            packet,
1050            dest_type,
1051            attached_interface,
1052            now,
1053        );
1054
1055        // Add to packet hashlist for outbound packets
1056        self.packet_hashlist.add(packet.packet_hash);
1057
1058        // Gate announces with hops > 0 through the bandwidth queue
1059        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE && packet.hops > 0 {
1060            self.gate_announce_actions(actions, &packet.destination_hash, packet.hops, now)
1061        } else {
1062            actions
1063        }
1064    }
1065
1066    /// Gate announce SendOnInterface actions through per-interface bandwidth queues.
1067    fn gate_announce_actions(
1068        &mut self,
1069        actions: Vec<TransportAction>,
1070        dest_hash: &[u8; 16],
1071        hops: u8,
1072        now: f64,
1073    ) -> Vec<TransportAction> {
1074        let mut result = Vec::new();
1075        for action in actions {
1076            match action {
1077                TransportAction::SendOnInterface { interface, raw } => {
1078                    let (bitrate, announce_cap) =
1079                        if let Some(info) = self.interfaces.get(&interface) {
1080                            (info.bitrate, info.announce_cap)
1081                        } else {
1082                            (None, constants::ANNOUNCE_CAP)
1083                        };
1084                    if let Some(send_action) = self.announce_queues.gate_announce(
1085                        interface,
1086                        raw,
1087                        *dest_hash,
1088                        hops,
1089                        now,
1090                        now,
1091                        bitrate,
1092                        announce_cap,
1093                    ) {
1094                        result.push(send_action);
1095                    }
1096                    // If None, it was queued — no action emitted now
1097                }
1098                other => result.push(other),
1099            }
1100        }
1101        result
1102    }
1103
1104    // =========================================================================
1105    // Core API: tick
1106    // =========================================================================
1107
1108    /// Periodic maintenance. Call regularly (e.g., every 250ms).
1109    pub fn tick(&mut self, now: f64, rng: &mut dyn Rng) -> Vec<TransportAction> {
1110        let mut actions = Vec::new();
1111
1112        // Process pending announces
1113        if now > self.announces_last_checked + constants::ANNOUNCES_CHECK_INTERVAL {
1114            if let Some(ref identity_hash) = self.config.identity_hash {
1115                let ih = *identity_hash;
1116                let announce_actions = jobs::process_pending_announces(
1117                    &mut self.announce_table,
1118                    &mut self.held_announces,
1119                    &ih,
1120                    now,
1121                );
1122                // Gate retransmitted announces through bandwidth queues
1123                let gated = self.gate_retransmit_actions(announce_actions, now);
1124                actions.extend(gated);
1125            }
1126            self.announces_last_checked = now;
1127        }
1128
1129        // Process announce queues — dequeue waiting announces when bandwidth available
1130        let mut queue_actions = self.announce_queues.process_queues(now, &self.interfaces);
1131        actions.append(&mut queue_actions);
1132
1133        // Process ingress control: release held announces
1134        let ic_interfaces = self.ingress_control.interfaces_with_held();
1135        for iface_id in ic_interfaces {
1136            let (ia_freq, started, ic_enabled) = match self.interfaces.get(&iface_id) {
1137                Some(info) => (info.ia_freq, info.started, info.ingress_control),
1138                None => continue,
1139            };
1140            if !ic_enabled {
1141                continue;
1142            }
1143            if let Some(held) = self
1144                .ingress_control
1145                .process_held_announces(iface_id, ia_freq, started, now)
1146            {
1147                let released_actions =
1148                    self.handle_inbound(&held.raw, held.receiving_interface, now, rng);
1149                actions.extend(released_actions);
1150            }
1151        }
1152
1153        // Cull tables
1154        if now > self.tables_last_culled + constants::TABLES_CULL_INTERVAL {
1155            jobs::cull_path_table(&mut self.path_table, &self.interfaces, now);
1156            jobs::cull_reverse_table(&mut self.reverse_table, &self.interfaces, now);
1157            let (_culled, link_closed_actions) =
1158                jobs::cull_link_table(&mut self.link_table, &self.interfaces, now);
1159            actions.extend(link_closed_actions);
1160            jobs::cull_path_states(&mut self.path_states, &self.path_table);
1161            self.cull_blackholed(now);
1162            // Cull expired discovery path requests
1163            self.discovery_path_requests
1164                .retain(|_, req| now - req.timestamp < constants::DISCOVERY_PATH_REQUEST_TIMEOUT);
1165            // Cull tunnels: void missing interfaces, then remove expired
1166            self.tunnel_table
1167                .void_missing_interfaces(|id| self.interfaces.contains_key(id));
1168            self.tunnel_table.cull(now);
1169            self.tables_last_culled = now;
1170        }
1171
1172        // Cull PR tags if over limit
1173        if self.discovery_pr_tags.len() > constants::MAX_PR_TAGS {
1174            let start = self.discovery_pr_tags.len() - constants::MAX_PR_TAGS;
1175            self.discovery_pr_tags = self.discovery_pr_tags[start..].to_vec();
1176        }
1177
1178        actions
1179    }
1180
1181    /// Gate retransmitted announce actions through per-interface bandwidth queues.
1182    ///
1183    /// Retransmitted announces always have hops > 0.
1184    /// `BroadcastOnAllInterfaces` is expanded to per-interface sends gated through queues.
1185    fn gate_retransmit_actions(
1186        &mut self,
1187        actions: Vec<TransportAction>,
1188        now: f64,
1189    ) -> Vec<TransportAction> {
1190        let mut result = Vec::new();
1191        for action in actions {
1192            match action {
1193                TransportAction::SendOnInterface { interface, raw } => {
1194                    // Extract dest_hash from raw (bytes 2..18 for H1, 18..34 for H2)
1195                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
1196                    let (bitrate, announce_cap) =
1197                        if let Some(info) = self.interfaces.get(&interface) {
1198                            (info.bitrate, info.announce_cap)
1199                        } else {
1200                            (None, constants::ANNOUNCE_CAP)
1201                        };
1202                    if let Some(send_action) = self.announce_queues.gate_announce(
1203                        interface,
1204                        raw,
1205                        dest_hash,
1206                        hops,
1207                        now,
1208                        now,
1209                        bitrate,
1210                        announce_cap,
1211                    ) {
1212                        result.push(send_action);
1213                    }
1214                }
1215                TransportAction::BroadcastOnAllInterfaces { raw, exclude } => {
1216                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
1217                    // Expand to per-interface sends gated through queues,
1218                    // applying mode filtering (AP blocks non-local announces, etc.)
1219                    let iface_ids: Vec<(InterfaceId, Option<u64>, f64)> = self
1220                        .interfaces
1221                        .iter()
1222                        .filter(|(_, info)| info.out_capable)
1223                        .filter(|(id, _)| {
1224                            if let Some(ref ex) = exclude {
1225                                **id != *ex
1226                            } else {
1227                                true
1228                            }
1229                        })
1230                        .filter(|(_, info)| {
1231                            should_transmit_announce(
1232                                info,
1233                                &dest_hash,
1234                                hops,
1235                                &self.local_destinations,
1236                                &self.path_table,
1237                                &self.interfaces,
1238                            )
1239                        })
1240                        .map(|(id, info)| (*id, info.bitrate, info.announce_cap))
1241                        .collect();
1242
1243                    for (iface_id, bitrate, announce_cap) in iface_ids {
1244                        if let Some(send_action) = self.announce_queues.gate_announce(
1245                            iface_id,
1246                            raw.clone(),
1247                            dest_hash,
1248                            hops,
1249                            now,
1250                            now,
1251                            bitrate,
1252                            announce_cap,
1253                        ) {
1254                            result.push(send_action);
1255                        }
1256                    }
1257                }
1258                other => result.push(other),
1259            }
1260        }
1261        result
1262    }
1263
1264    /// Extract destination hash and hops from raw announce bytes.
1265    fn extract_announce_info(raw: &[u8]) -> ([u8; 16], u8) {
1266        if raw.len() < 18 {
1267            return ([0; 16], 0);
1268        }
1269        let header_type = (raw[0] >> 6) & 0x03;
1270        let hops = raw[1];
1271        if header_type == constants::HEADER_2 && raw.len() >= 34 {
1272            // H2: transport_id at [2..18], dest_hash at [18..34]
1273            let mut dest = [0u8; 16];
1274            dest.copy_from_slice(&raw[18..34]);
1275            (dest, hops)
1276        } else {
1277            // H1: dest_hash at [2..18]
1278            let mut dest = [0u8; 16];
1279            dest.copy_from_slice(&raw[2..18]);
1280            (dest, hops)
1281        }
1282    }
1283
1284    // =========================================================================
1285    // Path request handling
1286    // =========================================================================
1287
1288    /// Handle an incoming path request.
1289    ///
1290    /// Transport.py path_request_handler (~line 2700):
1291    /// - Dedup via unique tag
1292    /// - If local destination, caller handles announce
1293    /// - If path known and transport enabled, queue retransmit (with ROAMING loop prevention)
1294    /// - If path unknown and receiving interface is in DISCOVER_PATHS_FOR, store a
1295    ///   DiscoveryPathRequest and forward the raw path request on other OUT interfaces
1296    pub fn handle_path_request(
1297        &mut self,
1298        data: &[u8],
1299        interface_id: InterfaceId,
1300        now: f64,
1301    ) -> Vec<TransportAction> {
1302        let mut actions = Vec::new();
1303
1304        if data.len() < 16 {
1305            return actions;
1306        }
1307
1308        let mut destination_hash = [0u8; 16];
1309        destination_hash.copy_from_slice(&data[..16]);
1310
1311        // Extract requesting transport instance
1312        let _requesting_transport_id = if data.len() > 32 {
1313            let mut id = [0u8; 16];
1314            id.copy_from_slice(&data[16..32]);
1315            Some(id)
1316        } else {
1317            None
1318        };
1319
1320        // Extract tag
1321        let tag_bytes = if data.len() > 32 {
1322            Some(&data[32..])
1323        } else if data.len() > 16 {
1324            Some(&data[16..])
1325        } else {
1326            None
1327        };
1328
1329        if let Some(tag) = tag_bytes {
1330            let tag_len = tag.len().min(16);
1331            let mut unique_tag = [0u8; 32];
1332            unique_tag[..16].copy_from_slice(&destination_hash);
1333            unique_tag[16..16 + tag_len].copy_from_slice(&tag[..tag_len]);
1334
1335            if self.discovery_pr_tags.contains(&unique_tag) {
1336                return actions; // Duplicate tag
1337            }
1338            self.discovery_pr_tags.push(unique_tag);
1339        } else {
1340            return actions; // Tagless request
1341        }
1342
1343        // If destination is local, the caller should handle the announce
1344        if self.local_destinations.contains_key(&destination_hash) {
1345            return actions;
1346        }
1347
1348        // If we know the path and transport is enabled, queue retransmit
1349        if self.config.transport_enabled && self.has_path(&destination_hash) {
1350            let path = self
1351                .path_table
1352                .get(&destination_hash)
1353                .unwrap()
1354                .primary()
1355                .unwrap()
1356                .clone();
1357
1358            // ROAMING loop prevention (Python Transport.py:2731-2732):
1359            // If the receiving interface is ROAMING and the known path's next-hop
1360            // is on the same interface, don't answer — it would loop.
1361            if let Some(recv_info) = self.interfaces.get(&interface_id) {
1362                if recv_info.mode == constants::MODE_ROAMING
1363                    && path.receiving_interface == interface_id
1364                {
1365                    return actions;
1366                }
1367            }
1368
1369            // We need the original announce raw bytes to build a valid retransmit.
1370            // Without them we can't populate packet_raw/packet_data and the response
1371            // would be a header-only packet that Python RNS discards.
1372            if let Some(ref raw) = path.announce_raw {
1373                // Check if there's already an announce in the table
1374                if let Some(existing) = self.announce_table.remove(&destination_hash) {
1375                    self.held_announces.insert(destination_hash, existing);
1376                }
1377                let retransmit_timeout =
1378                    if let Some(iface_info) = self.interfaces.get(&interface_id) {
1379                        let base = now + constants::PATH_REQUEST_GRACE;
1380                        if iface_info.mode == constants::MODE_ROAMING {
1381                            base + constants::PATH_REQUEST_RG
1382                        } else {
1383                            base
1384                        }
1385                    } else {
1386                        now + constants::PATH_REQUEST_GRACE
1387                    };
1388
1389                let (packet_data, context_flag) = match RawPacket::unpack(raw) {
1390                    Ok(parsed) => (parsed.data, parsed.flags.context_flag),
1391                    Err(_) => {
1392                        return actions;
1393                    }
1394                };
1395
1396                let entry = AnnounceEntry {
1397                    timestamp: now,
1398                    retransmit_timeout,
1399                    retries: constants::PATHFINDER_R,
1400                    received_from: path.next_hop,
1401                    hops: path.hops,
1402                    packet_raw: raw.clone(),
1403                    packet_data,
1404                    destination_hash,
1405                    context_flag,
1406                    local_rebroadcasts: 0,
1407                    block_rebroadcasts: true,
1408                    attached_interface: Some(interface_id),
1409                };
1410
1411                self.announce_table.insert(destination_hash, entry);
1412            }
1413        } else if self.config.transport_enabled {
1414            // Unknown path: check if receiving interface is in DISCOVER_PATHS_FOR
1415            let should_discover = self
1416                .interfaces
1417                .get(&interface_id)
1418                .map(|info| constants::DISCOVER_PATHS_FOR.contains(&info.mode))
1419                .unwrap_or(false);
1420
1421            if should_discover {
1422                // Store discovery request so we can respond when the announce arrives
1423                self.discovery_path_requests.insert(
1424                    destination_hash,
1425                    DiscoveryPathRequest {
1426                        timestamp: now,
1427                        requesting_interface: interface_id,
1428                    },
1429                );
1430
1431                // Forward the raw path request data on all other OUT-capable interfaces
1432                for (_, iface_info) in self.interfaces.iter() {
1433                    if iface_info.id != interface_id && iface_info.out_capable {
1434                        actions.push(TransportAction::SendOnInterface {
1435                            interface: iface_info.id,
1436                            raw: data.to_vec(),
1437                        });
1438                    }
1439                }
1440            }
1441        }
1442
1443        actions
1444    }
1445
1446    // =========================================================================
1447    // Public read accessors
1448    // =========================================================================
1449
1450    /// Iterate over primary path entries (one per destination).
1451    pub fn path_table_entries(&self) -> impl Iterator<Item = (&[u8; 16], &PathEntry)> {
1452        self.path_table
1453            .iter()
1454            .filter_map(|(k, ps)| ps.primary().map(|e| (k, e)))
1455    }
1456
1457    /// Iterate over all path sets (exposing alternatives).
1458    pub fn path_table_sets(&self) -> impl Iterator<Item = (&[u8; 16], &PathSet)> {
1459        self.path_table.iter()
1460    }
1461
1462    /// Number of registered interfaces.
1463    pub fn interface_count(&self) -> usize {
1464        self.interfaces.len()
1465    }
1466
1467    /// Number of link table entries.
1468    pub fn link_table_count(&self) -> usize {
1469        self.link_table.len()
1470    }
1471
1472    /// Number of path table entries.
1473    pub fn path_table_count(&self) -> usize {
1474        self.path_table.len()
1475    }
1476
1477    /// Number of announce table entries.
1478    pub fn announce_table_count(&self) -> usize {
1479        self.announce_table.len()
1480    }
1481
1482    /// Number of reverse table entries.
1483    pub fn reverse_table_count(&self) -> usize {
1484        self.reverse_table.len()
1485    }
1486
1487    /// Number of held announces.
1488    pub fn held_announces_count(&self) -> usize {
1489        self.held_announces.len()
1490    }
1491
1492    /// Number of entries in the packet hashlist.
1493    pub fn packet_hashlist_len(&self) -> usize {
1494        self.packet_hashlist.len()
1495    }
1496
1497    /// Number of entries in the rate limiter.
1498    pub fn rate_limiter_count(&self) -> usize {
1499        self.rate_limiter.len()
1500    }
1501
1502    /// Number of blackholed identities.
1503    pub fn blackholed_count(&self) -> usize {
1504        self.blackholed_identities.len()
1505    }
1506
1507    /// Number of tunnel table entries.
1508    pub fn tunnel_count(&self) -> usize {
1509        self.tunnel_table.len()
1510    }
1511
1512    /// Number of discovery PR tags.
1513    pub fn discovery_pr_tags_count(&self) -> usize {
1514        self.discovery_pr_tags.len()
1515    }
1516
1517    /// Number of discovery path requests.
1518    pub fn discovery_path_requests_count(&self) -> usize {
1519        self.discovery_path_requests.len()
1520    }
1521
1522    /// Number of local destinations.
1523    pub fn local_destinations_count(&self) -> usize {
1524        self.local_destinations.len()
1525    }
1526
1527    /// Access the rate limiter for reading rate table entries.
1528    pub fn rate_limiter(&self) -> &AnnounceRateLimiter {
1529        &self.rate_limiter
1530    }
1531
1532    /// Get interface info by id.
1533    pub fn interface_info(&self, id: &InterfaceId) -> Option<&InterfaceInfo> {
1534        self.interfaces.get(id)
1535    }
1536
1537    /// Redirect a path entry to a different interface (e.g. after direct connect).
1538    /// If no entry exists, creates a minimal direct path (hops=1).
1539    pub fn redirect_path(&mut self, dest_hash: &[u8; 16], interface: InterfaceId, now: f64) {
1540        if let Some(entry) = self
1541            .path_table
1542            .get_mut(dest_hash)
1543            .and_then(|ps| ps.primary_mut())
1544        {
1545            entry.receiving_interface = interface;
1546            entry.hops = 1;
1547        } else {
1548            let max_paths = self.config.max_paths_per_destination;
1549            self.path_table.insert(
1550                *dest_hash,
1551                PathSet::from_single(
1552                    PathEntry {
1553                        timestamp: now,
1554                        next_hop: [0u8; 16],
1555                        hops: 1,
1556                        expires: now + 3600.0,
1557                        random_blobs: Vec::new(),
1558                        receiving_interface: interface,
1559                        packet_hash: [0u8; 32],
1560                        announce_raw: None,
1561                    },
1562                    max_paths,
1563                ),
1564            );
1565        }
1566    }
1567
1568    /// Inject a path entry directly into the path table (full override).
1569    pub fn inject_path(&mut self, dest_hash: [u8; 16], entry: PathEntry) {
1570        let max_paths = self.config.max_paths_per_destination;
1571        self.path_table
1572            .insert(dest_hash, PathSet::from_single(entry, max_paths));
1573    }
1574
1575    /// Drop a path from the path table.
1576    pub fn drop_path(&mut self, dest_hash: &[u8; 16]) -> bool {
1577        self.path_table.remove(dest_hash).is_some()
1578    }
1579
1580    /// Drop all paths that route via a given transport hash.
1581    ///
1582    /// Removes matching individual paths from each PathSet, then removes
1583    /// any PathSets that become empty.
1584    pub fn drop_all_via(&mut self, transport_hash: &[u8; 16]) -> usize {
1585        let mut removed = 0usize;
1586        for ps in self.path_table.values_mut() {
1587            let before = ps.len();
1588            ps.retain(|entry| &entry.next_hop != transport_hash);
1589            removed += before - ps.len();
1590        }
1591        self.path_table.retain(|_, ps| !ps.is_empty());
1592        removed
1593    }
1594
1595    /// Drop all path entries learned via a given interface.
1596    pub fn drop_paths_for_interface(&mut self, interface: InterfaceId) -> usize {
1597        let mut removed = 0usize;
1598        let mut cleared_destinations = Vec::new();
1599        for (dest_hash, ps) in self.path_table.iter_mut() {
1600            let before = ps.len();
1601            ps.retain(|entry| entry.receiving_interface != interface);
1602            if ps.is_empty() {
1603                cleared_destinations.push(*dest_hash);
1604            }
1605            removed += before - ps.len();
1606        }
1607        self.path_table.retain(|_, ps| !ps.is_empty());
1608        for dest_hash in cleared_destinations {
1609            self.path_states.remove(&dest_hash);
1610        }
1611        removed
1612    }
1613
1614    /// Drop all reverse table entries that reference a given interface.
1615    pub fn drop_reverse_for_interface(&mut self, interface: InterfaceId) -> usize {
1616        let before = self.reverse_table.len();
1617        self.reverse_table.retain(|_, entry| {
1618            entry.receiving_interface != interface && entry.outbound_interface != interface
1619        });
1620        before - self.reverse_table.len()
1621    }
1622
1623    /// Drop all link table entries that reference a given interface.
1624    pub fn drop_links_for_interface(&mut self, interface: InterfaceId) -> usize {
1625        let before = self.link_table.len();
1626        self.link_table.retain(|_, entry| {
1627            entry.next_hop_interface != interface && entry.received_interface != interface
1628        });
1629        before - self.link_table.len()
1630    }
1631
1632    /// Drop all pending announce retransmissions and bandwidth queues.
1633    pub fn drop_announce_queues(&mut self) {
1634        self.announce_table.clear();
1635        self.held_announces.clear();
1636        self.announce_queues = AnnounceQueues::new();
1637        self.ingress_control.clear();
1638    }
1639
1640    /// Get the transport identity hash.
1641    pub fn identity_hash(&self) -> Option<&[u8; 16]> {
1642        self.config.identity_hash.as_ref()
1643    }
1644
1645    /// Whether transport is enabled.
1646    pub fn transport_enabled(&self) -> bool {
1647        self.config.transport_enabled
1648    }
1649
1650    /// Access the transport configuration.
1651    pub fn config(&self) -> &TransportConfig {
1652        &self.config
1653    }
1654
1655    pub fn set_packet_hashlist_max_entries(&mut self, max_entries: usize) {
1656        self.config.packet_hashlist_max_entries = max_entries;
1657        self.packet_hashlist = PacketHashlist::new(max_entries);
1658    }
1659
1660    /// Get path table entries as tuples for management queries.
1661    /// Returns (dest_hash, timestamp, next_hop, hops, expires, interface_name).
1662    /// Reports primaries only for backward compatibility.
1663    pub fn get_path_table(&self, max_hops: Option<u8>) -> Vec<PathTableRow> {
1664        let mut result = Vec::new();
1665        for (dest_hash, ps) in self.path_table.iter() {
1666            if let Some(entry) = ps.primary() {
1667                if let Some(max) = max_hops {
1668                    if entry.hops > max {
1669                        continue;
1670                    }
1671                }
1672                let iface_name = self
1673                    .interfaces
1674                    .get(&entry.receiving_interface)
1675                    .map(|i| i.name.clone())
1676                    .unwrap_or_else(|| {
1677                        alloc::format!("Interface({})", entry.receiving_interface.0)
1678                    });
1679                result.push((
1680                    *dest_hash,
1681                    entry.timestamp,
1682                    entry.next_hop,
1683                    entry.hops,
1684                    entry.expires,
1685                    iface_name,
1686                ));
1687            }
1688        }
1689        result
1690    }
1691
1692    /// Get rate table entries as tuples for management queries.
1693    /// Returns (dest_hash, last, rate_violations, blocked_until, timestamps).
1694    pub fn get_rate_table(&self) -> Vec<RateTableRow> {
1695        self.rate_limiter
1696            .entries()
1697            .map(|(hash, entry)| {
1698                (
1699                    *hash,
1700                    entry.last,
1701                    entry.rate_violations,
1702                    entry.blocked_until,
1703                    entry.timestamps.clone(),
1704                )
1705            })
1706            .collect()
1707    }
1708
1709    /// Get blackholed identities as tuples for management queries.
1710    /// Returns (identity_hash, created, expires, reason).
1711    pub fn get_blackholed(&self) -> Vec<([u8; 16], f64, f64, Option<alloc::string::String>)> {
1712        self.blackholed_entries()
1713            .map(|(hash, entry)| (*hash, entry.created, entry.expires, entry.reason.clone()))
1714            .collect()
1715    }
1716
1717    // =========================================================================
1718    // Cleanup
1719    // =========================================================================
1720
1721    /// Return the set of destination hashes that currently have active paths.
1722    pub fn active_destination_hashes(&self) -> alloc::collections::BTreeSet<[u8; 16]> {
1723        self.path_table.keys().copied().collect()
1724    }
1725
1726    /// Collect all packet hashes from active path entries (all paths, not just primaries).
1727    pub fn active_packet_hashes(&self) -> Vec<[u8; 32]> {
1728        self.path_table
1729            .values()
1730            .flat_map(|ps| ps.iter().map(|p| p.packet_hash))
1731            .collect()
1732    }
1733
1734    /// Cull rate limiter entries for destinations that are neither active nor recently used.
1735    /// Returns the number of removed entries.
1736    pub fn cull_rate_limiter(
1737        &mut self,
1738        active: &alloc::collections::BTreeSet<[u8; 16]>,
1739        now: f64,
1740        ttl_secs: f64,
1741    ) -> usize {
1742        self.rate_limiter.cull_stale(active, now, ttl_secs)
1743    }
1744
1745    // =========================================================================
1746    // Ingress control
1747    // =========================================================================
1748
1749    /// Update the incoming announce frequency for an interface.
1750    pub fn update_interface_freq(&mut self, id: InterfaceId, ia_freq: f64) {
1751        if let Some(info) = self.interfaces.get_mut(&id) {
1752            info.ia_freq = ia_freq;
1753        }
1754    }
1755
1756    /// Get the count of held announces for an interface (for management reporting).
1757    pub fn held_announce_count(&self, interface: &InterfaceId) -> usize {
1758        self.ingress_control.held_count(interface)
1759    }
1760
1761    // =========================================================================
1762    // Testing helpers
1763    // =========================================================================
1764
1765    #[cfg(test)]
1766    #[allow(dead_code)]
1767    pub(crate) fn path_table(&self) -> &BTreeMap<[u8; 16], PathSet> {
1768        &self.path_table
1769    }
1770
1771    #[cfg(test)]
1772    #[allow(dead_code)]
1773    pub(crate) fn announce_table(&self) -> &BTreeMap<[u8; 16], AnnounceEntry> {
1774        &self.announce_table
1775    }
1776
1777    #[cfg(test)]
1778    #[allow(dead_code)]
1779    pub(crate) fn reverse_table(&self) -> &BTreeMap<[u8; 16], tables::ReverseEntry> {
1780        &self.reverse_table
1781    }
1782
1783    #[cfg(test)]
1784    #[allow(dead_code)]
1785    pub(crate) fn link_table_ref(&self) -> &BTreeMap<[u8; 16], LinkEntry> {
1786        &self.link_table
1787    }
1788}
1789
1790#[cfg(test)]
1791mod tests {
1792    use super::*;
1793    use crate::packet::PacketFlags;
1794
1795    fn make_config(transport_enabled: bool) -> TransportConfig {
1796        TransportConfig {
1797            transport_enabled,
1798            identity_hash: if transport_enabled {
1799                Some([0x42; 16])
1800            } else {
1801                None
1802            },
1803            prefer_shorter_path: false,
1804            max_paths_per_destination: 1,
1805            packet_hashlist_max_entries: constants::HASHLIST_MAXSIZE,
1806        }
1807    }
1808
1809    fn make_interface(id: u64, mode: u8) -> InterfaceInfo {
1810        InterfaceInfo {
1811            id: InterfaceId(id),
1812            name: String::from("test"),
1813            mode,
1814            out_capable: true,
1815            in_capable: true,
1816            bitrate: None,
1817            announce_rate_target: None,
1818            announce_rate_grace: 0,
1819            announce_rate_penalty: 0.0,
1820            announce_cap: constants::ANNOUNCE_CAP,
1821            is_local_client: false,
1822            wants_tunnel: false,
1823            tunnel_id: None,
1824            mtu: constants::MTU as u32,
1825            ingress_control: false,
1826            ia_freq: 0.0,
1827            started: 0.0,
1828        }
1829    }
1830
1831    #[test]
1832    fn test_empty_engine() {
1833        let engine = TransportEngine::new(make_config(false));
1834        assert!(!engine.has_path(&[0; 16]));
1835        assert!(engine.hops_to(&[0; 16]).is_none());
1836        assert!(engine.next_hop(&[0; 16]).is_none());
1837    }
1838
1839    #[test]
1840    fn test_register_deregister_interface() {
1841        let mut engine = TransportEngine::new(make_config(false));
1842        engine.register_interface(make_interface(1, constants::MODE_FULL));
1843        assert!(engine.interfaces.contains_key(&InterfaceId(1)));
1844
1845        engine.deregister_interface(InterfaceId(1));
1846        assert!(!engine.interfaces.contains_key(&InterfaceId(1)));
1847    }
1848
1849    #[test]
1850    fn test_register_deregister_destination() {
1851        let mut engine = TransportEngine::new(make_config(false));
1852        let dest = [0x11; 16];
1853        engine.register_destination(dest, constants::DESTINATION_SINGLE);
1854        assert!(engine.local_destinations.contains_key(&dest));
1855
1856        engine.deregister_destination(&dest);
1857        assert!(!engine.local_destinations.contains_key(&dest));
1858    }
1859
1860    #[test]
1861    fn test_path_state() {
1862        let mut engine = TransportEngine::new(make_config(false));
1863        let dest = [0x22; 16];
1864
1865        assert!(!engine.path_is_unresponsive(&dest));
1866
1867        engine.mark_path_unresponsive(&dest, None);
1868        assert!(engine.path_is_unresponsive(&dest));
1869
1870        engine.mark_path_responsive(&dest);
1871        assert!(!engine.path_is_unresponsive(&dest));
1872    }
1873
1874    #[test]
1875    fn test_boundary_exempts_unresponsive() {
1876        let mut engine = TransportEngine::new(make_config(false));
1877        engine.register_interface(make_interface(1, constants::MODE_BOUNDARY));
1878        let dest = [0xB1; 16];
1879
1880        // Marking via a boundary interface should be skipped
1881        engine.mark_path_unresponsive(&dest, Some(InterfaceId(1)));
1882        assert!(!engine.path_is_unresponsive(&dest));
1883    }
1884
1885    #[test]
1886    fn test_non_boundary_marks_unresponsive() {
1887        let mut engine = TransportEngine::new(make_config(false));
1888        engine.register_interface(make_interface(1, constants::MODE_FULL));
1889        let dest = [0xB2; 16];
1890
1891        // Marking via a non-boundary interface should work
1892        engine.mark_path_unresponsive(&dest, Some(InterfaceId(1)));
1893        assert!(engine.path_is_unresponsive(&dest));
1894    }
1895
1896    #[test]
1897    fn test_expire_path() {
1898        let mut engine = TransportEngine::new(make_config(false));
1899        let dest = [0x33; 16];
1900
1901        engine.path_table.insert(
1902            dest,
1903            PathSet::from_single(
1904                PathEntry {
1905                    timestamp: 1000.0,
1906                    next_hop: [0; 16],
1907                    hops: 2,
1908                    expires: 9999.0,
1909                    random_blobs: Vec::new(),
1910                    receiving_interface: InterfaceId(1),
1911                    packet_hash: [0; 32],
1912                    announce_raw: None,
1913                },
1914                1,
1915            ),
1916        );
1917
1918        assert!(engine.has_path(&dest));
1919        engine.expire_path(&dest);
1920        // Path still exists but expires = 0
1921        assert!(engine.has_path(&dest));
1922        assert_eq!(engine.path_table[&dest].primary().unwrap().expires, 0.0);
1923    }
1924
1925    #[test]
1926    fn test_link_table_operations() {
1927        let mut engine = TransportEngine::new(make_config(false));
1928        let link_id = [0x44; 16];
1929
1930        engine.register_link(
1931            link_id,
1932            LinkEntry {
1933                timestamp: 100.0,
1934                next_hop_transport_id: [0; 16],
1935                next_hop_interface: InterfaceId(1),
1936                remaining_hops: 3,
1937                received_interface: InterfaceId(2),
1938                taken_hops: 2,
1939                destination_hash: [0xAA; 16],
1940                validated: false,
1941                proof_timeout: 200.0,
1942            },
1943        );
1944
1945        assert!(engine.link_table.contains_key(&link_id));
1946        assert!(!engine.link_table[&link_id].validated);
1947
1948        engine.validate_link(&link_id);
1949        assert!(engine.link_table[&link_id].validated);
1950
1951        engine.remove_link(&link_id);
1952        assert!(!engine.link_table.contains_key(&link_id));
1953    }
1954
1955    #[test]
1956    fn test_packet_filter_drops_plain_announce() {
1957        let engine = TransportEngine::new(make_config(false));
1958        let flags = PacketFlags {
1959            header_type: constants::HEADER_1,
1960            context_flag: constants::FLAG_UNSET,
1961            transport_type: constants::TRANSPORT_BROADCAST,
1962            destination_type: constants::DESTINATION_PLAIN,
1963            packet_type: constants::PACKET_TYPE_ANNOUNCE,
1964        };
1965        let packet =
1966            RawPacket::pack(flags, 0, &[0; 16], None, constants::CONTEXT_NONE, b"test").unwrap();
1967        assert!(!engine.packet_filter(&packet));
1968    }
1969
1970    #[test]
1971    fn test_packet_filter_allows_keepalive() {
1972        let engine = TransportEngine::new(make_config(false));
1973        let flags = PacketFlags {
1974            header_type: constants::HEADER_1,
1975            context_flag: constants::FLAG_UNSET,
1976            transport_type: constants::TRANSPORT_BROADCAST,
1977            destination_type: constants::DESTINATION_SINGLE,
1978            packet_type: constants::PACKET_TYPE_DATA,
1979        };
1980        let packet = RawPacket::pack(
1981            flags,
1982            0,
1983            &[0; 16],
1984            None,
1985            constants::CONTEXT_KEEPALIVE,
1986            b"test",
1987        )
1988        .unwrap();
1989        assert!(engine.packet_filter(&packet));
1990    }
1991
1992    #[test]
1993    fn test_packet_filter_drops_high_hop_plain() {
1994        let engine = TransportEngine::new(make_config(false));
1995        let flags = PacketFlags {
1996            header_type: constants::HEADER_1,
1997            context_flag: constants::FLAG_UNSET,
1998            transport_type: constants::TRANSPORT_BROADCAST,
1999            destination_type: constants::DESTINATION_PLAIN,
2000            packet_type: constants::PACKET_TYPE_DATA,
2001        };
2002        let mut packet =
2003            RawPacket::pack(flags, 0, &[0; 16], None, constants::CONTEXT_NONE, b"test").unwrap();
2004        packet.hops = 2;
2005        assert!(!engine.packet_filter(&packet));
2006    }
2007
2008    #[test]
2009    fn test_packet_filter_allows_duplicate_single_announce() {
2010        let mut engine = TransportEngine::new(make_config(false));
2011        let flags = PacketFlags {
2012            header_type: constants::HEADER_1,
2013            context_flag: constants::FLAG_UNSET,
2014            transport_type: constants::TRANSPORT_BROADCAST,
2015            destination_type: constants::DESTINATION_SINGLE,
2016            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2017        };
2018        let packet = RawPacket::pack(
2019            flags,
2020            0,
2021            &[0; 16],
2022            None,
2023            constants::CONTEXT_NONE,
2024            &[0xAA; 64],
2025        )
2026        .unwrap();
2027
2028        // Add to hashlist
2029        engine.packet_hashlist.add(packet.packet_hash);
2030
2031        // Should still pass filter (duplicate announce for SINGLE allowed)
2032        assert!(engine.packet_filter(&packet));
2033    }
2034
2035    #[test]
2036    fn test_packet_filter_fifo_eviction_allows_oldest_hash_again() {
2037        let mut engine = TransportEngine::new(make_config(false));
2038        engine.packet_hashlist = PacketHashlist::new(2);
2039
2040        let make_packet = |seed: u8| {
2041            let flags = PacketFlags {
2042                header_type: constants::HEADER_1,
2043                context_flag: constants::FLAG_UNSET,
2044                transport_type: constants::TRANSPORT_BROADCAST,
2045                destination_type: constants::DESTINATION_SINGLE,
2046                packet_type: constants::PACKET_TYPE_DATA,
2047            };
2048            RawPacket::pack(
2049                flags,
2050                0,
2051                &[seed; 16],
2052                None,
2053                constants::CONTEXT_NONE,
2054                &[seed; 4],
2055            )
2056            .unwrap()
2057        };
2058
2059        let packet1 = make_packet(1);
2060        let packet2 = make_packet(2);
2061        let packet3 = make_packet(3);
2062
2063        engine.packet_hashlist.add(packet1.packet_hash);
2064        engine.packet_hashlist.add(packet2.packet_hash);
2065        assert!(!engine.packet_filter(&packet1));
2066
2067        engine.packet_hashlist.add(packet3.packet_hash);
2068
2069        assert!(engine.packet_filter(&packet1));
2070        assert!(!engine.packet_filter(&packet2));
2071        assert!(!engine.packet_filter(&packet3));
2072    }
2073
2074    #[test]
2075    fn test_packet_filter_duplicate_does_not_refresh_recency() {
2076        let mut engine = TransportEngine::new(make_config(false));
2077        engine.packet_hashlist = PacketHashlist::new(2);
2078
2079        let make_packet = |seed: u8| {
2080            let flags = PacketFlags {
2081                header_type: constants::HEADER_1,
2082                context_flag: constants::FLAG_UNSET,
2083                transport_type: constants::TRANSPORT_BROADCAST,
2084                destination_type: constants::DESTINATION_SINGLE,
2085                packet_type: constants::PACKET_TYPE_DATA,
2086            };
2087            RawPacket::pack(
2088                flags,
2089                0,
2090                &[seed; 16],
2091                None,
2092                constants::CONTEXT_NONE,
2093                &[seed; 4],
2094            )
2095            .unwrap()
2096        };
2097
2098        let packet1 = make_packet(1);
2099        let packet2 = make_packet(2);
2100        let packet3 = make_packet(3);
2101
2102        engine.packet_hashlist.add(packet1.packet_hash);
2103        engine.packet_hashlist.add(packet2.packet_hash);
2104        engine.packet_hashlist.add(packet2.packet_hash);
2105        engine.packet_hashlist.add(packet3.packet_hash);
2106
2107        assert!(engine.packet_filter(&packet1));
2108        assert!(!engine.packet_filter(&packet2));
2109        assert!(!engine.packet_filter(&packet3));
2110    }
2111
2112    #[test]
2113    fn test_tick_retransmits_announce() {
2114        let mut engine = TransportEngine::new(make_config(true));
2115        engine.register_interface(make_interface(1, constants::MODE_FULL));
2116
2117        let dest = [0x55; 16];
2118        engine.announce_table.insert(
2119            dest,
2120            AnnounceEntry {
2121                timestamp: 100.0,
2122                retransmit_timeout: 100.0, // ready to retransmit
2123                retries: 0,
2124                received_from: [0xAA; 16],
2125                hops: 2,
2126                packet_raw: vec![0x01, 0x02],
2127                packet_data: vec![0xCC; 10],
2128                destination_hash: dest,
2129                context_flag: 0,
2130                local_rebroadcasts: 0,
2131                block_rebroadcasts: false,
2132                attached_interface: None,
2133            },
2134        );
2135
2136        let mut rng = rns_crypto::FixedRng::new(&[0x42; 32]);
2137        let actions = engine.tick(200.0, &mut rng);
2138
2139        // Should have a send action for the retransmit (gated through announce queue,
2140        // expanded from BroadcastOnAllInterfaces to per-interface SendOnInterface)
2141        assert!(!actions.is_empty());
2142        assert!(matches!(
2143            &actions[0],
2144            TransportAction::SendOnInterface { .. }
2145        ));
2146
2147        // Retries should have increased
2148        assert_eq!(engine.announce_table[&dest].retries, 1);
2149    }
2150
2151    #[test]
2152    fn test_blackhole_identity() {
2153        let mut engine = TransportEngine::new(make_config(false));
2154        let hash = [0xAA; 16];
2155        let now = 1000.0;
2156
2157        assert!(!engine.is_blackholed(&hash, now));
2158
2159        engine.blackhole_identity(hash, now, None, Some(String::from("test")));
2160        assert!(engine.is_blackholed(&hash, now));
2161        assert!(engine.is_blackholed(&hash, now + 999999.0)); // never expires
2162
2163        assert!(engine.unblackhole_identity(&hash));
2164        assert!(!engine.is_blackholed(&hash, now));
2165        assert!(!engine.unblackhole_identity(&hash)); // already removed
2166    }
2167
2168    #[test]
2169    fn test_blackhole_with_duration() {
2170        let mut engine = TransportEngine::new(make_config(false));
2171        let hash = [0xBB; 16];
2172        let now = 1000.0;
2173
2174        engine.blackhole_identity(hash, now, Some(1.0), None); // 1 hour
2175        assert!(engine.is_blackholed(&hash, now));
2176        assert!(engine.is_blackholed(&hash, now + 3599.0)); // just before expiry
2177        assert!(!engine.is_blackholed(&hash, now + 3601.0)); // after expiry
2178    }
2179
2180    #[test]
2181    fn test_cull_blackholed() {
2182        let mut engine = TransportEngine::new(make_config(false));
2183        let hash1 = [0xCC; 16];
2184        let hash2 = [0xDD; 16];
2185        let now = 1000.0;
2186
2187        engine.blackhole_identity(hash1, now, Some(1.0), None); // 1 hour
2188        engine.blackhole_identity(hash2, now, None, None); // never expires
2189
2190        engine.cull_blackholed(now + 4000.0); // past hash1 expiry
2191
2192        assert!(!engine.blackholed_identities.contains_key(&hash1));
2193        assert!(engine.blackholed_identities.contains_key(&hash2));
2194    }
2195
2196    #[test]
2197    fn test_blackhole_blocks_announce() {
2198        use crate::announce::AnnounceData;
2199        use crate::destination::{destination_hash, name_hash};
2200
2201        let mut engine = TransportEngine::new(make_config(false));
2202        engine.register_interface(make_interface(1, constants::MODE_FULL));
2203
2204        let identity =
2205            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x55; 32]));
2206        let dest_hash = destination_hash("test", &["app"], Some(identity.hash()));
2207        let name_h = name_hash("test", &["app"]);
2208        let random_hash = [0x42u8; 10];
2209
2210        let (announce_data, _) =
2211            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
2212
2213        let flags = PacketFlags {
2214            header_type: constants::HEADER_1,
2215            context_flag: constants::FLAG_UNSET,
2216            transport_type: constants::TRANSPORT_BROADCAST,
2217            destination_type: constants::DESTINATION_SINGLE,
2218            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2219        };
2220        let packet = RawPacket::pack(
2221            flags,
2222            0,
2223            &dest_hash,
2224            None,
2225            constants::CONTEXT_NONE,
2226            &announce_data,
2227        )
2228        .unwrap();
2229
2230        // Blackhole the identity
2231        let now = 1000.0;
2232        engine.blackhole_identity(*identity.hash(), now, None, None);
2233
2234        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
2235        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), now, &mut rng);
2236
2237        // Should produce no AnnounceReceived or PathUpdated actions
2238        assert!(actions
2239            .iter()
2240            .all(|a| !matches!(a, TransportAction::AnnounceReceived { .. })));
2241        assert!(actions
2242            .iter()
2243            .all(|a| !matches!(a, TransportAction::PathUpdated { .. })));
2244    }
2245
2246    #[test]
2247    fn test_tick_culls_expired_path() {
2248        let mut engine = TransportEngine::new(make_config(false));
2249        engine.register_interface(make_interface(1, constants::MODE_FULL));
2250
2251        let dest = [0x66; 16];
2252        engine.path_table.insert(
2253            dest,
2254            PathSet::from_single(
2255                PathEntry {
2256                    timestamp: 100.0,
2257                    next_hop: [0; 16],
2258                    hops: 2,
2259                    expires: 200.0,
2260                    random_blobs: Vec::new(),
2261                    receiving_interface: InterfaceId(1),
2262                    packet_hash: [0; 32],
2263                    announce_raw: None,
2264                },
2265                1,
2266            ),
2267        );
2268
2269        assert!(engine.has_path(&dest));
2270
2271        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2272        // Advance past cull interval and path expiry
2273        engine.tick(300.0, &mut rng);
2274
2275        assert!(!engine.has_path(&dest));
2276    }
2277
2278    // =========================================================================
2279    // Phase 7b: Local client transport tests
2280    // =========================================================================
2281
2282    fn make_local_client_interface(id: u64) -> InterfaceInfo {
2283        InterfaceInfo {
2284            id: InterfaceId(id),
2285            name: String::from("local_client"),
2286            mode: constants::MODE_FULL,
2287            out_capable: true,
2288            in_capable: true,
2289            bitrate: None,
2290            announce_rate_target: None,
2291            announce_rate_grace: 0,
2292            announce_rate_penalty: 0.0,
2293            announce_cap: constants::ANNOUNCE_CAP,
2294            is_local_client: true,
2295            wants_tunnel: false,
2296            tunnel_id: None,
2297            mtu: constants::MTU as u32,
2298            ingress_control: false,
2299            ia_freq: 0.0,
2300            started: 0.0,
2301        }
2302    }
2303
2304    #[test]
2305    fn test_has_local_clients() {
2306        let mut engine = TransportEngine::new(make_config(false));
2307        assert!(!engine.has_local_clients());
2308
2309        engine.register_interface(make_interface(1, constants::MODE_FULL));
2310        assert!(!engine.has_local_clients());
2311
2312        engine.register_interface(make_local_client_interface(2));
2313        assert!(engine.has_local_clients());
2314
2315        engine.deregister_interface(InterfaceId(2));
2316        assert!(!engine.has_local_clients());
2317    }
2318
2319    #[test]
2320    fn test_local_client_hop_decrement() {
2321        // Packets from local clients should have their hops decremented
2322        // to cancel the standard +1 (net zero change)
2323        let mut engine = TransportEngine::new(make_config(false));
2324        engine.register_interface(make_local_client_interface(1));
2325        engine.register_interface(make_interface(2, constants::MODE_FULL));
2326
2327        // Register destination so we get a DeliverLocal action
2328        let dest = [0xAA; 16];
2329        engine.register_destination(dest, constants::DESTINATION_PLAIN);
2330
2331        let flags = PacketFlags {
2332            header_type: constants::HEADER_1,
2333            context_flag: constants::FLAG_UNSET,
2334            transport_type: constants::TRANSPORT_BROADCAST,
2335            destination_type: constants::DESTINATION_PLAIN,
2336            packet_type: constants::PACKET_TYPE_DATA,
2337        };
2338        // Pack with hops=0
2339        let packet =
2340            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
2341
2342        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2343        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2344
2345        // Should have local delivery; hops should still be 0 (not 1)
2346        // because the local client decrement cancels the increment
2347        let deliver = actions
2348            .iter()
2349            .find(|a| matches!(a, TransportAction::DeliverLocal { .. }));
2350        assert!(deliver.is_some(), "Should deliver locally");
2351    }
2352
2353    #[test]
2354    fn test_plain_broadcast_from_local_client() {
2355        // PLAIN broadcast from local client should forward to external interfaces
2356        let mut engine = TransportEngine::new(make_config(false));
2357        engine.register_interface(make_local_client_interface(1));
2358        engine.register_interface(make_interface(2, constants::MODE_FULL));
2359
2360        let dest = [0xBB; 16];
2361        let flags = PacketFlags {
2362            header_type: constants::HEADER_1,
2363            context_flag: constants::FLAG_UNSET,
2364            transport_type: constants::TRANSPORT_BROADCAST,
2365            destination_type: constants::DESTINATION_PLAIN,
2366            packet_type: constants::PACKET_TYPE_DATA,
2367        };
2368        let packet =
2369            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
2370
2371        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2372        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2373
2374        // Should have ForwardPlainBroadcast to external (to_local=false)
2375        let forward = actions.iter().find(|a| {
2376            matches!(
2377                a,
2378                TransportAction::ForwardPlainBroadcast {
2379                    to_local: false,
2380                    ..
2381                }
2382            )
2383        });
2384        assert!(forward.is_some(), "Should forward to external interfaces");
2385    }
2386
2387    #[test]
2388    fn test_plain_broadcast_from_external() {
2389        // PLAIN broadcast from external should forward to local clients
2390        let mut engine = TransportEngine::new(make_config(false));
2391        engine.register_interface(make_local_client_interface(1));
2392        engine.register_interface(make_interface(2, constants::MODE_FULL));
2393
2394        let dest = [0xCC; 16];
2395        let flags = PacketFlags {
2396            header_type: constants::HEADER_1,
2397            context_flag: constants::FLAG_UNSET,
2398            transport_type: constants::TRANSPORT_BROADCAST,
2399            destination_type: constants::DESTINATION_PLAIN,
2400            packet_type: constants::PACKET_TYPE_DATA,
2401        };
2402        let packet =
2403            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
2404
2405        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2406        let actions = engine.handle_inbound(&packet.raw, InterfaceId(2), 1000.0, &mut rng);
2407
2408        // Should have ForwardPlainBroadcast to local clients (to_local=true)
2409        let forward = actions.iter().find(|a| {
2410            matches!(
2411                a,
2412                TransportAction::ForwardPlainBroadcast { to_local: true, .. }
2413            )
2414        });
2415        assert!(forward.is_some(), "Should forward to local clients");
2416    }
2417
2418    #[test]
2419    fn test_no_plain_broadcast_bridging_without_local_clients() {
2420        // Without local clients, no bridging should happen
2421        let mut engine = TransportEngine::new(make_config(false));
2422        engine.register_interface(make_interface(1, constants::MODE_FULL));
2423        engine.register_interface(make_interface(2, constants::MODE_FULL));
2424
2425        let dest = [0xDD; 16];
2426        let flags = PacketFlags {
2427            header_type: constants::HEADER_1,
2428            context_flag: constants::FLAG_UNSET,
2429            transport_type: constants::TRANSPORT_BROADCAST,
2430            destination_type: constants::DESTINATION_PLAIN,
2431            packet_type: constants::PACKET_TYPE_DATA,
2432        };
2433        let packet =
2434            RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
2435
2436        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2437        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2438
2439        // No ForwardPlainBroadcast should be emitted
2440        let has_forward = actions
2441            .iter()
2442            .any(|a| matches!(a, TransportAction::ForwardPlainBroadcast { .. }));
2443        assert!(!has_forward, "No bridging without local clients");
2444    }
2445
2446    #[test]
2447    fn test_announce_forwarded_to_local_clients() {
2448        use crate::announce::AnnounceData;
2449        use crate::destination::{destination_hash, name_hash};
2450
2451        let mut engine = TransportEngine::new(make_config(false));
2452        engine.register_interface(make_interface(1, constants::MODE_FULL));
2453        engine.register_interface(make_local_client_interface(2));
2454
2455        let identity =
2456            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x77; 32]));
2457        let dest_hash = destination_hash("test", &["fwd"], Some(identity.hash()));
2458        let name_h = name_hash("test", &["fwd"]);
2459        let random_hash = [0x42u8; 10];
2460
2461        let (announce_data, _) =
2462            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
2463
2464        let flags = PacketFlags {
2465            header_type: constants::HEADER_1,
2466            context_flag: constants::FLAG_UNSET,
2467            transport_type: constants::TRANSPORT_BROADCAST,
2468            destination_type: constants::DESTINATION_SINGLE,
2469            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2470        };
2471        let packet = RawPacket::pack(
2472            flags,
2473            0,
2474            &dest_hash,
2475            None,
2476            constants::CONTEXT_NONE,
2477            &announce_data,
2478        )
2479        .unwrap();
2480
2481        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
2482        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2483
2484        // Should have ForwardToLocalClients since we have local clients
2485        let forward = actions
2486            .iter()
2487            .find(|a| matches!(a, TransportAction::ForwardToLocalClients { .. }));
2488        assert!(
2489            forward.is_some(),
2490            "Should forward announce to local clients"
2491        );
2492
2493        // The exclude should be the receiving interface
2494        match forward.unwrap() {
2495            TransportAction::ForwardToLocalClients { exclude, .. } => {
2496                assert_eq!(*exclude, Some(InterfaceId(1)));
2497            }
2498            _ => unreachable!(),
2499        }
2500    }
2501
2502    #[test]
2503    fn test_no_announce_forward_without_local_clients() {
2504        use crate::announce::AnnounceData;
2505        use crate::destination::{destination_hash, name_hash};
2506
2507        let mut engine = TransportEngine::new(make_config(false));
2508        engine.register_interface(make_interface(1, constants::MODE_FULL));
2509
2510        let identity =
2511            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x88; 32]));
2512        let dest_hash = destination_hash("test", &["nofwd"], Some(identity.hash()));
2513        let name_h = name_hash("test", &["nofwd"]);
2514        let random_hash = [0x42u8; 10];
2515
2516        let (announce_data, _) =
2517            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
2518
2519        let flags = PacketFlags {
2520            header_type: constants::HEADER_1,
2521            context_flag: constants::FLAG_UNSET,
2522            transport_type: constants::TRANSPORT_BROADCAST,
2523            destination_type: constants::DESTINATION_SINGLE,
2524            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2525        };
2526        let packet = RawPacket::pack(
2527            flags,
2528            0,
2529            &dest_hash,
2530            None,
2531            constants::CONTEXT_NONE,
2532            &announce_data,
2533        )
2534        .unwrap();
2535
2536        let mut rng = rns_crypto::FixedRng::new(&[0x22; 32]);
2537        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2538
2539        // No ForwardToLocalClients should be emitted
2540        let has_forward = actions
2541            .iter()
2542            .any(|a| matches!(a, TransportAction::ForwardToLocalClients { .. }));
2543        assert!(!has_forward, "No forward without local clients");
2544    }
2545
2546    #[test]
2547    fn test_local_client_exclude_from_forward() {
2548        use crate::announce::AnnounceData;
2549        use crate::destination::{destination_hash, name_hash};
2550
2551        let mut engine = TransportEngine::new(make_config(false));
2552        engine.register_interface(make_local_client_interface(1));
2553        engine.register_interface(make_local_client_interface(2));
2554
2555        let identity =
2556            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
2557        let dest_hash = destination_hash("test", &["excl"], Some(identity.hash()));
2558        let name_h = name_hash("test", &["excl"]);
2559        let random_hash = [0x42u8; 10];
2560
2561        let (announce_data, _) =
2562            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
2563
2564        let flags = PacketFlags {
2565            header_type: constants::HEADER_1,
2566            context_flag: constants::FLAG_UNSET,
2567            transport_type: constants::TRANSPORT_BROADCAST,
2568            destination_type: constants::DESTINATION_SINGLE,
2569            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2570        };
2571        let packet = RawPacket::pack(
2572            flags,
2573            0,
2574            &dest_hash,
2575            None,
2576            constants::CONTEXT_NONE,
2577            &announce_data,
2578        )
2579        .unwrap();
2580
2581        let mut rng = rns_crypto::FixedRng::new(&[0x33; 32]);
2582        // Feed announce from local client 1
2583        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2584
2585        // Should forward to local clients, excluding interface 1 (the sender)
2586        let forward = actions
2587            .iter()
2588            .find(|a| matches!(a, TransportAction::ForwardToLocalClients { .. }));
2589        assert!(forward.is_some());
2590        match forward.unwrap() {
2591            TransportAction::ForwardToLocalClients { exclude, .. } => {
2592                assert_eq!(*exclude, Some(InterfaceId(1)));
2593            }
2594            _ => unreachable!(),
2595        }
2596    }
2597
2598    // =========================================================================
2599    // Phase 7d: Tunnel tests
2600    // =========================================================================
2601
2602    fn make_tunnel_interface(id: u64) -> InterfaceInfo {
2603        InterfaceInfo {
2604            id: InterfaceId(id),
2605            name: String::from("tunnel_iface"),
2606            mode: constants::MODE_FULL,
2607            out_capable: true,
2608            in_capable: true,
2609            bitrate: None,
2610            announce_rate_target: None,
2611            announce_rate_grace: 0,
2612            announce_rate_penalty: 0.0,
2613            announce_cap: constants::ANNOUNCE_CAP,
2614            is_local_client: false,
2615            wants_tunnel: true,
2616            tunnel_id: None,
2617            mtu: constants::MTU as u32,
2618            ingress_control: false,
2619            ia_freq: 0.0,
2620            started: 0.0,
2621        }
2622    }
2623
2624    #[test]
2625    fn test_handle_tunnel_new() {
2626        let mut engine = TransportEngine::new(make_config(true));
2627        engine.register_interface(make_tunnel_interface(1));
2628
2629        let tunnel_id = [0xAA; 32];
2630        let actions = engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2631
2632        // Should emit TunnelEstablished
2633        assert!(actions
2634            .iter()
2635            .any(|a| matches!(a, TransportAction::TunnelEstablished { .. })));
2636
2637        // Interface should now have tunnel_id set
2638        let info = engine.interface_info(&InterfaceId(1)).unwrap();
2639        assert_eq!(info.tunnel_id, Some(tunnel_id));
2640
2641        // Tunnel table should have the entry
2642        assert_eq!(engine.tunnel_table().len(), 1);
2643    }
2644
2645    #[test]
2646    fn test_announce_stores_tunnel_path() {
2647        use crate::announce::AnnounceData;
2648        use crate::destination::{destination_hash, name_hash};
2649
2650        let mut engine = TransportEngine::new(make_config(false));
2651        let mut iface = make_tunnel_interface(1);
2652        let tunnel_id = [0xBB; 32];
2653        iface.tunnel_id = Some(tunnel_id);
2654        engine.register_interface(iface);
2655
2656        // Create tunnel entry
2657        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2658
2659        // Create and send an announce
2660        let identity =
2661            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xCC; 32]));
2662        let dest_hash = destination_hash("test", &["tunnel"], Some(identity.hash()));
2663        let name_h = name_hash("test", &["tunnel"]);
2664        let random_hash = [0x42u8; 10];
2665
2666        let (announce_data, _) =
2667            AnnounceData::pack(&identity, &dest_hash, &name_h, &random_hash, None, None).unwrap();
2668
2669        let flags = PacketFlags {
2670            header_type: constants::HEADER_1,
2671            context_flag: constants::FLAG_UNSET,
2672            transport_type: constants::TRANSPORT_BROADCAST,
2673            destination_type: constants::DESTINATION_SINGLE,
2674            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2675        };
2676        let packet = RawPacket::pack(
2677            flags,
2678            0,
2679            &dest_hash,
2680            None,
2681            constants::CONTEXT_NONE,
2682            &announce_data,
2683        )
2684        .unwrap();
2685
2686        let mut rng = rns_crypto::FixedRng::new(&[0xDD; 32]);
2687        engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2688
2689        // Path should be in path table
2690        assert!(engine.has_path(&dest_hash));
2691
2692        // Path should also be in tunnel table
2693        let tunnel = engine.tunnel_table().get(&tunnel_id).unwrap();
2694        assert_eq!(tunnel.paths.len(), 1);
2695        assert!(tunnel.paths.contains_key(&dest_hash));
2696    }
2697
2698    #[test]
2699    fn test_tunnel_reattach_restores_paths() {
2700        let mut engine = TransportEngine::new(make_config(true));
2701        engine.register_interface(make_tunnel_interface(1));
2702
2703        let tunnel_id = [0xCC; 32];
2704        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2705
2706        // Manually add a path to the tunnel
2707        let dest = [0xDD; 16];
2708        engine.tunnel_table.store_tunnel_path(
2709            &tunnel_id,
2710            dest,
2711            tunnel::TunnelPath {
2712                timestamp: 1000.0,
2713                received_from: [0xEE; 16],
2714                hops: 3,
2715                expires: 1000.0 + constants::DESTINATION_TIMEOUT,
2716                random_blobs: Vec::new(),
2717                packet_hash: [0xFF; 32],
2718            },
2719            1000.0,
2720        );
2721
2722        // Void the tunnel interface (disconnect)
2723        engine.void_tunnel_interface(&tunnel_id);
2724
2725        // Remove path from path table to simulate it expiring
2726        engine.path_table.remove(&dest);
2727        assert!(!engine.has_path(&dest));
2728
2729        // Reattach tunnel on new interface
2730        engine.register_interface(make_interface(2, constants::MODE_FULL));
2731        let actions = engine.handle_tunnel(tunnel_id, InterfaceId(2), 2000.0);
2732
2733        // Should restore the path
2734        assert!(engine.has_path(&dest));
2735        let path = engine.path_table.get(&dest).unwrap().primary().unwrap();
2736        assert_eq!(path.hops, 3);
2737        assert_eq!(path.receiving_interface, InterfaceId(2));
2738
2739        // Should emit TunnelEstablished
2740        assert!(actions
2741            .iter()
2742            .any(|a| matches!(a, TransportAction::TunnelEstablished { .. })));
2743    }
2744
2745    #[test]
2746    fn test_void_tunnel_interface() {
2747        let mut engine = TransportEngine::new(make_config(true));
2748        engine.register_interface(make_tunnel_interface(1));
2749
2750        let tunnel_id = [0xDD; 32];
2751        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2752
2753        // Verify tunnel has interface
2754        assert_eq!(
2755            engine.tunnel_table().get(&tunnel_id).unwrap().interface,
2756            Some(InterfaceId(1))
2757        );
2758
2759        engine.void_tunnel_interface(&tunnel_id);
2760
2761        // Interface voided, but tunnel still exists
2762        assert_eq!(engine.tunnel_table().len(), 1);
2763        assert_eq!(
2764            engine.tunnel_table().get(&tunnel_id).unwrap().interface,
2765            None
2766        );
2767    }
2768
2769    #[test]
2770    fn test_tick_culls_tunnels() {
2771        let mut engine = TransportEngine::new(make_config(true));
2772        engine.register_interface(make_tunnel_interface(1));
2773
2774        let tunnel_id = [0xEE; 32];
2775        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2776        assert_eq!(engine.tunnel_table().len(), 1);
2777
2778        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2779
2780        // Tick past DESTINATION_TIMEOUT + TABLES_CULL_INTERVAL
2781        engine.tick(
2782            1000.0 + constants::DESTINATION_TIMEOUT + constants::TABLES_CULL_INTERVAL + 1.0,
2783            &mut rng,
2784        );
2785
2786        assert_eq!(engine.tunnel_table().len(), 0);
2787    }
2788
2789    #[test]
2790    fn test_synthesize_tunnel() {
2791        let mut engine = TransportEngine::new(make_config(true));
2792        engine.register_interface(make_tunnel_interface(1));
2793
2794        let identity =
2795            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xFF; 32]));
2796        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
2797
2798        let actions = engine.synthesize_tunnel(&identity, InterfaceId(1), &mut rng);
2799
2800        // Should produce a TunnelSynthesize action
2801        assert_eq!(actions.len(), 1);
2802        match &actions[0] {
2803            TransportAction::TunnelSynthesize {
2804                interface,
2805                data,
2806                dest_hash,
2807            } => {
2808                assert_eq!(*interface, InterfaceId(1));
2809                assert_eq!(data.len(), tunnel::TUNNEL_SYNTH_LENGTH);
2810                // dest_hash should be the tunnel.synthesize plain destination
2811                let expected_dest = crate::destination::destination_hash(
2812                    "rnstransport",
2813                    &["tunnel", "synthesize"],
2814                    None,
2815                );
2816                assert_eq!(*dest_hash, expected_dest);
2817            }
2818            _ => panic!("Expected TunnelSynthesize"),
2819        }
2820    }
2821
2822    // =========================================================================
2823    // DISCOVER_PATHS_FOR tests
2824    // =========================================================================
2825
2826    fn make_path_request_data(dest_hash: &[u8; 16], tag: &[u8]) -> Vec<u8> {
2827        let mut data = Vec::new();
2828        data.extend_from_slice(dest_hash);
2829        data.extend_from_slice(tag);
2830        data
2831    }
2832
2833    #[test]
2834    fn test_path_request_forwarded_on_ap() {
2835        let mut engine = TransportEngine::new(make_config(true));
2836        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
2837        engine.register_interface(make_interface(2, constants::MODE_FULL));
2838
2839        let dest = [0xD1; 16];
2840        let tag = [0x01; 16];
2841        let data = make_path_request_data(&dest, &tag);
2842
2843        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2844
2845        // Should forward the path request on interface 2 (the other OUT interface)
2846        assert_eq!(actions.len(), 1);
2847        match &actions[0] {
2848            TransportAction::SendOnInterface { interface, .. } => {
2849                assert_eq!(*interface, InterfaceId(2));
2850            }
2851            _ => panic!("Expected SendOnInterface for forwarded path request"),
2852        }
2853        // Should have stored a discovery path request
2854        assert!(engine.discovery_path_requests.contains_key(&dest));
2855    }
2856
2857    #[test]
2858    fn test_path_request_not_forwarded_on_full() {
2859        let mut engine = TransportEngine::new(make_config(true));
2860        engine.register_interface(make_interface(1, constants::MODE_FULL));
2861        engine.register_interface(make_interface(2, constants::MODE_FULL));
2862
2863        let dest = [0xD2; 16];
2864        let tag = [0x02; 16];
2865        let data = make_path_request_data(&dest, &tag);
2866
2867        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2868
2869        // MODE_FULL is not in DISCOVER_PATHS_FOR, so no forwarding
2870        assert!(actions.is_empty());
2871        assert!(!engine.discovery_path_requests.contains_key(&dest));
2872    }
2873
2874    #[test]
2875    fn test_roaming_loop_prevention() {
2876        let mut engine = TransportEngine::new(make_config(true));
2877        engine.register_interface(make_interface(1, constants::MODE_ROAMING));
2878
2879        let dest = [0xD3; 16];
2880        // Path is known and routes through the same interface (1)
2881        engine.path_table.insert(
2882            dest,
2883            PathSet::from_single(
2884                PathEntry {
2885                    timestamp: 900.0,
2886                    next_hop: [0xAA; 16],
2887                    hops: 2,
2888                    expires: 9999.0,
2889                    random_blobs: Vec::new(),
2890                    receiving_interface: InterfaceId(1),
2891                    packet_hash: [0; 32],
2892                    announce_raw: None,
2893                },
2894                1,
2895            ),
2896        );
2897
2898        let tag = [0x03; 16];
2899        let data = make_path_request_data(&dest, &tag);
2900
2901        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2902
2903        // ROAMING interface, path next-hop on same interface → loop prevention, no action
2904        assert!(actions.is_empty());
2905        assert!(!engine.announce_table.contains_key(&dest));
2906    }
2907
2908    /// Build a minimal HEADER_1 announce raw packet for testing.
2909    fn make_announce_raw(dest_hash: &[u8; 16], payload: &[u8]) -> Vec<u8> {
2910        // HEADER_1: [flags:1][hops:1][dest:16][context:1][data:*]
2911        // flags: HEADER_1(0) << 6 | context_flag(0) << 5 | TRANSPORT_BROADCAST(0) << 4 | SINGLE(0) << 2 | ANNOUNCE(1)
2912        let flags: u8 = 0x01; // HEADER_1, no context, broadcast, single, announce
2913        let mut raw = Vec::new();
2914        raw.push(flags);
2915        raw.push(0x02); // hops
2916        raw.extend_from_slice(dest_hash);
2917        raw.push(constants::CONTEXT_NONE);
2918        raw.extend_from_slice(payload);
2919        raw
2920    }
2921
2922    #[test]
2923    fn test_path_request_populates_announce_entry_from_raw() {
2924        let mut engine = TransportEngine::new(make_config(true));
2925        engine.register_interface(make_interface(1, constants::MODE_FULL));
2926        engine.register_interface(make_interface(2, constants::MODE_FULL));
2927
2928        let dest = [0xD5; 16];
2929        let payload = vec![0xAB; 32]; // simulated announce data (pubkey, sig, etc.)
2930        let announce_raw = make_announce_raw(&dest, &payload);
2931
2932        engine.path_table.insert(
2933            dest,
2934            PathSet::from_single(
2935                PathEntry {
2936                    timestamp: 900.0,
2937                    next_hop: [0xBB; 16],
2938                    hops: 2,
2939                    expires: 9999.0,
2940                    random_blobs: Vec::new(),
2941                    receiving_interface: InterfaceId(2),
2942                    packet_hash: [0; 32],
2943                    announce_raw: Some(announce_raw.clone()),
2944                },
2945                1,
2946            ),
2947        );
2948
2949        let tag = [0x05; 16];
2950        let data = make_path_request_data(&dest, &tag);
2951        let _actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2952
2953        // The announce table should now have an entry with populated packet_raw/packet_data
2954        let entry = engine
2955            .announce_table
2956            .get(&dest)
2957            .expect("announce entry must exist");
2958        assert_eq!(entry.packet_raw, announce_raw);
2959        assert_eq!(entry.packet_data, payload);
2960        assert!(entry.block_rebroadcasts);
2961    }
2962
2963    #[test]
2964    fn test_path_request_skips_when_no_announce_raw() {
2965        let mut engine = TransportEngine::new(make_config(true));
2966        engine.register_interface(make_interface(1, constants::MODE_FULL));
2967        engine.register_interface(make_interface(2, constants::MODE_FULL));
2968
2969        let dest = [0xD6; 16];
2970
2971        engine.path_table.insert(
2972            dest,
2973            PathSet::from_single(
2974                PathEntry {
2975                    timestamp: 900.0,
2976                    next_hop: [0xCC; 16],
2977                    hops: 1,
2978                    expires: 9999.0,
2979                    random_blobs: Vec::new(),
2980                    receiving_interface: InterfaceId(2),
2981                    packet_hash: [0; 32],
2982                    announce_raw: None, // no raw data available
2983                },
2984                1,
2985            ),
2986        );
2987
2988        let tag = [0x06; 16];
2989        let data = make_path_request_data(&dest, &tag);
2990        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2991
2992        // Should NOT create an announce entry without raw data
2993        assert!(actions.is_empty());
2994        assert!(!engine.announce_table.contains_key(&dest));
2995    }
2996
2997    #[test]
2998    fn test_discovery_request_consumed_on_announce() {
2999        let mut engine = TransportEngine::new(make_config(true));
3000        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
3001
3002        let dest = [0xD4; 16];
3003
3004        // Simulate a waiting discovery request
3005        engine.discovery_path_requests.insert(
3006            dest,
3007            DiscoveryPathRequest {
3008                timestamp: 900.0,
3009                requesting_interface: InterfaceId(1),
3010            },
3011        );
3012
3013        // Consume it
3014        let iface = engine.discovery_path_requests_waiting(&dest);
3015        assert_eq!(iface, Some(InterfaceId(1)));
3016
3017        // Should be gone now
3018        assert!(!engine.discovery_path_requests.contains_key(&dest));
3019        assert_eq!(engine.discovery_path_requests_waiting(&dest), None);
3020    }
3021
3022    // =========================================================================
3023    // Issue #4: Shared instance client 1-hop transport injection
3024    // =========================================================================
3025
3026    /// Helper: build a valid announce packet for use in issue #4 tests.
3027    fn build_announce_for_issue4(dest_hash: &[u8; 16], name_hash: &[u8; 10]) -> Vec<u8> {
3028        let identity =
3029            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
3030        let random_hash = [0x42u8; 10];
3031        let (announce_data, _) = crate::announce::AnnounceData::pack(
3032            &identity,
3033            dest_hash,
3034            name_hash,
3035            &random_hash,
3036            None,
3037            None,
3038        )
3039        .unwrap();
3040        let flags = PacketFlags {
3041            header_type: constants::HEADER_1,
3042            context_flag: constants::FLAG_UNSET,
3043            transport_type: constants::TRANSPORT_BROADCAST,
3044            destination_type: constants::DESTINATION_SINGLE,
3045            packet_type: constants::PACKET_TYPE_ANNOUNCE,
3046        };
3047        RawPacket::pack(flags, 0, dest_hash, None, constants::CONTEXT_NONE, &announce_data)
3048            .unwrap()
3049            .raw
3050    }
3051
3052    #[test]
3053    fn test_issue4_local_client_single_data_to_1hop_rewrites_on_outbound() {
3054        // Shared clients learn remote paths via their local shared-instance
3055        // interface and must inject transport headers on outbound when the
3056        // destination is exactly 1 hop away behind the daemon.
3057
3058        let mut engine = TransportEngine::new(make_config(false));
3059        engine.register_interface(make_local_client_interface(1));
3060
3061        let identity =
3062            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
3063        let dest_hash = crate::destination::destination_hash(
3064            "issue4",
3065            &["test"],
3066            Some(identity.hash()),
3067        );
3068        let name_hash = crate::destination::name_hash("issue4", &["test"]);
3069        let announce_raw = build_announce_for_issue4(&dest_hash, &name_hash);
3070
3071        // Model the announce as already forwarded by the shared daemon to
3072        // the local client. The raw hop count is 1 so that after the local
3073        // client hop compensation the learned path remains 1 hop away.
3074        let mut announce_packet = RawPacket::unpack(&announce_raw).unwrap();
3075        announce_packet.raw[1] = 1;
3076        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3077        engine.handle_inbound(&announce_packet.raw, InterfaceId(1), 1000.0, &mut rng);
3078        assert!(engine.has_path(&dest_hash));
3079        assert_eq!(engine.hops_to(&dest_hash), Some(1));
3080
3081        // Build DATA from the shared client to the 1-hop destination.
3082        let data_flags = PacketFlags {
3083            header_type: constants::HEADER_1,
3084            context_flag: constants::FLAG_UNSET,
3085            transport_type: constants::TRANSPORT_BROADCAST,
3086            destination_type: constants::DESTINATION_SINGLE,
3087            packet_type: constants::PACKET_TYPE_DATA,
3088        };
3089        let data_packet = RawPacket::pack(
3090            data_flags,
3091            0,
3092            &dest_hash,
3093            None,
3094            constants::CONTEXT_NONE,
3095            b"hello",
3096        )
3097        .unwrap();
3098
3099        let actions = engine.handle_outbound(
3100            &data_packet,
3101            constants::DESTINATION_SINGLE,
3102            None,
3103            1001.0,
3104        );
3105
3106        let send = actions.iter().find_map(|a| match a {
3107            TransportAction::SendOnInterface { interface, raw } => Some((interface, raw)),
3108            _ => None,
3109        });
3110        let (interface, raw) = send.expect("shared client should emit a transport-injected packet");
3111        assert_eq!(*interface, InterfaceId(1));
3112        let flags = PacketFlags::unpack(raw[0]);
3113        assert_eq!(flags.header_type, constants::HEADER_2);
3114        assert_eq!(flags.transport_type, constants::TRANSPORT_TRANSPORT);
3115    }
3116
3117    #[test]
3118    fn test_issue4_external_data_to_1hop_via_transport_works() {
3119        // Control test: when a DATA packet arrives from an external interface
3120        // with HEADER_2 and the daemon's transport_id, the daemon correctly
3121        // forwards it via step 5.  This proves the multi-hop path works;
3122        // it's only the 1-hop shared-client case that's broken.
3123
3124        let daemon_id = [0x42; 16];
3125        let mut engine = TransportEngine::new(TransportConfig {
3126            transport_enabled: true,
3127            identity_hash: Some(daemon_id),
3128            prefer_shorter_path: false,
3129            max_paths_per_destination: 1,
3130            packet_hashlist_max_entries: constants::HASHLIST_MAXSIZE,
3131        });
3132        engine.register_interface(make_interface(1, constants::MODE_FULL)); // inbound
3133        engine.register_interface(make_interface(2, constants::MODE_FULL)); // outbound to Bob
3134
3135        let identity =
3136            rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
3137        let dest_hash = crate::destination::destination_hash(
3138            "issue4",
3139            &["ctrl"],
3140            Some(identity.hash()),
3141        );
3142        let name_hash = crate::destination::name_hash("issue4", &["ctrl"]);
3143        let announce_raw = build_announce_for_issue4(&dest_hash, &name_hash);
3144
3145        // Feed announce from interface 2 (Bob's side), hops=0 → stored as hops=1
3146        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
3147        engine.handle_inbound(&announce_raw, InterfaceId(2), 1000.0, &mut rng);
3148        assert_eq!(engine.hops_to(&dest_hash), Some(1));
3149
3150        // Now send a HEADER_2 transport packet addressed to the daemon
3151        // (simulating what Alice would send in a multi-hop scenario)
3152        let h2_flags = PacketFlags {
3153            header_type: constants::HEADER_2,
3154            context_flag: constants::FLAG_UNSET,
3155            transport_type: constants::TRANSPORT_TRANSPORT,
3156            destination_type: constants::DESTINATION_SINGLE,
3157            packet_type: constants::PACKET_TYPE_DATA,
3158        };
3159        // Build HEADER_2 manually: [flags, hops, transport_id(16), dest_hash(16), context, data...]
3160        let mut h2_raw = Vec::new();
3161        h2_raw.push(h2_flags.pack());
3162        h2_raw.push(0); // hops
3163        h2_raw.extend_from_slice(&daemon_id); // transport_id = daemon
3164        h2_raw.extend_from_slice(&dest_hash);
3165        h2_raw.push(constants::CONTEXT_NONE);
3166        h2_raw.extend_from_slice(b"hello via transport");
3167
3168        let mut rng2 = rns_crypto::FixedRng::new(&[0x22; 32]);
3169        let actions = engine.handle_inbound(&h2_raw, InterfaceId(1), 1001.0, &mut rng2);
3170
3171        // This SHOULD forward via step 5 (transport forwarding)
3172        let has_send = actions.iter().any(|a| {
3173            matches!(
3174                a,
3175                TransportAction::SendOnInterface { interface, .. } if *interface == InterfaceId(2)
3176            )
3177        });
3178        assert!(
3179            has_send,
3180            "HEADER_2 transport packet should be forwarded (control test)"
3181        );
3182    }
3183}