Skip to main content

rns_core/transport/
mod.rs

1pub mod types;
2pub mod tables;
3pub mod dedup;
4pub mod pathfinder;
5pub mod rate_limit;
6pub mod announce_proc;
7pub mod outbound;
8pub mod inbound;
9pub mod announce_queue;
10pub mod tunnel;
11pub mod ingress_control;
12pub mod jobs;
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::tunnel::TunnelTable;
29use self::inbound::{
30    create_link_entry, create_reverse_entry, forward_transport_packet,
31    route_proof_via_reverse, route_via_link_table,
32};
33use self::outbound::{route_outbound, should_transmit_announce};
34use self::pathfinder::{
35    extract_random_blob, should_update_path, timebase_from_random_blob, PathDecision,
36};
37use self::ingress_control::IngressControl;
38use self::rate_limit::AnnounceRateLimiter;
39use self::tables::{AnnounceEntry, DiscoveryPathRequest, LinkEntry, PathEntry};
40use self::types::{BlackholeEntry, InterfaceId, InterfaceInfo, TransportAction, TransportConfig};
41
42/// The core transport/routing engine.
43///
44/// Maintains routing tables and processes packets without performing any I/O.
45/// Returns `Vec<TransportAction>` that the caller must execute.
46pub struct TransportEngine {
47    config: TransportConfig,
48    path_table: BTreeMap<[u8; 16], PathEntry>,
49    announce_table: BTreeMap<[u8; 16], AnnounceEntry>,
50    reverse_table: BTreeMap<[u8; 16], tables::ReverseEntry>,
51    link_table: BTreeMap<[u8; 16], LinkEntry>,
52    held_announces: BTreeMap<[u8; 16], AnnounceEntry>,
53    packet_hashlist: PacketHashlist,
54    rate_limiter: AnnounceRateLimiter,
55    path_states: BTreeMap<[u8; 16], u8>,
56    interfaces: BTreeMap<InterfaceId, InterfaceInfo>,
57    local_destinations: BTreeMap<[u8; 16], u8>,
58    blackholed_identities: BTreeMap<[u8; 16], BlackholeEntry>,
59    announce_queues: AnnounceQueues,
60    ingress_control: IngressControl,
61    tunnel_table: TunnelTable,
62    discovery_pr_tags: Vec<[u8; 32]>,
63    discovery_path_requests: BTreeMap<[u8; 16], DiscoveryPathRequest>,
64    // Job timing
65    announces_last_checked: f64,
66    tables_last_culled: f64,
67}
68
69impl TransportEngine {
70    pub fn new(config: TransportConfig) -> Self {
71        TransportEngine {
72            config,
73            path_table: BTreeMap::new(),
74            announce_table: BTreeMap::new(),
75            reverse_table: BTreeMap::new(),
76            link_table: BTreeMap::new(),
77            held_announces: BTreeMap::new(),
78            packet_hashlist: PacketHashlist::new(constants::HASHLIST_MAXSIZE),
79            rate_limiter: AnnounceRateLimiter::new(),
80            path_states: BTreeMap::new(),
81            interfaces: BTreeMap::new(),
82            local_destinations: BTreeMap::new(),
83            blackholed_identities: BTreeMap::new(),
84            announce_queues: AnnounceQueues::new(),
85            ingress_control: IngressControl::new(),
86            tunnel_table: TunnelTable::new(),
87            discovery_pr_tags: Vec::new(),
88            discovery_path_requests: BTreeMap::new(),
89            announces_last_checked: 0.0,
90            tables_last_culled: 0.0,
91        }
92    }
93
94    // =========================================================================
95    // Interface management
96    // =========================================================================
97
98    pub fn register_interface(&mut self, info: InterfaceInfo) {
99        self.interfaces.insert(info.id, info);
100    }
101
102    pub fn deregister_interface(&mut self, id: InterfaceId) {
103        self.interfaces.remove(&id);
104        self.ingress_control.remove_interface(&id);
105    }
106
107    // =========================================================================
108    // Destination management
109    // =========================================================================
110
111    pub fn register_destination(&mut self, dest_hash: [u8; 16], dest_type: u8) {
112        self.local_destinations.insert(dest_hash, dest_type);
113    }
114
115    pub fn deregister_destination(&mut self, dest_hash: &[u8; 16]) {
116        self.local_destinations.remove(dest_hash);
117    }
118
119    // =========================================================================
120    // Path queries
121    // =========================================================================
122
123    pub fn has_path(&self, dest_hash: &[u8; 16]) -> bool {
124        self.path_table.contains_key(dest_hash)
125    }
126
127    pub fn hops_to(&self, dest_hash: &[u8; 16]) -> Option<u8> {
128        self.path_table.get(dest_hash).map(|e| e.hops)
129    }
130
131    pub fn next_hop(&self, dest_hash: &[u8; 16]) -> Option<[u8; 16]> {
132        self.path_table.get(dest_hash).map(|e| e.next_hop)
133    }
134
135    pub fn next_hop_interface(&self, dest_hash: &[u8; 16]) -> Option<InterfaceId> {
136        self.path_table.get(dest_hash).map(|e| e.receiving_interface)
137    }
138
139    // =========================================================================
140    // Path state
141    // =========================================================================
142
143    /// Mark a path as unresponsive.
144    ///
145    /// If `receiving_interface` is provided and points to a MODE_BOUNDARY interface,
146    /// the marking is skipped — boundary interfaces must not poison path tables.
147    /// (Python Transport.py: mark_path_unknown/unresponsive boundary exemption)
148    pub fn mark_path_unresponsive(
149        &mut self,
150        dest_hash: &[u8; 16],
151        receiving_interface: Option<InterfaceId>,
152    ) {
153        if let Some(iface_id) = receiving_interface {
154            if let Some(info) = self.interfaces.get(&iface_id) {
155                if info.mode == constants::MODE_BOUNDARY {
156                    return;
157                }
158            }
159        }
160        self.path_states
161            .insert(*dest_hash, constants::STATE_UNRESPONSIVE);
162    }
163
164    pub fn mark_path_responsive(&mut self, dest_hash: &[u8; 16]) {
165        self.path_states
166            .insert(*dest_hash, constants::STATE_RESPONSIVE);
167    }
168
169    pub fn path_is_unresponsive(&self, dest_hash: &[u8; 16]) -> bool {
170        self.path_states.get(dest_hash) == Some(&constants::STATE_UNRESPONSIVE)
171    }
172
173    pub fn expire_path(&mut self, dest_hash: &[u8; 16]) {
174        if let Some(entry) = self.path_table.get_mut(dest_hash) {
175            entry.timestamp = 0.0;
176            entry.expires = 0.0;
177        }
178    }
179
180    // =========================================================================
181    // Link table
182    // =========================================================================
183
184    pub fn register_link(&mut self, link_id: [u8; 16], entry: LinkEntry) {
185        self.link_table.insert(link_id, entry);
186    }
187
188    pub fn validate_link(&mut self, link_id: &[u8; 16]) {
189        if let Some(entry) = self.link_table.get_mut(link_id) {
190            entry.validated = true;
191        }
192    }
193
194    pub fn remove_link(&mut self, link_id: &[u8; 16]) {
195        self.link_table.remove(link_id);
196    }
197
198    // =========================================================================
199    // Blackhole management
200    // =========================================================================
201
202    /// Add an identity hash to the blackhole list.
203    pub fn blackhole_identity(
204        &mut self,
205        identity_hash: [u8; 16],
206        now: f64,
207        duration_hours: Option<f64>,
208        reason: Option<String>,
209    ) {
210        let expires = match duration_hours {
211            Some(h) if h > 0.0 => now + h * 3600.0,
212            _ => 0.0, // never expires
213        };
214        self.blackholed_identities.insert(
215            identity_hash,
216            BlackholeEntry {
217                created: now,
218                expires,
219                reason,
220            },
221        );
222    }
223
224    /// Remove an identity hash from the blackhole list.
225    pub fn unblackhole_identity(&mut self, identity_hash: &[u8; 16]) -> bool {
226        self.blackholed_identities.remove(identity_hash).is_some()
227    }
228
229    /// Check if an identity hash is blackholed (and not expired).
230    pub fn is_blackholed(&self, identity_hash: &[u8; 16], now: f64) -> bool {
231        if let Some(entry) = self.blackholed_identities.get(identity_hash) {
232            if entry.expires == 0.0 || entry.expires > now {
233                return true;
234            }
235        }
236        false
237    }
238
239    /// Get all blackhole entries (for queries).
240    pub fn blackholed_entries(&self) -> impl Iterator<Item = (&[u8; 16], &BlackholeEntry)> {
241        self.blackholed_identities.iter()
242    }
243
244    /// Cull expired blackhole entries.
245    fn cull_blackholed(&mut self, now: f64) {
246        self.blackholed_identities.retain(|_, entry| {
247            entry.expires == 0.0 || entry.expires > now
248        });
249    }
250
251    // =========================================================================
252    // Tunnel management
253    // =========================================================================
254
255    /// Handle a validated tunnel synthesis — create new or reattach.
256    ///
257    /// Returns actions for any restored paths.
258    pub fn handle_tunnel(
259        &mut self,
260        tunnel_id: [u8; 32],
261        interface: InterfaceId,
262        now: f64,
263    ) -> Vec<TransportAction> {
264        let mut actions = Vec::new();
265
266        // Set tunnel_id on the interface
267        if let Some(info) = self.interfaces.get_mut(&interface) {
268            info.tunnel_id = Some(tunnel_id);
269        }
270
271        let restored_paths = self.tunnel_table.handle_tunnel(tunnel_id, interface, now);
272
273        // Restore paths to path table if they're better than existing
274        for (dest_hash, tunnel_path) in &restored_paths {
275            let should_restore = match self.path_table.get(dest_hash) {
276                Some(existing) => {
277                    // Restore if fewer hops or existing expired
278                    tunnel_path.hops <= existing.hops || existing.expires < now
279                }
280                None => true,
281            };
282
283            if should_restore {
284                self.path_table.insert(
285                    *dest_hash,
286                    PathEntry {
287                        timestamp: tunnel_path.timestamp,
288                        next_hop: tunnel_path.received_from,
289                        hops: tunnel_path.hops,
290                        expires: tunnel_path.expires,
291                        random_blobs: tunnel_path.random_blobs.clone(),
292                        receiving_interface: interface,
293                        packet_hash: tunnel_path.packet_hash,
294                        announce_raw: None,
295                    },
296                );
297            }
298        }
299
300        actions.push(TransportAction::TunnelEstablished {
301            tunnel_id,
302            interface,
303        });
304
305        actions
306    }
307
308    /// Synthesize a tunnel on an interface.
309    ///
310    /// `identity`: the transport identity (must have private key for signing)
311    /// `interface_id`: which interface to send the synthesis on
312    /// `rng`: random number generator
313    ///
314    /// Returns TunnelSynthesize action to send the synthesis packet.
315    pub fn synthesize_tunnel(
316        &self,
317        identity: &rns_crypto::identity::Identity,
318        interface_id: InterfaceId,
319        rng: &mut dyn Rng,
320    ) -> Vec<TransportAction> {
321        let mut actions = Vec::new();
322
323        // Compute interface hash from the interface name
324        let interface_hash = if let Some(info) = self.interfaces.get(&interface_id) {
325            hash::full_hash(info.name.as_bytes())
326        } else {
327            return actions;
328        };
329
330        match tunnel::build_tunnel_synthesize_data(identity, &interface_hash, rng) {
331            Ok((data, _tunnel_id)) => {
332                let dest_hash = crate::destination::destination_hash(
333                    "rnstransport",
334                    &["tunnel", "synthesize"],
335                    None,
336                );
337                actions.push(TransportAction::TunnelSynthesize {
338                    interface: interface_id,
339                    data,
340                    dest_hash,
341                });
342            }
343            Err(e) => {
344                // Can't synthesize — no private key or other error
345                let _ = e;
346            }
347        }
348
349        actions
350    }
351
352    /// Void a tunnel's interface connection (tunnel disconnected).
353    pub fn void_tunnel_interface(&mut self, tunnel_id: &[u8; 32]) {
354        self.tunnel_table.void_tunnel_interface(tunnel_id);
355    }
356
357    /// Access the tunnel table for queries.
358    pub fn tunnel_table(&self) -> &TunnelTable {
359        &self.tunnel_table
360    }
361
362    // =========================================================================
363    // Packet filter
364    // =========================================================================
365
366    /// Check if any local client interfaces are registered.
367    fn has_local_clients(&self) -> bool {
368        self.interfaces.values().any(|i| i.is_local_client)
369    }
370
371    /// Packet filter: dedup + basic validity.
372    ///
373    /// Transport.py:1187-1238
374    fn packet_filter(&self, packet: &RawPacket) -> bool {
375        // Filter packets for other transport instances
376        if packet.transport_id.is_some()
377            && packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
378        {
379            if let Some(ref identity_hash) = self.config.identity_hash {
380                if packet.transport_id.as_ref() != Some(identity_hash) {
381                    return false;
382                }
383            }
384        }
385
386        // Allow certain contexts unconditionally
387        match packet.context {
388            constants::CONTEXT_KEEPALIVE
389            | constants::CONTEXT_RESOURCE_REQ
390            | constants::CONTEXT_RESOURCE_PRF
391            | constants::CONTEXT_RESOURCE
392            | constants::CONTEXT_CACHE_REQUEST
393            | constants::CONTEXT_CHANNEL => return true,
394            _ => {}
395        }
396
397        // PLAIN/GROUP checks
398        if packet.flags.destination_type == constants::DESTINATION_PLAIN
399            || packet.flags.destination_type == constants::DESTINATION_GROUP
400        {
401            if packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE {
402                return packet.hops <= 1;
403            } else {
404                // PLAIN/GROUP ANNOUNCE is invalid
405                return false;
406            }
407        }
408
409        // Deduplication
410        if !self.packet_hashlist.is_duplicate(&packet.packet_hash) {
411            return true;
412        }
413
414        // Duplicate announce for SINGLE dest is allowed (path update)
415        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE
416            && packet.flags.destination_type == constants::DESTINATION_SINGLE
417        {
418            return true;
419        }
420
421        false
422    }
423
424    // =========================================================================
425    // Core API: handle_inbound
426    // =========================================================================
427
428    /// Process an inbound raw packet from a network interface.
429    ///
430    /// Returns a list of actions for the caller to execute.
431    pub fn handle_inbound(
432        &mut self,
433        raw: &[u8],
434        iface: InterfaceId,
435        now: f64,
436        rng: &mut dyn Rng,
437    ) -> Vec<TransportAction> {
438        let mut actions = Vec::new();
439
440        // 1. Unpack
441        let mut packet = match RawPacket::unpack(raw) {
442            Ok(p) => p,
443            Err(_) => return actions, // silent drop
444        };
445
446        // Save original raw (pre-hop-increment) for announce caching
447        let original_raw = raw.to_vec();
448
449        // 2. Increment hops
450        packet.hops += 1;
451
452        // 2a. If from a local client, decrement hops to cancel the +1
453        // (local clients are attached via shared instance, not a real hop)
454        let from_local_client = self
455            .interfaces
456            .get(&iface)
457            .map(|i| i.is_local_client)
458            .unwrap_or(false);
459        if from_local_client {
460            packet.hops = packet.hops.saturating_sub(1);
461        }
462
463        // 3. Packet filter
464        if !self.packet_filter(&packet) {
465            return actions;
466        }
467
468        // 4. Determine whether to add to hashlist now or defer
469        let mut remember_hash = true;
470
471        if self.link_table.contains_key(&packet.destination_hash) {
472            remember_hash = false;
473        }
474        if packet.flags.packet_type == constants::PACKET_TYPE_PROOF
475            && packet.context == constants::CONTEXT_LRPROOF
476        {
477            remember_hash = false;
478        }
479
480        if remember_hash {
481            self.packet_hashlist.add(packet.packet_hash);
482        }
483
484        // 4a. PLAIN broadcast bridging between local clients and external interfaces
485        if packet.flags.destination_type == constants::DESTINATION_PLAIN
486            && packet.flags.transport_type == constants::TRANSPORT_BROADCAST
487            && self.has_local_clients()
488        {
489            if from_local_client {
490                // From local client → forward to all external interfaces
491                actions.push(TransportAction::ForwardPlainBroadcast {
492                    raw: packet.raw.clone(),
493                    to_local: false,
494                    exclude: Some(iface),
495                });
496            } else {
497                // From external → forward to all local clients
498                actions.push(TransportAction::ForwardPlainBroadcast {
499                    raw: packet.raw.clone(),
500                    to_local: true,
501                    exclude: None,
502                });
503            }
504        }
505
506        // 5. Transport forwarding: if we are the designated next hop
507        if self.config.transport_enabled || self.config.identity_hash.is_some() {
508            if packet.transport_id.is_some()
509                && packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
510            {
511                if let Some(ref identity_hash) = self.config.identity_hash {
512                    if packet.transport_id.as_ref() == Some(identity_hash) {
513                        if let Some(path_entry) = self.path_table.get(&packet.destination_hash) {
514                            let next_hop = path_entry.next_hop;
515                            let remaining_hops = path_entry.hops;
516                            let outbound_interface = path_entry.receiving_interface;
517
518                            let new_raw = forward_transport_packet(
519                                &packet,
520                                next_hop,
521                                remaining_hops,
522                                outbound_interface,
523                            );
524
525                            // Create link table or reverse table entry
526                            if packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST {
527                                let proof_timeout = now
528                                    + constants::LINK_ESTABLISHMENT_TIMEOUT_PER_HOP
529                                        * (remaining_hops.max(1) as f64);
530
531                                let (link_id, link_entry) = create_link_entry(
532                                    &packet,
533                                    next_hop,
534                                    outbound_interface,
535                                    remaining_hops,
536                                    iface,
537                                    now,
538                                    proof_timeout,
539                                );
540                                self.link_table.insert(link_id, link_entry);
541                            } else {
542                                let (trunc_hash, reverse_entry) = create_reverse_entry(
543                                    &packet,
544                                    outbound_interface,
545                                    iface,
546                                    now,
547                                );
548                                self.reverse_table.insert(trunc_hash, reverse_entry);
549                            }
550
551                            actions.push(TransportAction::SendOnInterface {
552                                interface: outbound_interface,
553                                raw: new_raw,
554                            });
555
556                            // Update path timestamp
557                            if let Some(entry) = self.path_table.get_mut(&packet.destination_hash) {
558                                entry.timestamp = now;
559                            }
560                        }
561                    }
562                }
563            }
564
565            // 6. Link table routing for non-announce, non-linkrequest, non-lrproof
566            if packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
567                && packet.flags.packet_type != constants::PACKET_TYPE_LINKREQUEST
568                && packet.context != constants::CONTEXT_LRPROOF
569            {
570                if let Some(link_entry) = self.link_table.get(&packet.destination_hash).cloned() {
571                    if let Some((outbound_iface, new_raw)) =
572                        route_via_link_table(&packet, &link_entry, iface)
573                    {
574                        // Add to hashlist now that we know it's for us
575                        self.packet_hashlist.add(packet.packet_hash);
576
577                        actions.push(TransportAction::SendOnInterface {
578                            interface: outbound_iface,
579                            raw: new_raw,
580                        });
581
582                        // Update link timestamp
583                        if let Some(entry) =
584                            self.link_table.get_mut(&packet.destination_hash)
585                        {
586                            entry.timestamp = now;
587                        }
588                    }
589                }
590            }
591        }
592
593        // 7. Announce handling
594        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE {
595            self.process_inbound_announce(&packet, &original_raw, iface, now, rng, &mut actions);
596        }
597
598        // 8. Proof handling
599        if packet.flags.packet_type == constants::PACKET_TYPE_PROOF {
600            self.process_inbound_proof(&packet, iface, now, &mut actions);
601        }
602
603        // 9. Local delivery for LINKREQUEST and DATA
604        if packet.flags.packet_type == constants::PACKET_TYPE_LINKREQUEST
605            || packet.flags.packet_type == constants::PACKET_TYPE_DATA
606        {
607            if self.local_destinations.contains_key(&packet.destination_hash) {
608                actions.push(TransportAction::DeliverLocal {
609                    destination_hash: packet.destination_hash,
610                    raw: packet.raw.clone(),
611                    packet_hash: packet.packet_hash,
612                });
613            }
614        }
615
616        actions
617    }
618
619    // =========================================================================
620    // Inbound announce processing
621    // =========================================================================
622
623    fn process_inbound_announce(
624        &mut self,
625        packet: &RawPacket,
626        original_raw: &[u8],
627        iface: InterfaceId,
628        now: f64,
629        rng: &mut dyn Rng,
630        actions: &mut Vec<TransportAction>,
631    ) {
632        if packet.flags.destination_type != constants::DESTINATION_SINGLE {
633            return;
634        }
635
636        let has_ratchet = packet.flags.context_flag == constants::FLAG_SET;
637
638        // Unpack and validate announce
639        let announce = match AnnounceData::unpack(&packet.data, has_ratchet) {
640            Ok(a) => a,
641            Err(_) => return,
642        };
643
644        let validated = match announce.validate(&packet.destination_hash) {
645            Ok(v) => v,
646            Err(_) => return,
647        };
648
649        // Skip blackholed identities
650        if self.is_blackholed(&validated.identity_hash, now) {
651            return;
652        }
653
654        // Skip local destinations
655        if self.local_destinations.contains_key(&packet.destination_hash) {
656            return;
657        }
658
659        // Ingress control: hold announces from unknown destinations during bursts
660        if !self.path_table.contains_key(&packet.destination_hash) {
661            if let Some(info) = self.interfaces.get(&iface) {
662                if info.ingress_control {
663                    if self.ingress_control.should_ingress_limit(iface, info.ia_freq, info.started, now) {
664                        self.ingress_control.hold_announce(
665                            iface,
666                            packet.destination_hash,
667                            ingress_control::HeldAnnounce {
668                                raw: original_raw.to_vec(),
669                                hops: packet.hops,
670                                receiving_interface: iface,
671                                timestamp: now,
672                            },
673                        );
674                        return;
675                    }
676                }
677            }
678        }
679
680        // Detect retransmit completion
681        let received_from = if let Some(transport_id) = packet.transport_id {
682            // Check if this is a retransmit we can stop
683            if self.config.transport_enabled {
684                if let Some(announce_entry) = self.announce_table.get_mut(&packet.destination_hash) {
685                    if packet.hops.checked_sub(1) == Some(announce_entry.hops) {
686                        announce_entry.local_rebroadcasts += 1;
687                        if announce_entry.retries > 0
688                            && announce_entry.local_rebroadcasts >= constants::LOCAL_REBROADCASTS_MAX
689                        {
690                            self.announce_table.remove(&packet.destination_hash);
691                        }
692                    }
693                    // Check if our retransmit was passed on
694                    if let Some(announce_entry) = self.announce_table.get(&packet.destination_hash) {
695                        if packet.hops.checked_sub(1) == Some(announce_entry.hops + 1)
696                            && announce_entry.retries > 0
697                        {
698                            if now < announce_entry.retransmit_timeout {
699                                self.announce_table.remove(&packet.destination_hash);
700                            }
701                        }
702                    }
703                }
704            }
705            transport_id
706        } else {
707            packet.destination_hash
708        };
709
710        // Extract random blob
711        let random_blob = match extract_random_blob(&packet.data) {
712            Some(b) => b,
713            None => return,
714        };
715
716        // Check hop limit
717        if packet.hops >= constants::PATHFINDER_M + 1 {
718            return;
719        }
720
721        let announce_emitted = timebase_from_random_blob(&random_blob);
722
723        // Path update decision
724        let existing = self.path_table.get(&packet.destination_hash);
725        let is_unresponsive = self.path_is_unresponsive(&packet.destination_hash);
726
727        let decision = should_update_path(
728            existing,
729            packet.hops,
730            announce_emitted,
731            &random_blob,
732            is_unresponsive,
733            now,
734        );
735
736        if decision == PathDecision::Reject {
737            return;
738        }
739
740        // Rate limiting
741        let rate_blocked = if packet.context != constants::CONTEXT_PATH_RESPONSE {
742            if let Some(iface_info) = self.interfaces.get(&iface) {
743                self.rate_limiter.check_and_update(
744                    &packet.destination_hash,
745                    now,
746                    iface_info.announce_rate_target,
747                    iface_info.announce_rate_grace,
748                    iface_info.announce_rate_penalty,
749                )
750            } else {
751                false
752            }
753        } else {
754            false
755        };
756
757        // Get interface mode for expiry calculation
758        let interface_mode = self
759            .interfaces
760            .get(&iface)
761            .map(|i| i.mode)
762            .unwrap_or(constants::MODE_FULL);
763
764        let expires = compute_path_expires(now, interface_mode);
765
766        // Get existing random blobs
767        let existing_blobs = self
768            .path_table
769            .get(&packet.destination_hash)
770            .map(|e| e.random_blobs.clone())
771            .unwrap_or_default();
772
773        // Generate RNG value for retransmit timeout
774        let mut rng_bytes = [0u8; 8];
775        rng.fill_bytes(&mut rng_bytes);
776        let rng_value = (u64::from_le_bytes(rng_bytes) as f64) / (u64::MAX as f64);
777
778        let is_path_response = packet.context == constants::CONTEXT_PATH_RESPONSE;
779
780        let (path_entry, announce_entry) = announce_proc::process_validated_announce(
781            packet.destination_hash,
782            packet.hops,
783            &packet.data,
784            &packet.raw,
785            packet.packet_hash,
786            packet.flags.context_flag,
787            received_from,
788            iface,
789            now,
790            existing_blobs,
791            random_blob,
792            expires,
793            rng_value,
794            self.config.transport_enabled,
795            is_path_response,
796            rate_blocked,
797            Some(original_raw.to_vec()),
798        );
799
800        // Emit CacheAnnounce for disk caching (pre-hop-increment raw)
801        actions.push(TransportAction::CacheAnnounce {
802            packet_hash: packet.packet_hash,
803            raw: original_raw.to_vec(),
804        });
805
806        // Store path
807        self.path_table
808            .insert(packet.destination_hash, path_entry);
809
810        // If receiving interface has a tunnel_id, store path in tunnel table too
811        if let Some(tunnel_id) = self.interfaces.get(&iface).and_then(|i| i.tunnel_id) {
812            let blobs = self
813                .path_table
814                .get(&packet.destination_hash)
815                .map(|e| e.random_blobs.clone())
816                .unwrap_or_default();
817            self.tunnel_table.store_tunnel_path(
818                &tunnel_id,
819                packet.destination_hash,
820                tunnel::TunnelPath {
821                    timestamp: now,
822                    received_from,
823                    hops: packet.hops,
824                    expires,
825                    random_blobs: blobs,
826                    packet_hash: packet.packet_hash,
827                },
828                now,
829            );
830        }
831
832        // Mark path as unknown state on update
833        self.path_states.remove(&packet.destination_hash);
834
835        // Store announce for retransmission
836        if let Some(ann) = announce_entry {
837            self.announce_table.insert(packet.destination_hash, ann);
838        }
839
840        // Emit actions
841        actions.push(TransportAction::AnnounceReceived {
842            destination_hash: packet.destination_hash,
843            identity_hash: validated.identity_hash,
844            public_key: validated.public_key,
845            name_hash: validated.name_hash,
846            random_hash: validated.random_hash,
847            app_data: validated.app_data,
848            hops: packet.hops,
849            receiving_interface: iface,
850        });
851
852        actions.push(TransportAction::PathUpdated {
853            destination_hash: packet.destination_hash,
854            hops: packet.hops,
855            next_hop: received_from,
856            interface: iface,
857        });
858
859        // Forward announce to local clients if any are connected
860        if self.has_local_clients() {
861            actions.push(TransportAction::ForwardToLocalClients {
862                raw: packet.raw.clone(),
863                exclude: Some(iface),
864            });
865        }
866
867        // Check for discovery path requests waiting for this announce
868        if let Some(pr_entry) = self.discovery_path_requests_waiting(&packet.destination_hash) {
869            // Build a path response announce and queue it
870            let entry = AnnounceEntry {
871                timestamp: now,
872                retransmit_timeout: now,
873                retries: constants::PATHFINDER_R,
874                received_from,
875                hops: packet.hops,
876                packet_raw: packet.raw.clone(),
877                packet_data: packet.data.clone(),
878                destination_hash: packet.destination_hash,
879                context_flag: packet.flags.context_flag,
880                local_rebroadcasts: 0,
881                block_rebroadcasts: true,
882                attached_interface: Some(pr_entry),
883            };
884            self.announce_table
885                .insert(packet.destination_hash, entry);
886        }
887    }
888
889    /// Check if there's a waiting discovery path request for a destination.
890    /// Consumes the request if found (one-shot: the caller queues the announce response).
891    fn discovery_path_requests_waiting(&mut self, dest_hash: &[u8; 16]) -> Option<InterfaceId> {
892        self.discovery_path_requests
893            .remove(dest_hash)
894            .map(|req| req.requesting_interface)
895    }
896
897    // =========================================================================
898    // Inbound proof processing
899    // =========================================================================
900
901    fn process_inbound_proof(
902        &mut self,
903        packet: &RawPacket,
904        iface: InterfaceId,
905        _now: f64,
906        actions: &mut Vec<TransportAction>,
907    ) {
908        if packet.context == constants::CONTEXT_LRPROOF {
909            // Link request proof routing
910            if (self.config.transport_enabled) && self.link_table.contains_key(&packet.destination_hash)
911            {
912                let link_entry = self.link_table.get(&packet.destination_hash).cloned();
913                if let Some(entry) = link_entry {
914                    if packet.hops == entry.remaining_hops
915                        && iface == entry.next_hop_interface
916                    {
917                        // Forward the proof (simplified: skip signature validation
918                        // which requires Identity recall)
919                        let mut new_raw = Vec::new();
920                        new_raw.push(packet.raw[0]);
921                        new_raw.push(packet.hops);
922                        new_raw.extend_from_slice(&packet.raw[2..]);
923
924                        // Mark link as validated
925                        if let Some(le) =
926                            self.link_table.get_mut(&packet.destination_hash)
927                        {
928                            le.validated = true;
929                        }
930
931                        actions.push(TransportAction::SendOnInterface {
932                            interface: entry.received_interface,
933                            raw: new_raw,
934                        });
935                    }
936                }
937            } else {
938                // Could be for a local pending link - deliver locally
939                actions.push(TransportAction::DeliverLocal {
940                    destination_hash: packet.destination_hash,
941                    raw: packet.raw.clone(),
942                    packet_hash: packet.packet_hash,
943                });
944            }
945        } else {
946            // Regular proof: check reverse table
947            if self.config.transport_enabled {
948                if let Some(reverse_entry) =
949                    self.reverse_table.remove(&packet.destination_hash)
950                {
951                    if let Some(action) =
952                        route_proof_via_reverse(packet, &reverse_entry, iface)
953                    {
954                        actions.push(action);
955                    }
956                }
957            }
958
959            // Deliver to local receipts
960            actions.push(TransportAction::DeliverLocal {
961                destination_hash: packet.destination_hash,
962                raw: packet.raw.clone(),
963                packet_hash: packet.packet_hash,
964            });
965        }
966    }
967
968    // =========================================================================
969    // Core API: handle_outbound
970    // =========================================================================
971
972    /// Route an outbound packet.
973    pub fn handle_outbound(
974        &mut self,
975        packet: &RawPacket,
976        dest_type: u8,
977        attached_interface: Option<InterfaceId>,
978        now: f64,
979    ) -> Vec<TransportAction> {
980        let actions = route_outbound(
981            &self.path_table,
982            &self.interfaces,
983            &self.local_destinations,
984            packet,
985            dest_type,
986            attached_interface,
987            now,
988        );
989
990        // Add to packet hashlist for outbound packets
991        self.packet_hashlist.add(packet.packet_hash);
992
993        // Gate announces with hops > 0 through the bandwidth queue
994        if packet.flags.packet_type == constants::PACKET_TYPE_ANNOUNCE && packet.hops > 0 {
995            self.gate_announce_actions(actions, &packet.destination_hash, packet.hops, now)
996        } else {
997            actions
998        }
999    }
1000
1001    /// Gate announce SendOnInterface actions through per-interface bandwidth queues.
1002    fn gate_announce_actions(
1003        &mut self,
1004        actions: Vec<TransportAction>,
1005        dest_hash: &[u8; 16],
1006        hops: u8,
1007        now: f64,
1008    ) -> Vec<TransportAction> {
1009        let mut result = Vec::new();
1010        for action in actions {
1011            match action {
1012                TransportAction::SendOnInterface { interface, raw } => {
1013                    let (bitrate, announce_cap) =
1014                        if let Some(info) = self.interfaces.get(&interface) {
1015                            (info.bitrate, info.announce_cap)
1016                        } else {
1017                            (None, constants::ANNOUNCE_CAP)
1018                        };
1019                    if let Some(send_action) = self.announce_queues.gate_announce(
1020                        interface,
1021                        raw,
1022                        *dest_hash,
1023                        hops,
1024                        now,
1025                        now,
1026                        bitrate,
1027                        announce_cap,
1028                    ) {
1029                        result.push(send_action);
1030                    }
1031                    // If None, it was queued — no action emitted now
1032                }
1033                other => result.push(other),
1034            }
1035        }
1036        result
1037    }
1038
1039    // =========================================================================
1040    // Core API: tick
1041    // =========================================================================
1042
1043    /// Periodic maintenance. Call regularly (e.g., every 250ms).
1044    pub fn tick(&mut self, now: f64, rng: &mut dyn Rng) -> Vec<TransportAction> {
1045        let mut actions = Vec::new();
1046
1047        // Process pending announces
1048        if now > self.announces_last_checked + constants::ANNOUNCES_CHECK_INTERVAL {
1049            if let Some(ref identity_hash) = self.config.identity_hash {
1050                let ih = *identity_hash;
1051                let announce_actions = jobs::process_pending_announces(
1052                    &mut self.announce_table,
1053                    &mut self.held_announces,
1054                    &ih,
1055                    now,
1056                );
1057                // Gate retransmitted announces through bandwidth queues
1058                let gated = self.gate_retransmit_actions(announce_actions, now);
1059                actions.extend(gated);
1060            }
1061            self.announces_last_checked = now;
1062        }
1063
1064        // Process announce queues — dequeue waiting announces when bandwidth available
1065        let mut queue_actions = self.announce_queues.process_queues(now, &self.interfaces);
1066        actions.append(&mut queue_actions);
1067
1068        // Process ingress control: release held announces
1069        let ic_interfaces = self.ingress_control.interfaces_with_held();
1070        for iface_id in ic_interfaces {
1071            let (ia_freq, started, ic_enabled) = match self.interfaces.get(&iface_id) {
1072                Some(info) => (info.ia_freq, info.started, info.ingress_control),
1073                None => continue,
1074            };
1075            if !ic_enabled {
1076                continue;
1077            }
1078            if let Some(held) = self.ingress_control.process_held_announces(iface_id, ia_freq, started, now) {
1079                let released_actions = self.handle_inbound(&held.raw, held.receiving_interface, now, rng);
1080                actions.extend(released_actions);
1081            }
1082        }
1083
1084        // Cull tables
1085        if now > self.tables_last_culled + constants::TABLES_CULL_INTERVAL {
1086            jobs::cull_path_table(&mut self.path_table, &self.interfaces, now);
1087            jobs::cull_reverse_table(&mut self.reverse_table, &self.interfaces, now);
1088            jobs::cull_link_table(&mut self.link_table, &self.interfaces, now);
1089            jobs::cull_path_states(&mut self.path_states, &self.path_table);
1090            self.cull_blackholed(now);
1091            // Cull expired discovery path requests
1092            self.discovery_path_requests
1093                .retain(|_, req| now - req.timestamp < constants::DISCOVERY_PATH_REQUEST_TIMEOUT);
1094            // Cull tunnels: void missing interfaces, then remove expired
1095            self.tunnel_table.void_missing_interfaces(|id| self.interfaces.contains_key(id));
1096            self.tunnel_table.cull(now);
1097            self.tables_last_culled = now;
1098        }
1099
1100        // Hashlist rotation
1101        self.packet_hashlist.maybe_rotate();
1102
1103        // Cull PR tags if over limit
1104        if self.discovery_pr_tags.len() > constants::MAX_PR_TAGS {
1105            let start = self.discovery_pr_tags.len() - constants::MAX_PR_TAGS;
1106            self.discovery_pr_tags = self.discovery_pr_tags[start..].to_vec();
1107        }
1108
1109        actions
1110    }
1111
1112    /// Gate retransmitted announce actions through per-interface bandwidth queues.
1113    ///
1114    /// Retransmitted announces always have hops > 0.
1115    /// `BroadcastOnAllInterfaces` is expanded to per-interface sends gated through queues.
1116    fn gate_retransmit_actions(
1117        &mut self,
1118        actions: Vec<TransportAction>,
1119        now: f64,
1120    ) -> Vec<TransportAction> {
1121        let mut result = Vec::new();
1122        for action in actions {
1123            match action {
1124                TransportAction::SendOnInterface { interface, raw } => {
1125                    // Extract dest_hash from raw (bytes 2..18 for H1, 18..34 for H2)
1126                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
1127                    let (bitrate, announce_cap) =
1128                        if let Some(info) = self.interfaces.get(&interface) {
1129                            (info.bitrate, info.announce_cap)
1130                        } else {
1131                            (None, constants::ANNOUNCE_CAP)
1132                        };
1133                    if let Some(send_action) = self.announce_queues.gate_announce(
1134                        interface,
1135                        raw,
1136                        dest_hash,
1137                        hops,
1138                        now,
1139                        now,
1140                        bitrate,
1141                        announce_cap,
1142                    ) {
1143                        result.push(send_action);
1144                    }
1145                }
1146                TransportAction::BroadcastOnAllInterfaces { raw, exclude } => {
1147                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
1148                    // Expand to per-interface sends gated through queues,
1149                    // applying mode filtering (AP blocks non-local announces, etc.)
1150                    let iface_ids: Vec<(InterfaceId, Option<u64>, f64)> = self
1151                        .interfaces
1152                        .iter()
1153                        .filter(|(_, info)| info.out_capable)
1154                        .filter(|(id, _)| {
1155                            if let Some(ref ex) = exclude {
1156                                **id != *ex
1157                            } else {
1158                                true
1159                            }
1160                        })
1161                        .filter(|(_, info)| {
1162                            should_transmit_announce(
1163                                info,
1164                                &dest_hash,
1165                                hops,
1166                                &self.local_destinations,
1167                                &self.path_table,
1168                                &self.interfaces,
1169                            )
1170                        })
1171                        .map(|(id, info)| (*id, info.bitrate, info.announce_cap))
1172                        .collect();
1173
1174                    for (iface_id, bitrate, announce_cap) in iface_ids {
1175                        if let Some(send_action) = self.announce_queues.gate_announce(
1176                            iface_id,
1177                            raw.clone(),
1178                            dest_hash,
1179                            hops,
1180                            now,
1181                            now,
1182                            bitrate,
1183                            announce_cap,
1184                        ) {
1185                            result.push(send_action);
1186                        }
1187                    }
1188                }
1189                other => result.push(other),
1190            }
1191        }
1192        result
1193    }
1194
1195    /// Extract destination hash and hops from raw announce bytes.
1196    fn extract_announce_info(raw: &[u8]) -> ([u8; 16], u8) {
1197        if raw.len() < 18 {
1198            return ([0; 16], 0);
1199        }
1200        let header_type = (raw[0] >> 6) & 0x03;
1201        let hops = raw[1];
1202        if header_type == constants::HEADER_2 && raw.len() >= 34 {
1203            // H2: transport_id at [2..18], dest_hash at [18..34]
1204            let mut dest = [0u8; 16];
1205            dest.copy_from_slice(&raw[18..34]);
1206            (dest, hops)
1207        } else {
1208            // H1: dest_hash at [2..18]
1209            let mut dest = [0u8; 16];
1210            dest.copy_from_slice(&raw[2..18]);
1211            (dest, hops)
1212        }
1213    }
1214
1215    // =========================================================================
1216    // Path request handling
1217    // =========================================================================
1218
1219    /// Handle an incoming path request.
1220    ///
1221    /// Transport.py path_request_handler (~line 2700):
1222    /// - Dedup via unique tag
1223    /// - If local destination, caller handles announce
1224    /// - If path known and transport enabled, queue retransmit (with ROAMING loop prevention)
1225    /// - If path unknown and receiving interface is in DISCOVER_PATHS_FOR, store a
1226    ///   DiscoveryPathRequest and forward the raw path request on other OUT interfaces
1227    pub fn handle_path_request(
1228        &mut self,
1229        data: &[u8],
1230        interface_id: InterfaceId,
1231        now: f64,
1232    ) -> Vec<TransportAction> {
1233        let mut actions = Vec::new();
1234
1235        if data.len() < 16 {
1236            return actions;
1237        }
1238
1239        let mut destination_hash = [0u8; 16];
1240        destination_hash.copy_from_slice(&data[..16]);
1241
1242        // Extract requesting transport instance
1243        let _requesting_transport_id = if data.len() > 32 {
1244            let mut id = [0u8; 16];
1245            id.copy_from_slice(&data[16..32]);
1246            Some(id)
1247        } else {
1248            None
1249        };
1250
1251        // Extract tag
1252        let tag_bytes = if data.len() > 32 {
1253            Some(&data[32..])
1254        } else if data.len() > 16 {
1255            Some(&data[16..])
1256        } else {
1257            None
1258        };
1259
1260        if let Some(tag) = tag_bytes {
1261            let tag_len = tag.len().min(16);
1262            let mut unique_tag = [0u8; 32];
1263            unique_tag[..16].copy_from_slice(&destination_hash);
1264            unique_tag[16..16 + tag_len].copy_from_slice(&tag[..tag_len]);
1265
1266            if self.discovery_pr_tags.contains(&unique_tag) {
1267                return actions; // Duplicate tag
1268            }
1269            self.discovery_pr_tags.push(unique_tag);
1270        } else {
1271            return actions; // Tagless request
1272        }
1273
1274        // If destination is local, the caller should handle the announce
1275        if self.local_destinations.contains_key(&destination_hash) {
1276            return actions;
1277        }
1278
1279        // If we know the path and transport is enabled, queue retransmit
1280        if self.config.transport_enabled && self.path_table.contains_key(&destination_hash) {
1281            let path = self.path_table.get(&destination_hash).unwrap().clone();
1282
1283            // ROAMING loop prevention (Python Transport.py:2731-2732):
1284            // If the receiving interface is ROAMING and the known path's next-hop
1285            // is on the same interface, don't answer — it would loop.
1286            if let Some(recv_info) = self.interfaces.get(&interface_id) {
1287                if recv_info.mode == constants::MODE_ROAMING
1288                    && path.receiving_interface == interface_id
1289                {
1290                    return actions;
1291                }
1292            }
1293
1294            // Check if there's already an announce in the table
1295            if let Some(existing) = self.announce_table.remove(&destination_hash) {
1296                self.held_announces.insert(destination_hash, existing);
1297            }
1298
1299            let retransmit_timeout = if let Some(iface_info) = self.interfaces.get(&interface_id) {
1300                let base = now + constants::PATH_REQUEST_GRACE;
1301                if iface_info.mode == constants::MODE_ROAMING {
1302                    base + constants::PATH_REQUEST_RG
1303                } else {
1304                    base
1305                }
1306            } else {
1307                now + constants::PATH_REQUEST_GRACE
1308            };
1309
1310            let entry = AnnounceEntry {
1311                timestamp: now,
1312                retransmit_timeout,
1313                retries: constants::PATHFINDER_R,
1314                received_from: path.next_hop,
1315                hops: path.hops,
1316                packet_raw: Vec::new(),
1317                packet_data: Vec::new(),
1318                destination_hash,
1319                context_flag: 0,
1320                local_rebroadcasts: 0,
1321                block_rebroadcasts: true,
1322                attached_interface: Some(interface_id),
1323            };
1324
1325            self.announce_table.insert(destination_hash, entry);
1326        } else if self.config.transport_enabled {
1327            // Unknown path: check if receiving interface is in DISCOVER_PATHS_FOR
1328            let should_discover = self
1329                .interfaces
1330                .get(&interface_id)
1331                .map(|info| constants::DISCOVER_PATHS_FOR.contains(&info.mode))
1332                .unwrap_or(false);
1333
1334            if should_discover {
1335                // Store discovery request so we can respond when the announce arrives
1336                self.discovery_path_requests.insert(
1337                    destination_hash,
1338                    DiscoveryPathRequest {
1339                        timestamp: now,
1340                        requesting_interface: interface_id,
1341                    },
1342                );
1343
1344                // Forward the raw path request data on all other OUT-capable interfaces
1345                for (_, iface_info) in self.interfaces.iter() {
1346                    if iface_info.id != interface_id && iface_info.out_capable {
1347                        actions.push(TransportAction::SendOnInterface {
1348                            interface: iface_info.id,
1349                            raw: data.to_vec(),
1350                        });
1351                    }
1352                }
1353            }
1354        }
1355
1356        actions
1357    }
1358
1359    // =========================================================================
1360    // Public read accessors
1361    // =========================================================================
1362
1363    /// Iterate over all path table entries.
1364    pub fn path_table_entries(&self) -> impl Iterator<Item = (&[u8; 16], &PathEntry)> {
1365        self.path_table.iter()
1366    }
1367
1368    /// Number of registered interfaces.
1369    pub fn interface_count(&self) -> usize {
1370        self.interfaces.len()
1371    }
1372
1373    /// Number of link table entries.
1374    pub fn link_table_count(&self) -> usize {
1375        self.link_table.len()
1376    }
1377
1378    /// Access the rate limiter for reading rate table entries.
1379    pub fn rate_limiter(&self) -> &AnnounceRateLimiter {
1380        &self.rate_limiter
1381    }
1382
1383    /// Get interface info by id.
1384    pub fn interface_info(&self, id: &InterfaceId) -> Option<&InterfaceInfo> {
1385        self.interfaces.get(id)
1386    }
1387
1388    /// Redirect a path entry to a different interface (e.g. after direct connect).
1389    /// If no entry exists, creates a minimal direct path (hops=1).
1390    pub fn redirect_path(&mut self, dest_hash: &[u8; 16], interface: InterfaceId, now: f64) {
1391        if let Some(entry) = self.path_table.get_mut(dest_hash) {
1392            entry.receiving_interface = interface;
1393            entry.hops = 1;
1394        } else {
1395            self.path_table.insert(*dest_hash, PathEntry {
1396                timestamp: now,
1397                next_hop: [0u8; 16],
1398                hops: 1,
1399                expires: now + 3600.0,
1400                random_blobs: Vec::new(),
1401                receiving_interface: interface,
1402                packet_hash: [0u8; 32],
1403                announce_raw: None,
1404            });
1405        }
1406    }
1407
1408    /// Drop a path from the path table.
1409    pub fn drop_path(&mut self, dest_hash: &[u8; 16]) -> bool {
1410        self.path_table.remove(dest_hash).is_some()
1411    }
1412
1413    /// Drop all paths that route via a given transport hash.
1414    pub fn drop_all_via(&mut self, transport_hash: &[u8; 16]) -> usize {
1415        let before = self.path_table.len();
1416        self.path_table.retain(|_, entry| &entry.next_hop != transport_hash);
1417        before - self.path_table.len()
1418    }
1419
1420    /// Drop all pending announce retransmissions and bandwidth queues.
1421    pub fn drop_announce_queues(&mut self) {
1422        self.announce_table.clear();
1423        self.held_announces.clear();
1424        self.announce_queues = AnnounceQueues::new();
1425        self.ingress_control.clear();
1426    }
1427
1428    /// Get the transport identity hash.
1429    pub fn identity_hash(&self) -> Option<&[u8; 16]> {
1430        self.config.identity_hash.as_ref()
1431    }
1432
1433    /// Whether transport is enabled.
1434    pub fn transport_enabled(&self) -> bool {
1435        self.config.transport_enabled
1436    }
1437
1438    /// Access the transport configuration.
1439    pub fn config(&self) -> &TransportConfig {
1440        &self.config
1441    }
1442
1443    /// Get path table entries as tuples for management queries.
1444    /// Returns (dest_hash, timestamp, next_hop, hops, expires, interface_name).
1445    pub fn get_path_table(&self, max_hops: Option<u8>) -> Vec<([u8; 16], f64, [u8; 16], u8, f64, alloc::string::String)> {
1446        let mut result = Vec::new();
1447        for (dest_hash, entry) in self.path_table.iter() {
1448            if let Some(max) = max_hops {
1449                if entry.hops > max {
1450                    continue;
1451                }
1452            }
1453            let iface_name = self.interfaces.get(&entry.receiving_interface)
1454                .map(|i| i.name.clone())
1455                .unwrap_or_else(|| alloc::format!("Interface({})", entry.receiving_interface.0));
1456            result.push((*dest_hash, entry.timestamp, entry.next_hop, entry.hops, entry.expires, iface_name));
1457        }
1458        result
1459    }
1460
1461    /// Get rate table entries as tuples for management queries.
1462    /// Returns (dest_hash, last, rate_violations, blocked_until, timestamps).
1463    pub fn get_rate_table(&self) -> Vec<([u8; 16], f64, u32, f64, Vec<f64>)> {
1464        self.rate_limiter.entries()
1465            .map(|(hash, entry)| (*hash, entry.last, entry.rate_violations, entry.blocked_until, entry.timestamps.clone()))
1466            .collect()
1467    }
1468
1469    /// Get blackholed identities as tuples for management queries.
1470    /// Returns (identity_hash, created, expires, reason).
1471    pub fn get_blackholed(&self) -> Vec<([u8; 16], f64, f64, Option<alloc::string::String>)> {
1472        self.blackholed_entries()
1473            .map(|(hash, entry)| (*hash, entry.created, entry.expires, entry.reason.clone()))
1474            .collect()
1475    }
1476
1477    // =========================================================================
1478    // Ingress control
1479    // =========================================================================
1480
1481    /// Update the incoming announce frequency for an interface.
1482    pub fn update_interface_freq(&mut self, id: InterfaceId, ia_freq: f64) {
1483        if let Some(info) = self.interfaces.get_mut(&id) {
1484            info.ia_freq = ia_freq;
1485        }
1486    }
1487
1488    /// Get the count of held announces for an interface (for management reporting).
1489    pub fn held_announce_count(&self, interface: &InterfaceId) -> usize {
1490        self.ingress_control.held_count(interface)
1491    }
1492
1493    // =========================================================================
1494    // Testing helpers
1495    // =========================================================================
1496
1497    #[cfg(test)]
1498    pub(crate) fn path_table(&self) -> &BTreeMap<[u8; 16], PathEntry> {
1499        &self.path_table
1500    }
1501
1502    #[cfg(test)]
1503    pub(crate) fn announce_table(&self) -> &BTreeMap<[u8; 16], AnnounceEntry> {
1504        &self.announce_table
1505    }
1506
1507    #[cfg(test)]
1508    pub(crate) fn reverse_table(&self) -> &BTreeMap<[u8; 16], tables::ReverseEntry> {
1509        &self.reverse_table
1510    }
1511
1512    #[cfg(test)]
1513    pub(crate) fn link_table_ref(&self) -> &BTreeMap<[u8; 16], LinkEntry> {
1514        &self.link_table
1515    }
1516}
1517
1518#[cfg(test)]
1519mod tests {
1520    use super::*;
1521    use crate::packet::PacketFlags;
1522
1523    fn make_config(transport_enabled: bool) -> TransportConfig {
1524        TransportConfig {
1525            transport_enabled,
1526            identity_hash: if transport_enabled {
1527                Some([0x42; 16])
1528            } else {
1529                None
1530            },
1531        }
1532    }
1533
1534    fn make_interface(id: u64, mode: u8) -> InterfaceInfo {
1535        InterfaceInfo {
1536            id: InterfaceId(id),
1537            name: String::from("test"),
1538            mode,
1539            out_capable: true,
1540            in_capable: true,
1541            bitrate: None,
1542            announce_rate_target: None,
1543            announce_rate_grace: 0,
1544            announce_rate_penalty: 0.0,
1545            announce_cap: constants::ANNOUNCE_CAP,
1546            is_local_client: false,
1547            wants_tunnel: false,
1548            tunnel_id: None,
1549            mtu: constants::MTU as u32,
1550            ingress_control: false,
1551            ia_freq: 0.0,
1552            started: 0.0,
1553        }
1554    }
1555
1556    #[test]
1557    fn test_empty_engine() {
1558        let engine = TransportEngine::new(make_config(false));
1559        assert!(!engine.has_path(&[0; 16]));
1560        assert!(engine.hops_to(&[0; 16]).is_none());
1561        assert!(engine.next_hop(&[0; 16]).is_none());
1562    }
1563
1564    #[test]
1565    fn test_register_deregister_interface() {
1566        let mut engine = TransportEngine::new(make_config(false));
1567        engine.register_interface(make_interface(1, constants::MODE_FULL));
1568        assert!(engine.interfaces.contains_key(&InterfaceId(1)));
1569
1570        engine.deregister_interface(InterfaceId(1));
1571        assert!(!engine.interfaces.contains_key(&InterfaceId(1)));
1572    }
1573
1574    #[test]
1575    fn test_register_deregister_destination() {
1576        let mut engine = TransportEngine::new(make_config(false));
1577        let dest = [0x11; 16];
1578        engine.register_destination(dest, constants::DESTINATION_SINGLE);
1579        assert!(engine.local_destinations.contains_key(&dest));
1580
1581        engine.deregister_destination(&dest);
1582        assert!(!engine.local_destinations.contains_key(&dest));
1583    }
1584
1585    #[test]
1586    fn test_path_state() {
1587        let mut engine = TransportEngine::new(make_config(false));
1588        let dest = [0x22; 16];
1589
1590        assert!(!engine.path_is_unresponsive(&dest));
1591
1592        engine.mark_path_unresponsive(&dest, None);
1593        assert!(engine.path_is_unresponsive(&dest));
1594
1595        engine.mark_path_responsive(&dest);
1596        assert!(!engine.path_is_unresponsive(&dest));
1597    }
1598
1599    #[test]
1600    fn test_boundary_exempts_unresponsive() {
1601        let mut engine = TransportEngine::new(make_config(false));
1602        engine.register_interface(make_interface(1, constants::MODE_BOUNDARY));
1603        let dest = [0xB1; 16];
1604
1605        // Marking via a boundary interface should be skipped
1606        engine.mark_path_unresponsive(&dest, Some(InterfaceId(1)));
1607        assert!(!engine.path_is_unresponsive(&dest));
1608    }
1609
1610    #[test]
1611    fn test_non_boundary_marks_unresponsive() {
1612        let mut engine = TransportEngine::new(make_config(false));
1613        engine.register_interface(make_interface(1, constants::MODE_FULL));
1614        let dest = [0xB2; 16];
1615
1616        // Marking via a non-boundary interface should work
1617        engine.mark_path_unresponsive(&dest, Some(InterfaceId(1)));
1618        assert!(engine.path_is_unresponsive(&dest));
1619    }
1620
1621    #[test]
1622    fn test_expire_path() {
1623        let mut engine = TransportEngine::new(make_config(false));
1624        let dest = [0x33; 16];
1625
1626        engine.path_table.insert(
1627            dest,
1628            PathEntry {
1629                timestamp: 1000.0,
1630                next_hop: [0; 16],
1631                hops: 2,
1632                expires: 9999.0,
1633                random_blobs: Vec::new(),
1634                receiving_interface: InterfaceId(1),
1635                packet_hash: [0; 32],
1636                announce_raw: None,
1637            },
1638        );
1639
1640        assert!(engine.has_path(&dest));
1641        engine.expire_path(&dest);
1642        // Path still exists but expires = 0
1643        assert!(engine.has_path(&dest));
1644        assert_eq!(engine.path_table[&dest].expires, 0.0);
1645    }
1646
1647    #[test]
1648    fn test_link_table_operations() {
1649        let mut engine = TransportEngine::new(make_config(false));
1650        let link_id = [0x44; 16];
1651
1652        engine.register_link(
1653            link_id,
1654            LinkEntry {
1655                timestamp: 100.0,
1656                next_hop_transport_id: [0; 16],
1657                next_hop_interface: InterfaceId(1),
1658                remaining_hops: 3,
1659                received_interface: InterfaceId(2),
1660                taken_hops: 2,
1661                destination_hash: [0xAA; 16],
1662                validated: false,
1663                proof_timeout: 200.0,
1664            },
1665        );
1666
1667        assert!(engine.link_table.contains_key(&link_id));
1668        assert!(!engine.link_table[&link_id].validated);
1669
1670        engine.validate_link(&link_id);
1671        assert!(engine.link_table[&link_id].validated);
1672
1673        engine.remove_link(&link_id);
1674        assert!(!engine.link_table.contains_key(&link_id));
1675    }
1676
1677    #[test]
1678    fn test_packet_filter_drops_plain_announce() {
1679        let engine = TransportEngine::new(make_config(false));
1680        let flags = PacketFlags {
1681            header_type: constants::HEADER_1,
1682            context_flag: constants::FLAG_UNSET,
1683            transport_type: constants::TRANSPORT_BROADCAST,
1684            destination_type: constants::DESTINATION_PLAIN,
1685            packet_type: constants::PACKET_TYPE_ANNOUNCE,
1686        };
1687        let packet =
1688            RawPacket::pack(flags, 0, &[0; 16], None, constants::CONTEXT_NONE, b"test").unwrap();
1689        assert!(!engine.packet_filter(&packet));
1690    }
1691
1692    #[test]
1693    fn test_packet_filter_allows_keepalive() {
1694        let engine = TransportEngine::new(make_config(false));
1695        let flags = PacketFlags {
1696            header_type: constants::HEADER_1,
1697            context_flag: constants::FLAG_UNSET,
1698            transport_type: constants::TRANSPORT_BROADCAST,
1699            destination_type: constants::DESTINATION_SINGLE,
1700            packet_type: constants::PACKET_TYPE_DATA,
1701        };
1702        let packet = RawPacket::pack(
1703            flags,
1704            0,
1705            &[0; 16],
1706            None,
1707            constants::CONTEXT_KEEPALIVE,
1708            b"test",
1709        )
1710        .unwrap();
1711        assert!(engine.packet_filter(&packet));
1712    }
1713
1714    #[test]
1715    fn test_packet_filter_drops_high_hop_plain() {
1716        let engine = TransportEngine::new(make_config(false));
1717        let flags = PacketFlags {
1718            header_type: constants::HEADER_1,
1719            context_flag: constants::FLAG_UNSET,
1720            transport_type: constants::TRANSPORT_BROADCAST,
1721            destination_type: constants::DESTINATION_PLAIN,
1722            packet_type: constants::PACKET_TYPE_DATA,
1723        };
1724        let mut packet =
1725            RawPacket::pack(flags, 0, &[0; 16], None, constants::CONTEXT_NONE, b"test").unwrap();
1726        packet.hops = 2;
1727        assert!(!engine.packet_filter(&packet));
1728    }
1729
1730    #[test]
1731    fn test_packet_filter_allows_duplicate_single_announce() {
1732        let mut engine = TransportEngine::new(make_config(false));
1733        let flags = PacketFlags {
1734            header_type: constants::HEADER_1,
1735            context_flag: constants::FLAG_UNSET,
1736            transport_type: constants::TRANSPORT_BROADCAST,
1737            destination_type: constants::DESTINATION_SINGLE,
1738            packet_type: constants::PACKET_TYPE_ANNOUNCE,
1739        };
1740        let packet =
1741            RawPacket::pack(flags, 0, &[0; 16], None, constants::CONTEXT_NONE, &[0xAA; 64])
1742                .unwrap();
1743
1744        // Add to hashlist
1745        engine.packet_hashlist.add(packet.packet_hash);
1746
1747        // Should still pass filter (duplicate announce for SINGLE allowed)
1748        assert!(engine.packet_filter(&packet));
1749    }
1750
1751    #[test]
1752    fn test_tick_retransmits_announce() {
1753        let mut engine = TransportEngine::new(make_config(true));
1754        engine.register_interface(make_interface(1, constants::MODE_FULL));
1755
1756        let dest = [0x55; 16];
1757        engine.announce_table.insert(
1758            dest,
1759            AnnounceEntry {
1760                timestamp: 100.0,
1761                retransmit_timeout: 100.0, // ready to retransmit
1762                retries: 0,
1763                received_from: [0xAA; 16],
1764                hops: 2,
1765                packet_raw: vec![0x01, 0x02],
1766                packet_data: vec![0xCC; 10],
1767                destination_hash: dest,
1768                context_flag: 0,
1769                local_rebroadcasts: 0,
1770                block_rebroadcasts: false,
1771                attached_interface: None,
1772            },
1773        );
1774
1775        let mut rng = rns_crypto::FixedRng::new(&[0x42; 32]);
1776        let actions = engine.tick(200.0, &mut rng);
1777
1778        // Should have a send action for the retransmit (gated through announce queue,
1779        // expanded from BroadcastOnAllInterfaces to per-interface SendOnInterface)
1780        assert!(!actions.is_empty());
1781        assert!(matches!(
1782            &actions[0],
1783            TransportAction::SendOnInterface { .. }
1784        ));
1785
1786        // Retries should have increased
1787        assert_eq!(engine.announce_table[&dest].retries, 1);
1788    }
1789
1790    #[test]
1791    fn test_blackhole_identity() {
1792        let mut engine = TransportEngine::new(make_config(false));
1793        let hash = [0xAA; 16];
1794        let now = 1000.0;
1795
1796        assert!(!engine.is_blackholed(&hash, now));
1797
1798        engine.blackhole_identity(hash, now, None, Some(String::from("test")));
1799        assert!(engine.is_blackholed(&hash, now));
1800        assert!(engine.is_blackholed(&hash, now + 999999.0)); // never expires
1801
1802        assert!(engine.unblackhole_identity(&hash));
1803        assert!(!engine.is_blackholed(&hash, now));
1804        assert!(!engine.unblackhole_identity(&hash)); // already removed
1805    }
1806
1807    #[test]
1808    fn test_blackhole_with_duration() {
1809        let mut engine = TransportEngine::new(make_config(false));
1810        let hash = [0xBB; 16];
1811        let now = 1000.0;
1812
1813        engine.blackhole_identity(hash, now, Some(1.0), None); // 1 hour
1814        assert!(engine.is_blackholed(&hash, now));
1815        assert!(engine.is_blackholed(&hash, now + 3599.0)); // just before expiry
1816        assert!(!engine.is_blackholed(&hash, now + 3601.0)); // after expiry
1817    }
1818
1819    #[test]
1820    fn test_cull_blackholed() {
1821        let mut engine = TransportEngine::new(make_config(false));
1822        let hash1 = [0xCC; 16];
1823        let hash2 = [0xDD; 16];
1824        let now = 1000.0;
1825
1826        engine.blackhole_identity(hash1, now, Some(1.0), None); // 1 hour
1827        engine.blackhole_identity(hash2, now, None, None); // never expires
1828
1829        engine.cull_blackholed(now + 4000.0); // past hash1 expiry
1830
1831        assert!(!engine.blackholed_identities.contains_key(&hash1));
1832        assert!(engine.blackholed_identities.contains_key(&hash2));
1833    }
1834
1835    #[test]
1836    fn test_blackhole_blocks_announce() {
1837        use crate::announce::AnnounceData;
1838        use crate::destination::{destination_hash, name_hash};
1839
1840        let mut engine = TransportEngine::new(make_config(false));
1841        engine.register_interface(make_interface(1, constants::MODE_FULL));
1842
1843        let identity = rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x55; 32]));
1844        let dest_hash = destination_hash("test", &["app"], Some(identity.hash()));
1845        let name_h = name_hash("test", &["app"]);
1846        let random_hash = [0x42u8; 10];
1847
1848        let (announce_data, _) = AnnounceData::pack(
1849            &identity, &dest_hash, &name_h, &random_hash, None, None,
1850        ).unwrap();
1851
1852        let flags = PacketFlags {
1853            header_type: constants::HEADER_1,
1854            context_flag: constants::FLAG_UNSET,
1855            transport_type: constants::TRANSPORT_BROADCAST,
1856            destination_type: constants::DESTINATION_SINGLE,
1857            packet_type: constants::PACKET_TYPE_ANNOUNCE,
1858        };
1859        let packet = RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data).unwrap();
1860
1861        // Blackhole the identity
1862        let now = 1000.0;
1863        engine.blackhole_identity(*identity.hash(), now, None, None);
1864
1865        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
1866        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), now, &mut rng);
1867
1868        // Should produce no AnnounceReceived or PathUpdated actions
1869        assert!(actions.iter().all(|a| !matches!(a, TransportAction::AnnounceReceived { .. })));
1870        assert!(actions.iter().all(|a| !matches!(a, TransportAction::PathUpdated { .. })));
1871    }
1872
1873    #[test]
1874    fn test_tick_culls_expired_path() {
1875        let mut engine = TransportEngine::new(make_config(false));
1876        engine.register_interface(make_interface(1, constants::MODE_FULL));
1877
1878        let dest = [0x66; 16];
1879        engine.path_table.insert(
1880            dest,
1881            PathEntry {
1882                timestamp: 100.0,
1883                next_hop: [0; 16],
1884                hops: 2,
1885                expires: 200.0,
1886                random_blobs: Vec::new(),
1887                receiving_interface: InterfaceId(1),
1888                packet_hash: [0; 32],
1889                announce_raw: None,
1890            },
1891        );
1892
1893        assert!(engine.has_path(&dest));
1894
1895        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
1896        // Advance past cull interval and path expiry
1897        engine.tick(300.0, &mut rng);
1898
1899        assert!(!engine.has_path(&dest));
1900    }
1901
1902    // =========================================================================
1903    // Phase 7b: Local client transport tests
1904    // =========================================================================
1905
1906    fn make_local_client_interface(id: u64) -> InterfaceInfo {
1907        InterfaceInfo {
1908            id: InterfaceId(id),
1909            name: String::from("local_client"),
1910            mode: constants::MODE_FULL,
1911            out_capable: true,
1912            in_capable: true,
1913            bitrate: None,
1914            announce_rate_target: None,
1915            announce_rate_grace: 0,
1916            announce_rate_penalty: 0.0,
1917            announce_cap: constants::ANNOUNCE_CAP,
1918            is_local_client: true,
1919            wants_tunnel: false,
1920            tunnel_id: None,
1921            mtu: constants::MTU as u32,
1922            ingress_control: false,
1923            ia_freq: 0.0,
1924            started: 0.0,
1925        }
1926    }
1927
1928    #[test]
1929    fn test_has_local_clients() {
1930        let mut engine = TransportEngine::new(make_config(false));
1931        assert!(!engine.has_local_clients());
1932
1933        engine.register_interface(make_interface(1, constants::MODE_FULL));
1934        assert!(!engine.has_local_clients());
1935
1936        engine.register_interface(make_local_client_interface(2));
1937        assert!(engine.has_local_clients());
1938
1939        engine.deregister_interface(InterfaceId(2));
1940        assert!(!engine.has_local_clients());
1941    }
1942
1943    #[test]
1944    fn test_local_client_hop_decrement() {
1945        // Packets from local clients should have their hops decremented
1946        // to cancel the standard +1 (net zero change)
1947        let mut engine = TransportEngine::new(make_config(false));
1948        engine.register_interface(make_local_client_interface(1));
1949        engine.register_interface(make_interface(2, constants::MODE_FULL));
1950
1951        // Register destination so we get a DeliverLocal action
1952        let dest = [0xAA; 16];
1953        engine.register_destination(dest, constants::DESTINATION_PLAIN);
1954
1955        let flags = PacketFlags {
1956            header_type: constants::HEADER_1,
1957            context_flag: constants::FLAG_UNSET,
1958            transport_type: constants::TRANSPORT_BROADCAST,
1959            destination_type: constants::DESTINATION_PLAIN,
1960            packet_type: constants::PACKET_TYPE_DATA,
1961        };
1962        // Pack with hops=0
1963        let packet = RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"hello").unwrap();
1964
1965        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
1966        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
1967
1968        // Should have local delivery; hops should still be 0 (not 1)
1969        // because the local client decrement cancels the increment
1970        let deliver = actions.iter().find(|a| matches!(a, TransportAction::DeliverLocal { .. }));
1971        assert!(deliver.is_some(), "Should deliver locally");
1972    }
1973
1974    #[test]
1975    fn test_plain_broadcast_from_local_client() {
1976        // PLAIN broadcast from local client should forward to external interfaces
1977        let mut engine = TransportEngine::new(make_config(false));
1978        engine.register_interface(make_local_client_interface(1));
1979        engine.register_interface(make_interface(2, constants::MODE_FULL));
1980
1981        let dest = [0xBB; 16];
1982        let flags = PacketFlags {
1983            header_type: constants::HEADER_1,
1984            context_flag: constants::FLAG_UNSET,
1985            transport_type: constants::TRANSPORT_BROADCAST,
1986            destination_type: constants::DESTINATION_PLAIN,
1987            packet_type: constants::PACKET_TYPE_DATA,
1988        };
1989        let packet = RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
1990
1991        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
1992        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
1993
1994        // Should have ForwardPlainBroadcast to external (to_local=false)
1995        let forward = actions.iter().find(|a| matches!(
1996            a, TransportAction::ForwardPlainBroadcast { to_local: false, .. }
1997        ));
1998        assert!(forward.is_some(), "Should forward to external interfaces");
1999    }
2000
2001    #[test]
2002    fn test_plain_broadcast_from_external() {
2003        // PLAIN broadcast from external should forward to local clients
2004        let mut engine = TransportEngine::new(make_config(false));
2005        engine.register_interface(make_local_client_interface(1));
2006        engine.register_interface(make_interface(2, constants::MODE_FULL));
2007
2008        let dest = [0xCC; 16];
2009        let flags = PacketFlags {
2010            header_type: constants::HEADER_1,
2011            context_flag: constants::FLAG_UNSET,
2012            transport_type: constants::TRANSPORT_BROADCAST,
2013            destination_type: constants::DESTINATION_PLAIN,
2014            packet_type: constants::PACKET_TYPE_DATA,
2015        };
2016        let packet = RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
2017
2018        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2019        let actions = engine.handle_inbound(&packet.raw, InterfaceId(2), 1000.0, &mut rng);
2020
2021        // Should have ForwardPlainBroadcast to local clients (to_local=true)
2022        let forward = actions.iter().find(|a| matches!(
2023            a, TransportAction::ForwardPlainBroadcast { to_local: true, .. }
2024        ));
2025        assert!(forward.is_some(), "Should forward to local clients");
2026    }
2027
2028    #[test]
2029    fn test_no_plain_broadcast_bridging_without_local_clients() {
2030        // Without local clients, no bridging should happen
2031        let mut engine = TransportEngine::new(make_config(false));
2032        engine.register_interface(make_interface(1, constants::MODE_FULL));
2033        engine.register_interface(make_interface(2, constants::MODE_FULL));
2034
2035        let dest = [0xDD; 16];
2036        let flags = PacketFlags {
2037            header_type: constants::HEADER_1,
2038            context_flag: constants::FLAG_UNSET,
2039            transport_type: constants::TRANSPORT_BROADCAST,
2040            destination_type: constants::DESTINATION_PLAIN,
2041            packet_type: constants::PACKET_TYPE_DATA,
2042        };
2043        let packet = RawPacket::pack(flags, 0, &dest, None, constants::CONTEXT_NONE, b"test").unwrap();
2044
2045        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2046        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2047
2048        // No ForwardPlainBroadcast should be emitted
2049        let has_forward = actions.iter().any(|a| matches!(
2050            a, TransportAction::ForwardPlainBroadcast { .. }
2051        ));
2052        assert!(!has_forward, "No bridging without local clients");
2053    }
2054
2055    #[test]
2056    fn test_announce_forwarded_to_local_clients() {
2057        use crate::announce::AnnounceData;
2058        use crate::destination::{destination_hash, name_hash};
2059
2060        let mut engine = TransportEngine::new(make_config(false));
2061        engine.register_interface(make_interface(1, constants::MODE_FULL));
2062        engine.register_interface(make_local_client_interface(2));
2063
2064        let identity = rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x77; 32]));
2065        let dest_hash = destination_hash("test", &["fwd"], Some(identity.hash()));
2066        let name_h = name_hash("test", &["fwd"]);
2067        let random_hash = [0x42u8; 10];
2068
2069        let (announce_data, _) = AnnounceData::pack(
2070            &identity, &dest_hash, &name_h, &random_hash, None, None,
2071        ).unwrap();
2072
2073        let flags = PacketFlags {
2074            header_type: constants::HEADER_1,
2075            context_flag: constants::FLAG_UNSET,
2076            transport_type: constants::TRANSPORT_BROADCAST,
2077            destination_type: constants::DESTINATION_SINGLE,
2078            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2079        };
2080        let packet = RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data).unwrap();
2081
2082        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
2083        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2084
2085        // Should have ForwardToLocalClients since we have local clients
2086        let forward = actions.iter().find(|a| matches!(
2087            a, TransportAction::ForwardToLocalClients { .. }
2088        ));
2089        assert!(forward.is_some(), "Should forward announce to local clients");
2090
2091        // The exclude should be the receiving interface
2092        match forward.unwrap() {
2093            TransportAction::ForwardToLocalClients { exclude, .. } => {
2094                assert_eq!(*exclude, Some(InterfaceId(1)));
2095            }
2096            _ => unreachable!(),
2097        }
2098    }
2099
2100    #[test]
2101    fn test_no_announce_forward_without_local_clients() {
2102        use crate::announce::AnnounceData;
2103        use crate::destination::{destination_hash, name_hash};
2104
2105        let mut engine = TransportEngine::new(make_config(false));
2106        engine.register_interface(make_interface(1, constants::MODE_FULL));
2107
2108        let identity = rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x88; 32]));
2109        let dest_hash = destination_hash("test", &["nofwd"], Some(identity.hash()));
2110        let name_h = name_hash("test", &["nofwd"]);
2111        let random_hash = [0x42u8; 10];
2112
2113        let (announce_data, _) = AnnounceData::pack(
2114            &identity, &dest_hash, &name_h, &random_hash, None, None,
2115        ).unwrap();
2116
2117        let flags = PacketFlags {
2118            header_type: constants::HEADER_1,
2119            context_flag: constants::FLAG_UNSET,
2120            transport_type: constants::TRANSPORT_BROADCAST,
2121            destination_type: constants::DESTINATION_SINGLE,
2122            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2123        };
2124        let packet = RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data).unwrap();
2125
2126        let mut rng = rns_crypto::FixedRng::new(&[0x22; 32]);
2127        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2128
2129        // No ForwardToLocalClients should be emitted
2130        let has_forward = actions.iter().any(|a| matches!(
2131            a, TransportAction::ForwardToLocalClients { .. }
2132        ));
2133        assert!(!has_forward, "No forward without local clients");
2134    }
2135
2136    #[test]
2137    fn test_local_client_exclude_from_forward() {
2138        use crate::announce::AnnounceData;
2139        use crate::destination::{destination_hash, name_hash};
2140
2141        let mut engine = TransportEngine::new(make_config(false));
2142        engine.register_interface(make_local_client_interface(1));
2143        engine.register_interface(make_local_client_interface(2));
2144
2145        let identity = rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0x99; 32]));
2146        let dest_hash = destination_hash("test", &["excl"], Some(identity.hash()));
2147        let name_h = name_hash("test", &["excl"]);
2148        let random_hash = [0x42u8; 10];
2149
2150        let (announce_data, _) = AnnounceData::pack(
2151            &identity, &dest_hash, &name_h, &random_hash, None, None,
2152        ).unwrap();
2153
2154        let flags = PacketFlags {
2155            header_type: constants::HEADER_1,
2156            context_flag: constants::FLAG_UNSET,
2157            transport_type: constants::TRANSPORT_BROADCAST,
2158            destination_type: constants::DESTINATION_SINGLE,
2159            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2160        };
2161        let packet = RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data).unwrap();
2162
2163        let mut rng = rns_crypto::FixedRng::new(&[0x33; 32]);
2164        // Feed announce from local client 1
2165        let actions = engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2166
2167        // Should forward to local clients, excluding interface 1 (the sender)
2168        let forward = actions.iter().find(|a| matches!(
2169            a, TransportAction::ForwardToLocalClients { .. }
2170        ));
2171        assert!(forward.is_some());
2172        match forward.unwrap() {
2173            TransportAction::ForwardToLocalClients { exclude, .. } => {
2174                assert_eq!(*exclude, Some(InterfaceId(1)));
2175            }
2176            _ => unreachable!(),
2177        }
2178    }
2179
2180    // =========================================================================
2181    // Phase 7d: Tunnel tests
2182    // =========================================================================
2183
2184    fn make_tunnel_interface(id: u64) -> InterfaceInfo {
2185        InterfaceInfo {
2186            id: InterfaceId(id),
2187            name: String::from("tunnel_iface"),
2188            mode: constants::MODE_FULL,
2189            out_capable: true,
2190            in_capable: true,
2191            bitrate: None,
2192            announce_rate_target: None,
2193            announce_rate_grace: 0,
2194            announce_rate_penalty: 0.0,
2195            announce_cap: constants::ANNOUNCE_CAP,
2196            is_local_client: false,
2197            wants_tunnel: true,
2198            tunnel_id: None,
2199            mtu: constants::MTU as u32,
2200            ingress_control: false,
2201            ia_freq: 0.0,
2202            started: 0.0,
2203        }
2204    }
2205
2206    #[test]
2207    fn test_handle_tunnel_new() {
2208        let mut engine = TransportEngine::new(make_config(true));
2209        engine.register_interface(make_tunnel_interface(1));
2210
2211        let tunnel_id = [0xAA; 32];
2212        let actions = engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2213
2214        // Should emit TunnelEstablished
2215        assert!(actions.iter().any(|a| matches!(
2216            a, TransportAction::TunnelEstablished { .. }
2217        )));
2218
2219        // Interface should now have tunnel_id set
2220        let info = engine.interface_info(&InterfaceId(1)).unwrap();
2221        assert_eq!(info.tunnel_id, Some(tunnel_id));
2222
2223        // Tunnel table should have the entry
2224        assert_eq!(engine.tunnel_table().len(), 1);
2225    }
2226
2227    #[test]
2228    fn test_announce_stores_tunnel_path() {
2229        use crate::announce::AnnounceData;
2230        use crate::destination::{destination_hash, name_hash};
2231
2232        let mut engine = TransportEngine::new(make_config(false));
2233        let mut iface = make_tunnel_interface(1);
2234        let tunnel_id = [0xBB; 32];
2235        iface.tunnel_id = Some(tunnel_id);
2236        engine.register_interface(iface);
2237
2238        // Create tunnel entry
2239        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2240
2241        // Create and send an announce
2242        let identity = rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xCC; 32]));
2243        let dest_hash = destination_hash("test", &["tunnel"], Some(identity.hash()));
2244        let name_h = name_hash("test", &["tunnel"]);
2245        let random_hash = [0x42u8; 10];
2246
2247        let (announce_data, _) = AnnounceData::pack(
2248            &identity, &dest_hash, &name_h, &random_hash, None, None,
2249        ).unwrap();
2250
2251        let flags = PacketFlags {
2252            header_type: constants::HEADER_1,
2253            context_flag: constants::FLAG_UNSET,
2254            transport_type: constants::TRANSPORT_BROADCAST,
2255            destination_type: constants::DESTINATION_SINGLE,
2256            packet_type: constants::PACKET_TYPE_ANNOUNCE,
2257        };
2258        let packet = RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, &announce_data).unwrap();
2259
2260        let mut rng = rns_crypto::FixedRng::new(&[0xDD; 32]);
2261        engine.handle_inbound(&packet.raw, InterfaceId(1), 1000.0, &mut rng);
2262
2263        // Path should be in path table
2264        assert!(engine.has_path(&dest_hash));
2265
2266        // Path should also be in tunnel table
2267        let tunnel = engine.tunnel_table().get(&tunnel_id).unwrap();
2268        assert_eq!(tunnel.paths.len(), 1);
2269        assert!(tunnel.paths.contains_key(&dest_hash));
2270    }
2271
2272    #[test]
2273    fn test_tunnel_reattach_restores_paths() {
2274        let mut engine = TransportEngine::new(make_config(true));
2275        engine.register_interface(make_tunnel_interface(1));
2276
2277        let tunnel_id = [0xCC; 32];
2278        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2279
2280        // Manually add a path to the tunnel
2281        let dest = [0xDD; 16];
2282        engine.tunnel_table.store_tunnel_path(
2283            &tunnel_id,
2284            dest,
2285            tunnel::TunnelPath {
2286                timestamp: 1000.0,
2287                received_from: [0xEE; 16],
2288                hops: 3,
2289                expires: 1000.0 + constants::DESTINATION_TIMEOUT,
2290                random_blobs: Vec::new(),
2291                packet_hash: [0xFF; 32],
2292            },
2293            1000.0,
2294        );
2295
2296        // Void the tunnel interface (disconnect)
2297        engine.void_tunnel_interface(&tunnel_id);
2298
2299        // Remove path from path table to simulate it expiring
2300        engine.path_table.remove(&dest);
2301        assert!(!engine.has_path(&dest));
2302
2303        // Reattach tunnel on new interface
2304        engine.register_interface(make_interface(2, constants::MODE_FULL));
2305        let actions = engine.handle_tunnel(tunnel_id, InterfaceId(2), 2000.0);
2306
2307        // Should restore the path
2308        assert!(engine.has_path(&dest));
2309        let path = engine.path_table.get(&dest).unwrap();
2310        assert_eq!(path.hops, 3);
2311        assert_eq!(path.receiving_interface, InterfaceId(2));
2312
2313        // Should emit TunnelEstablished
2314        assert!(actions.iter().any(|a| matches!(
2315            a, TransportAction::TunnelEstablished { .. }
2316        )));
2317    }
2318
2319    #[test]
2320    fn test_void_tunnel_interface() {
2321        let mut engine = TransportEngine::new(make_config(true));
2322        engine.register_interface(make_tunnel_interface(1));
2323
2324        let tunnel_id = [0xDD; 32];
2325        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2326
2327        // Verify tunnel has interface
2328        assert_eq!(
2329            engine.tunnel_table().get(&tunnel_id).unwrap().interface,
2330            Some(InterfaceId(1))
2331        );
2332
2333        engine.void_tunnel_interface(&tunnel_id);
2334
2335        // Interface voided, but tunnel still exists
2336        assert_eq!(engine.tunnel_table().len(), 1);
2337        assert_eq!(
2338            engine.tunnel_table().get(&tunnel_id).unwrap().interface,
2339            None
2340        );
2341    }
2342
2343    #[test]
2344    fn test_tick_culls_tunnels() {
2345        let mut engine = TransportEngine::new(make_config(true));
2346        engine.register_interface(make_tunnel_interface(1));
2347
2348        let tunnel_id = [0xEE; 32];
2349        engine.handle_tunnel(tunnel_id, InterfaceId(1), 1000.0);
2350        assert_eq!(engine.tunnel_table().len(), 1);
2351
2352        let mut rng = rns_crypto::FixedRng::new(&[0; 32]);
2353
2354        // Tick past DESTINATION_TIMEOUT + TABLES_CULL_INTERVAL
2355        engine.tick(1000.0 + constants::DESTINATION_TIMEOUT + constants::TABLES_CULL_INTERVAL + 1.0, &mut rng);
2356
2357        assert_eq!(engine.tunnel_table().len(), 0);
2358    }
2359
2360    #[test]
2361    fn test_synthesize_tunnel() {
2362        let mut engine = TransportEngine::new(make_config(true));
2363        engine.register_interface(make_tunnel_interface(1));
2364
2365        let identity = rns_crypto::identity::Identity::new(&mut rns_crypto::FixedRng::new(&[0xFF; 32]));
2366        let mut rng = rns_crypto::FixedRng::new(&[0x11; 32]);
2367
2368        let actions = engine.synthesize_tunnel(&identity, InterfaceId(1), &mut rng);
2369
2370        // Should produce a TunnelSynthesize action
2371        assert_eq!(actions.len(), 1);
2372        match &actions[0] {
2373            TransportAction::TunnelSynthesize { interface, data, dest_hash } => {
2374                assert_eq!(*interface, InterfaceId(1));
2375                assert_eq!(data.len(), tunnel::TUNNEL_SYNTH_LENGTH);
2376                // dest_hash should be the tunnel.synthesize plain destination
2377                let expected_dest = crate::destination::destination_hash(
2378                    "rnstransport", &["tunnel", "synthesize"], None,
2379                );
2380                assert_eq!(*dest_hash, expected_dest);
2381            }
2382            _ => panic!("Expected TunnelSynthesize"),
2383        }
2384    }
2385
2386    // =========================================================================
2387    // DISCOVER_PATHS_FOR tests
2388    // =========================================================================
2389
2390    fn make_path_request_data(dest_hash: &[u8; 16], tag: &[u8]) -> Vec<u8> {
2391        let mut data = Vec::new();
2392        data.extend_from_slice(dest_hash);
2393        data.extend_from_slice(tag);
2394        data
2395    }
2396
2397    #[test]
2398    fn test_path_request_forwarded_on_ap() {
2399        let mut engine = TransportEngine::new(make_config(true));
2400        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
2401        engine.register_interface(make_interface(2, constants::MODE_FULL));
2402
2403        let dest = [0xD1; 16];
2404        let tag = [0x01; 16];
2405        let data = make_path_request_data(&dest, &tag);
2406
2407        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2408
2409        // Should forward the path request on interface 2 (the other OUT interface)
2410        assert_eq!(actions.len(), 1);
2411        match &actions[0] {
2412            TransportAction::SendOnInterface { interface, .. } => {
2413                assert_eq!(*interface, InterfaceId(2));
2414            }
2415            _ => panic!("Expected SendOnInterface for forwarded path request"),
2416        }
2417        // Should have stored a discovery path request
2418        assert!(engine.discovery_path_requests.contains_key(&dest));
2419    }
2420
2421    #[test]
2422    fn test_path_request_not_forwarded_on_full() {
2423        let mut engine = TransportEngine::new(make_config(true));
2424        engine.register_interface(make_interface(1, constants::MODE_FULL));
2425        engine.register_interface(make_interface(2, constants::MODE_FULL));
2426
2427        let dest = [0xD2; 16];
2428        let tag = [0x02; 16];
2429        let data = make_path_request_data(&dest, &tag);
2430
2431        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2432
2433        // MODE_FULL is not in DISCOVER_PATHS_FOR, so no forwarding
2434        assert!(actions.is_empty());
2435        assert!(!engine.discovery_path_requests.contains_key(&dest));
2436    }
2437
2438    #[test]
2439    fn test_roaming_loop_prevention() {
2440        let mut engine = TransportEngine::new(make_config(true));
2441        engine.register_interface(make_interface(1, constants::MODE_ROAMING));
2442
2443        let dest = [0xD3; 16];
2444        // Path is known and routes through the same interface (1)
2445        engine.path_table.insert(
2446            dest,
2447            PathEntry {
2448                timestamp: 900.0,
2449                next_hop: [0xAA; 16],
2450                hops: 2,
2451                expires: 9999.0,
2452                random_blobs: Vec::new(),
2453                receiving_interface: InterfaceId(1),
2454                packet_hash: [0; 32],
2455                announce_raw: None,
2456            },
2457        );
2458
2459        let tag = [0x03; 16];
2460        let data = make_path_request_data(&dest, &tag);
2461
2462        let actions = engine.handle_path_request(&data, InterfaceId(1), 1000.0);
2463
2464        // ROAMING interface, path next-hop on same interface → loop prevention, no action
2465        assert!(actions.is_empty());
2466        assert!(!engine.announce_table.contains_key(&dest));
2467    }
2468
2469    #[test]
2470    fn test_discovery_request_consumed_on_announce() {
2471        let mut engine = TransportEngine::new(make_config(true));
2472        engine.register_interface(make_interface(1, constants::MODE_ACCESS_POINT));
2473
2474        let dest = [0xD4; 16];
2475
2476        // Simulate a waiting discovery request
2477        engine.discovery_path_requests.insert(
2478            dest,
2479            DiscoveryPathRequest {
2480                timestamp: 900.0,
2481                requesting_interface: InterfaceId(1),
2482            },
2483        );
2484
2485        // Consume it
2486        let iface = engine.discovery_path_requests_waiting(&dest);
2487        assert_eq!(iface, Some(InterfaceId(1)));
2488
2489        // Should be gone now
2490        assert!(!engine.discovery_path_requests.contains_key(&dest));
2491        assert_eq!(engine.discovery_path_requests_waiting(&dest), None);
2492    }
2493}