Skip to main content

rns_core/transport/
mod.rs

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