Skip to main content

rns_core/transport/
mod.rs

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