Skip to main content

rns_net/
driver.rs

1//! Driver loop: receives events, drives the TransportEngine, dispatches actions.
2
3use std::collections::HashMap;
4use std::io;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant};
8
9use rns_core::packet::RawPacket;
10use rns_core::transport::announce_verify_queue::{AnnounceVerifyQueue, OverflowPolicy};
11use rns_core::transport::tables::PathEntry;
12use rns_core::transport::types::{InterfaceId, TransportAction, TransportConfig};
13use rns_core::transport::TransportEngine;
14use rns_crypto::{OsRng, Rng};
15
16#[cfg(feature = "hooks")]
17use crate::provider_bridge::ProviderBridge;
18#[cfg(feature = "hooks")]
19use rns_hooks::{create_hook_slots, EngineAccess, HookContext, HookManager, HookPoint, HookSlot};
20
21#[cfg(feature = "hooks")]
22use crate::event::BackbonePeerHookEvent;
23use crate::event::{
24    BackbonePeerPoolMemberStatus, BackbonePeerPoolStatus, BackbonePeerStateEntry, BlackholeInfo,
25    DrainStatus, Event, EventReceiver, InterfaceStatsResponse, KnownDestinationEntry,
26    LifecycleState, LocalDestinationEntry, NextHopResponse, PathTableEntry, QueryRequest,
27    QueryResponse, RateTableEntry, RuntimeConfigApplyMode, RuntimeConfigEntry, RuntimeConfigError,
28    RuntimeConfigErrorCode, RuntimeConfigSource, RuntimeConfigValue, SingleInterfaceStat,
29};
30use crate::holepunch::orchestrator::{HolePunchManager, HolePunchManagerAction};
31use crate::ifac;
32#[cfg(all(feature = "iface-auto", test))]
33use crate::interface::auto::AutoRuntime;
34#[cfg(feature = "iface-auto")]
35use crate::interface::auto::AutoRuntimeConfigHandle;
36#[cfg(feature = "iface-backbone")]
37use crate::interface::backbone::{
38    start_client_with_ifac, BackboneClientConfig, BackboneClientRuntime,
39    BackboneClientRuntimeConfigHandle, BackbonePeerStateHandle, BackboneRuntimeConfigHandle,
40};
41#[cfg(all(feature = "iface-backbone", target_os = "linux", test))]
42use crate::interface::backbone::{BackboneAbuseConfig, BackboneServerRuntime};
43#[cfg(all(feature = "iface-i2p", test))]
44use crate::interface::i2p::I2pRuntime;
45#[cfg(feature = "iface-i2p")]
46use crate::interface::i2p::I2pRuntimeConfigHandle;
47#[cfg(all(feature = "iface-pipe", test))]
48use crate::interface::pipe::PipeRuntime;
49#[cfg(feature = "iface-pipe")]
50use crate::interface::pipe::PipeRuntimeConfigHandle;
51#[cfg(feature = "iface-rnode")]
52use crate::interface::rnode::{
53    validate_sub_config, RNodeRuntime, RNodeRuntimeConfigHandle, RNodeSubConfig,
54};
55#[cfg(feature = "iface-tcp")]
56use crate::interface::tcp::TcpClientRuntimeConfigHandle;
57#[cfg(all(feature = "iface-tcp", test))]
58use crate::interface::tcp_server::TcpServerRuntime;
59#[cfg(feature = "iface-tcp")]
60use crate::interface::tcp_server::TcpServerRuntimeConfigHandle;
61#[cfg(all(feature = "iface-udp", test))]
62use crate::interface::udp::UdpRuntime;
63#[cfg(feature = "iface-udp")]
64use crate::interface::udp::UdpRuntimeConfigHandle;
65use crate::interface::{InterfaceEntry, InterfaceStats};
66use crate::link_manager::{LinkManager, LinkManagerAction};
67use crate::time;
68
69const DEFAULT_KNOWN_DESTINATIONS_TTL: f64 = 48.0 * 60.0 * 60.0;
70const DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES: usize = 8192;
71const DEFAULT_RATE_LIMITER_TTL_SECS: f64 = 48.0 * 60.0 * 60.0;
72const DEFAULT_TICK_INTERVAL_MS: u64 = 1000;
73const DEFAULT_KNOWN_DESTINATIONS_CLEANUP_INTERVAL_TICKS: u32 = 3600;
74const KNOWN_DESTINATION_CLEANUP_BATCH_SIZE: usize = 64;
75const DEFAULT_ANNOUNCE_CACHE_CLEANUP_INTERVAL_TICKS: u32 = 3600;
76const DEFAULT_ANNOUNCE_CACHE_CLEANUP_BATCH_SIZE: usize = 10_000;
77const DEFAULT_DISCOVERY_CLEANUP_INTERVAL_TICKS: u32 = 3600;
78const DEFAULT_MANAGEMENT_ANNOUNCE_INTERVAL_SECS: f64 = 300.0;
79const DEFAULT_LINK_TEARDOWN_FLUSH: Duration = Duration::from_millis(150);
80const SEND_RETRY_BACKOFF_MIN: Duration = Duration::from_millis(25);
81const SEND_RETRY_BACKOFF_MAX: Duration = Duration::from_millis(1000);
82
83/// Default announce-rate controls applied to transport-node interfaces.
84#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct AnnounceRateDefaults {
86    pub target: Option<f64>,
87    pub penalty: f64,
88    pub grace: u32,
89}
90
91impl Default for AnnounceRateDefaults {
92    fn default() -> Self {
93        Self {
94            target: Some(3600.0),
95            penalty: 0.0,
96            grace: 5,
97        }
98    }
99}
100
101mod dispatch;
102mod events;
103mod lifecycle;
104mod queries;
105mod runtime_config;
106
107#[cfg(test)]
108mod tests;
109
110fn inject_transport_header(raw: &[u8], next_hop: &[u8; 16]) -> Vec<u8> {
111    if raw.len() < 18 {
112        return raw.to_vec();
113    }
114
115    let new_flags = (rns_core::constants::HEADER_2 << 6)
116        | (rns_core::constants::TRANSPORT_TRANSPORT << 4)
117        | (raw[0] & 0x0F);
118
119    let mut new_raw = Vec::with_capacity(raw.len() + 16);
120    new_raw.push(new_flags);
121    new_raw.push(raw[1]);
122    new_raw.extend_from_slice(next_hop);
123    new_raw.extend_from_slice(&raw[2..]);
124    new_raw
125}
126
127fn recover_mutex_guard<'a, T>(mutex: &'a Mutex<T>, label: &str) -> std::sync::MutexGuard<'a, T> {
128    match mutex.lock() {
129        Ok(guard) => guard,
130        Err(poisoned) => {
131            log::error!("recovering from poisoned mutex: {}", label);
132            poisoned.into_inner()
133        }
134    }
135}
136
137#[derive(Debug, Clone, Copy)]
138pub(crate) struct RuntimeConfigDefaults {
139    pub(crate) tick_interval_ms: u64,
140    pub(crate) known_destinations_ttl: f64,
141    pub(crate) rate_limiter_ttl_secs: f64,
142    pub(crate) known_destinations_cleanup_interval_ticks: u32,
143    pub(crate) announce_cache_cleanup_interval_ticks: u32,
144    pub(crate) announce_cache_cleanup_batch_size: usize,
145    pub(crate) discovery_cleanup_interval_ticks: u32,
146    pub(crate) management_announce_interval_secs: f64,
147    pub(crate) direct_connect_policy: crate::event::HolePunchPolicy,
148    #[cfg(feature = "hooks")]
149    pub(crate) provider_queue_max_events: usize,
150    #[cfg(feature = "hooks")]
151    pub(crate) provider_queue_max_bytes: usize,
152}
153
154#[cfg(feature = "iface-backbone")]
155#[derive(Debug, Clone)]
156pub(crate) struct BackboneDiscoveryRuntime {
157    pub(crate) discoverable: bool,
158    pub(crate) config: crate::discovery::DiscoveryConfig,
159    pub(crate) transport_enabled: bool,
160    pub(crate) ifac_netname: Option<String>,
161    pub(crate) ifac_netkey: Option<String>,
162}
163
164#[cfg(feature = "iface-backbone")]
165#[derive(Debug, Clone)]
166pub(crate) struct BackboneDiscoveryRuntimeHandle {
167    pub(crate) interface_name: String,
168    pub(crate) current: BackboneDiscoveryRuntime,
169    pub(crate) startup: BackboneDiscoveryRuntime,
170}
171
172#[cfg(feature = "iface-tcp")]
173#[derive(Debug, Clone)]
174pub(crate) struct TcpServerDiscoveryRuntime {
175    pub(crate) discoverable: bool,
176    pub(crate) config: crate::discovery::DiscoveryConfig,
177    pub(crate) transport_enabled: bool,
178    pub(crate) ifac_netname: Option<String>,
179    pub(crate) ifac_netkey: Option<String>,
180}
181
182#[cfg(feature = "iface-tcp")]
183#[derive(Debug, Clone)]
184pub(crate) struct TcpServerDiscoveryRuntimeHandle {
185    pub(crate) interface_name: String,
186    pub(crate) current: TcpServerDiscoveryRuntime,
187    pub(crate) startup: TcpServerDiscoveryRuntime,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub(crate) struct IfacRuntimeConfig {
192    pub(crate) netname: Option<String>,
193    pub(crate) netkey: Option<String>,
194    pub(crate) size: usize,
195}
196
197#[cfg(feature = "iface-backbone")]
198impl BackboneDiscoveryRuntimeHandle {
199    pub(crate) fn from_parts(
200        interface_name: String,
201        startup_config: crate::discovery::DiscoveryConfig,
202        transport_enabled: bool,
203        ifac_netname: Option<String>,
204        ifac_netkey: Option<String>,
205        discoverable: bool,
206    ) -> Self {
207        let startup = BackboneDiscoveryRuntime {
208            discoverable,
209            config: startup_config,
210            transport_enabled,
211            ifac_netname,
212            ifac_netkey,
213        };
214        Self {
215            interface_name,
216            current: startup.clone(),
217            startup,
218        }
219    }
220}
221
222#[cfg(feature = "iface-tcp")]
223impl TcpServerDiscoveryRuntimeHandle {
224    pub(crate) fn from_parts(
225        interface_name: String,
226        startup_config: crate::discovery::DiscoveryConfig,
227        transport_enabled: bool,
228        ifac_netname: Option<String>,
229        ifac_netkey: Option<String>,
230        discoverable: bool,
231    ) -> Self {
232        let startup = TcpServerDiscoveryRuntime {
233            discoverable,
234            config: startup_config,
235            transport_enabled,
236            ifac_netname,
237            ifac_netkey,
238        };
239        Self {
240            interface_name,
241            current: startup.clone(),
242            startup,
243        }
244    }
245}
246
247impl IfacRuntimeConfig {
248    pub(crate) fn from_parts(netname: Option<String>, netkey: Option<String>, size: usize) -> Self {
249        Self {
250            netname,
251            netkey,
252            size,
253        }
254    }
255}
256
257#[cfg(feature = "iface-backbone")]
258#[derive(Debug, Clone)]
259pub struct BackbonePeerPoolSettings {
260    pub max_connected: usize,
261    pub failure_threshold: usize,
262    pub failure_window: Duration,
263    pub cooldown: Duration,
264}
265
266#[cfg(feature = "iface-backbone")]
267pub(crate) const BACKBONE_PEER_POOL_CONFIGURED_DEFAULT_PRIORITY: u8 = 60;
268#[cfg(feature = "iface-backbone")]
269pub(crate) const BACKBONE_PEER_POOL_DISCOVERED_PRIORITY: u8 = 40;
270
271#[cfg(feature = "iface-backbone")]
272pub(crate) struct BackbonePeerPoolCandidateConfig {
273    pub(crate) client: BackboneClientConfig,
274    pub(crate) mode: u8,
275    pub(crate) gravity: i64,
276    pub(crate) recursive_prs: bool,
277    pub(crate) announces_from_internal: bool,
278    pub(crate) announces_to_internal: Option<bool>,
279    pub(crate) ingress_control: rns_core::transport::types::IngressControlConfig,
280    pub(crate) ifac_runtime: IfacRuntimeConfig,
281    pub(crate) ifac_enabled: bool,
282    pub(crate) interface_type_name: String,
283    pub(crate) source: BackbonePeerPoolCandidateSource,
284    pub(crate) priority: u8,
285    pub(crate) discovery: Option<BackbonePeerPoolDiscoveryCandidate>,
286}
287
288#[cfg(feature = "iface-backbone")]
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub(crate) enum BackbonePeerPoolCandidateSource {
291    Configured,
292    Discovered,
293}
294
295#[cfg(feature = "iface-backbone")]
296impl BackbonePeerPoolCandidateSource {
297    fn as_str(self) -> &'static str {
298        match self {
299            Self::Configured => "configured",
300            Self::Discovered => "discovered",
301        }
302    }
303}
304
305#[cfg(feature = "iface-backbone")]
306#[derive(Debug, Clone)]
307pub(crate) struct BackbonePeerPoolDiscoveryCandidate {
308    pub(crate) discovery_hash: [u8; 32],
309    pub(crate) status: crate::discovery::DiscoveredStatus,
310    pub(crate) hops: u8,
311    pub(crate) stamp_value: u32,
312    pub(crate) last_heard: f64,
313}
314
315#[cfg(feature = "iface-backbone")]
316struct BackbonePeerPool {
317    settings: BackbonePeerPoolSettings,
318    candidates: Vec<BackbonePeerPoolCandidate>,
319}
320
321#[cfg(feature = "iface-backbone")]
322struct BackbonePeerPoolCandidate {
323    config: BackbonePeerPoolCandidateConfig,
324    active_id: Option<InterfaceId>,
325    failures: Vec<f64>,
326    retry_after: Option<f64>,
327    cooldown_until: Option<f64>,
328    last_error: Option<String>,
329}
330
331/// Thin wrapper providing `EngineAccess` for a `TransportEngine` + Driver interfaces.
332#[cfg(feature = "hooks")]
333struct EngineRef<'a> {
334    engine: &'a TransportEngine,
335    interfaces: &'a HashMap<InterfaceId, InterfaceEntry>,
336    link_manager: &'a LinkManager,
337    now: f64,
338}
339
340#[cfg(feature = "hooks")]
341impl<'a> EngineAccess for EngineRef<'a> {
342    fn has_path(&self, dest: &[u8; 16]) -> bool {
343        self.engine.has_path(dest)
344    }
345    fn hops_to(&self, dest: &[u8; 16]) -> Option<u8> {
346        self.engine.hops_to(dest)
347    }
348    fn next_hop(&self, dest: &[u8; 16]) -> Option<[u8; 16]> {
349        self.engine.next_hop(dest)
350    }
351    fn is_blackholed(&self, identity: &[u8; 16]) -> bool {
352        self.engine.is_blackholed(identity, self.now)
353    }
354    fn interface_name(&self, id: u64) -> Option<String> {
355        self.interfaces
356            .get(&InterfaceId(id))
357            .map(|e| e.info.name.clone())
358    }
359    fn interface_mode(&self, id: u64) -> Option<u8> {
360        self.interfaces.get(&InterfaceId(id)).map(|e| e.info.mode)
361    }
362    fn identity_hash(&self) -> Option<[u8; 16]> {
363        self.engine.identity_hash().copied()
364    }
365    fn announce_rate(&self, id: u64) -> Option<i32> {
366        self.interfaces
367            .get(&InterfaceId(id))
368            .map(|e| (e.stats.outgoing_announce_freq() * 1000.0) as i32)
369    }
370    fn link_state(&self, link_hash: &[u8; 16]) -> Option<u8> {
371        use rns_core::link::types::LinkState;
372        self.link_manager.link_state(link_hash).map(|s| match s {
373            LinkState::Pending => 0,
374            LinkState::Handshake => 1,
375            LinkState::Active => 2,
376            LinkState::Stale => 3,
377            LinkState::Closed => 4,
378        })
379    }
380}
381
382/// Extract the 16-byte destination hash from a raw packet header.
383///
384/// HEADER_1 (raw[0] & 0x40 == 0): dest at bytes 2..18
385/// HEADER_2 (raw[0] & 0x40 != 0): dest at bytes 18..34 (after transport ID)
386#[cfg(any(test, feature = "hooks"))]
387fn extract_dest_hash(raw: &[u8]) -> [u8; 16] {
388    let mut dest = [0u8; 16];
389    if raw.is_empty() {
390        return dest;
391    }
392    let is_header2 = raw[0] & 0x40 != 0;
393    let start = if is_header2 { 18 } else { 2 };
394    let end = start + 16;
395    if raw.len() >= end {
396        dest.copy_from_slice(&raw[start..end]);
397    }
398    dest
399}
400
401/// Execute a hook chain on disjoint Driver fields (avoids &mut self borrow conflict).
402#[cfg(feature = "hooks")]
403fn run_hook_inner(
404    programs: &mut [rns_hooks::LoadedProgram],
405    hook_manager: &Option<HookManager>,
406    engine_access: &dyn EngineAccess,
407    ctx: &HookContext,
408    now: f64,
409    provider_events_enabled: bool,
410) -> Option<rns_hooks::ExecuteResult> {
411    if programs.is_empty() {
412        return None;
413    }
414    let mgr = hook_manager.as_ref()?;
415    mgr.run_chain_with_provider_events(programs, ctx, engine_access, now, provider_events_enabled)
416}
417
418#[cfg(feature = "hooks")]
419fn backbone_peer_hook_context(event: &BackbonePeerHookEvent) -> HookContext<'_> {
420    HookContext::BackbonePeer {
421        server_interface_id: event.server_interface_id.0,
422        peer_interface_id: event.peer_interface_id.map(|id| id.0),
423        peer_ip: event.peer_ip,
424        peer_port: event.peer_port,
425        connected_for: event.connected_for,
426        had_received_data: event.had_received_data,
427        penalty_level: event.penalty_level,
428        blacklist_for: event.blacklist_for,
429    }
430}
431
432/// Convert a Vec of ActionWire into TransportActions for dispatch.
433#[cfg(feature = "hooks")]
434fn convert_injected_actions(actions: Vec<rns_hooks::ActionWire>) -> Vec<TransportAction> {
435    actions
436        .into_iter()
437        .map(|a| {
438            use rns_hooks::ActionWire;
439            match a {
440                ActionWire::SendOnInterface { interface, raw } => {
441                    TransportAction::SendOnInterface {
442                        interface: InterfaceId(interface),
443                        raw: raw.into(),
444                    }
445                }
446                ActionWire::BroadcastOnAllInterfaces {
447                    raw,
448                    exclude,
449                    has_exclude,
450                } => TransportAction::BroadcastOnAllInterfaces {
451                    raw: raw.into(),
452                    exclude: if has_exclude != 0 {
453                        Some(InterfaceId(exclude))
454                    } else {
455                        None
456                    },
457                },
458                ActionWire::DeliverLocal {
459                    destination_hash,
460                    raw,
461                    packet_hash,
462                    receiving_interface,
463                } => TransportAction::DeliverLocal {
464                    destination_hash,
465                    raw: raw.into(),
466                    packet_hash,
467                    receiving_interface: InterfaceId(receiving_interface),
468                },
469                ActionWire::PathUpdated {
470                    destination_hash,
471                    hops,
472                    next_hop,
473                    interface,
474                } => TransportAction::PathUpdated {
475                    destination_hash,
476                    hops,
477                    next_hop,
478                    interface: InterfaceId(interface),
479                },
480                ActionWire::CacheAnnounce { packet_hash, raw } => TransportAction::CacheAnnounce {
481                    packet_hash,
482                    raw: raw.into(),
483                },
484                ActionWire::TunnelEstablished {
485                    tunnel_id,
486                    interface,
487                } => TransportAction::TunnelEstablished {
488                    tunnel_id,
489                    interface: InterfaceId(interface),
490                },
491                ActionWire::TunnelSynthesize {
492                    interface,
493                    data,
494                    dest_hash,
495                } => TransportAction::TunnelSynthesize {
496                    interface: InterfaceId(interface),
497                    data,
498                    dest_hash,
499                },
500                ActionWire::ForwardToLocalClients {
501                    raw,
502                    exclude,
503                    has_exclude,
504                } => TransportAction::ForwardToLocalClients {
505                    raw: raw.into(),
506                    exclude: if has_exclude != 0 {
507                        Some(InterfaceId(exclude))
508                    } else {
509                        None
510                    },
511                },
512                ActionWire::ForwardPlainBroadcast {
513                    raw,
514                    to_local,
515                    exclude,
516                    has_exclude,
517                } => TransportAction::ForwardPlainBroadcast {
518                    raw: raw.into(),
519                    to_local: to_local != 0,
520                    exclude: if has_exclude != 0 {
521                        Some(InterfaceId(exclude))
522                    } else {
523                        None
524                    },
525                },
526                ActionWire::AnnounceReceived {
527                    destination_hash,
528                    identity_hash,
529                    public_key,
530                    name_hash,
531                    random_hash,
532                    app_data,
533                    hops,
534                    receiving_interface,
535                } => TransportAction::AnnounceReceived {
536                    destination_hash,
537                    identity_hash,
538                    public_key,
539                    name_hash,
540                    random_hash,
541                    ratchet: None,
542                    app_data,
543                    hops,
544                    receiving_interface: InterfaceId(receiving_interface),
545                    rx: rns_core::transport::RxMetadata::default(),
546                },
547            }
548        })
549        .collect()
550}
551
552/// Infer the interface type string from a dynamic interface's name.
553/// Dynamic interfaces (TCP server clients, backbone peers, auto peers, local server clients)
554/// include their type in the name prefix set at construction.
555fn infer_interface_type(name: &str) -> String {
556    if name.starts_with("TCPServerInterface") {
557        "TCPServerClientInterface".to_string()
558    } else if name.starts_with("BackboneInterface") {
559        "BackboneInterface".to_string()
560    } else if name.starts_with("LocalInterface") {
561        "LocalServerClientInterface".to_string()
562    } else if name.starts_with("WeaveInterfacePeer[") {
563        "WeaveInterfacePeer".to_string()
564    } else {
565        // AutoInterface peers use "{group_name}:{peer_addr}" format where
566        // group_name is the config section name (typically "AutoInterface" or similar).
567        "AutoInterface".to_string()
568    }
569}
570
571pub use crate::common::callbacks::Callbacks;
572
573#[derive(Clone)]
574struct SharedAnnounceRecord {
575    name_hash: [u8; 10],
576    identity_prv_key: [u8; 64],
577    app_data: Option<Vec<u8>>,
578}
579
580#[derive(Debug, Clone)]
581pub(crate) struct KnownDestinationState {
582    announced: crate::destination::AnnouncedIdentity,
583    was_used: bool,
584    last_used_at: Option<f64>,
585    retained: bool,
586}
587
588struct KnownDestinationCleanup {
589    candidates: Vec<[u8; 16]>,
590    cursor: usize,
591    started_at: Instant,
592    removed: usize,
593}
594
595/// The driver loop. Owns the engine and all interface entries.
596pub struct Driver {
597    pub(crate) engine: TransportEngine,
598    pub(crate) interfaces: HashMap<InterfaceId, InterfaceEntry>,
599    pub(crate) rng: OsRng,
600    pub(crate) rx: EventReceiver,
601    pub(crate) callbacks: Box<dyn Callbacks>,
602    pub(crate) started: f64,
603    pub(crate) lifecycle_state: LifecycleState,
604    pub(crate) drain_started_at: Option<Instant>,
605    pub(crate) drain_deadline: Option<Instant>,
606    pub(crate) listener_controls: Vec<crate::interface::ListenerControl>,
607    pub(crate) announce_cache: Option<crate::announce_cache::AnnounceCache>,
608    /// Destination hash for rnstransport.tunnel.synthesize (PLAIN).
609    pub(crate) tunnel_synth_dest: [u8; 16],
610    /// Transport identity (optional, needed for tunnel synthesis).
611    pub(crate) transport_identity: Option<rns_crypto::identity::Identity>,
612    /// Link manager: handles link lifecycle, request/response.
613    pub(crate) link_manager: LinkManager,
614    /// Management configuration for ACL checks.
615    pub(crate) management_config: crate::management::ManagementConfig,
616    /// Last time management announces were emitted.
617    pub(crate) last_management_announce: f64,
618    /// Whether initial management announce has been sent (delayed 5s after start).
619    pub(crate) initial_announce_sent: bool,
620    /// Cache of known announced identities and lifecycle state, keyed by destination hash.
621    pub(crate) known_destinations: HashMap<[u8; 16], KnownDestinationState>,
622    /// Store for received remote ratchets, if persistence/use is enabled.
623    pub(crate) ratchet_store: Option<Arc<dyn crate::storage::RatchetStore>>,
624    /// TTL for received ratchets, in seconds.
625    pub(crate) ratchet_expiry_secs: f64,
626    /// TTL for known destinations without an active path, in seconds.
627    pub(crate) known_destinations_ttl: f64,
628    /// Maximum number of retained known destinations.
629    pub(crate) known_destinations_max_entries: usize,
630    /// TTL for announce rate-limiter entries without an active path, in seconds.
631    pub(crate) rate_limiter_ttl_secs: f64,
632    /// Destination hash for rnstransport.path.request (PLAIN).
633    pub(crate) path_request_dest: [u8; 16],
634    /// Proof strategies per destination hash.
635    /// Maps dest_hash → (strategy, optional signing identity for generating proofs).
636    pub(crate) proof_strategies: HashMap<
637        [u8; 16],
638        (
639            rns_core::types::ProofStrategy,
640            Option<rns_crypto::identity::Identity>,
641        ),
642    >,
643    /// Tracked sent packets for proof matching: packet_hash → (dest_hash, sent_time).
644    pub(crate) sent_packets: HashMap<[u8; 32], ([u8; 16], f64)>,
645    /// Completed proofs for probe polling: packet_hash → (rtt_seconds, received_time).
646    pub(crate) completed_proofs: HashMap<[u8; 32], (f64, f64)>,
647    /// Locally registered destinations: hash → dest_type.
648    pub(crate) local_destinations: HashMap<[u8; 16], u8>,
649    /// Latest explicit SINGLE announces to replay after shared-client reconnect.
650    shared_announces: HashMap<[u8; 16], SharedAnnounceRecord>,
651    /// Shared local interfaces that went down and should replay announces on reconnect.
652    shared_reconnect_pending: HashMap<InterfaceId, bool>,
653    /// Hole-punch manager for direct P2P connections.
654    pub(crate) holepunch_manager: HolePunchManager,
655    /// Event sender for worker threads to send results back to the driver loop.
656    pub(crate) event_tx: crate::event::EventSender,
657    /// Maximum queued outbound frames per interface writer worker.
658    pub(crate) interface_writer_queue_capacity: usize,
659    /// Default announce-rate controls for interfaces on transport nodes.
660    pub(crate) announce_rate_defaults: AnnounceRateDefaults,
661    /// Default ingress/egress controls for interfaces on transport nodes.
662    pub(crate) ingress_control_defaults: rns_core::transport::types::IngressControlConfig,
663    /// Shared timer interval used by the node timer thread.
664    pub(crate) tick_interval_ms: Arc<AtomicU64>,
665    /// Runtime-config handles for backbone server interfaces, keyed by config name.
666    #[cfg(feature = "iface-backbone")]
667    pub(crate) backbone_runtime: HashMap<String, BackboneRuntimeConfigHandle>,
668    /// Live peer-state handles for backbone server interfaces, keyed by config name.
669    #[cfg(feature = "iface-backbone")]
670    pub(crate) backbone_peer_state: HashMap<String, BackbonePeerStateHandle>,
671    /// Runtime-config handles for backbone client interfaces, keyed by config name.
672    #[cfg(feature = "iface-backbone")]
673    pub(crate) backbone_client_runtime: HashMap<String, BackboneClientRuntimeConfigHandle>,
674    /// Runtime-config state for backbone discovery metadata, keyed by config name.
675    #[cfg(feature = "iface-backbone")]
676    pub(crate) backbone_discovery_runtime: HashMap<String, BackboneDiscoveryRuntimeHandle>,
677    /// Ordered outbound Backbone peer pool, if enabled.
678    #[cfg(feature = "iface-backbone")]
679    backbone_peer_pool: Option<BackbonePeerPool>,
680    /// Shared allocator for listener-created and discovery-created dynamic interfaces.
681    #[cfg(feature = "iface-backbone")]
682    pub(crate) next_dynamic_interface_id: Arc<AtomicU64>,
683    /// Runtime-config handles for TCP server interfaces, keyed by config name.
684    #[cfg(feature = "iface-tcp")]
685    pub(crate) tcp_server_runtime: HashMap<String, TcpServerRuntimeConfigHandle>,
686    /// Runtime-config handles for TCP client interfaces, keyed by config name.
687    #[cfg(feature = "iface-tcp")]
688    pub(crate) tcp_client_runtime: HashMap<String, TcpClientRuntimeConfigHandle>,
689    /// Runtime-config state for TCP server discovery metadata, keyed by config name.
690    #[cfg(feature = "iface-tcp")]
691    pub(crate) tcp_server_discovery_runtime: HashMap<String, TcpServerDiscoveryRuntimeHandle>,
692    /// Runtime-config handles for UDP interfaces, keyed by config name.
693    #[cfg(feature = "iface-udp")]
694    pub(crate) udp_runtime: HashMap<String, UdpRuntimeConfigHandle>,
695    /// Runtime-config handles for Auto interfaces, keyed by config name.
696    #[cfg(feature = "iface-auto")]
697    pub(crate) auto_runtime: HashMap<String, AutoRuntimeConfigHandle>,
698    /// Runtime-config handles for I2P interfaces, keyed by config name.
699    #[cfg(feature = "iface-i2p")]
700    pub(crate) i2p_runtime: HashMap<String, I2pRuntimeConfigHandle>,
701    /// Runtime-config handles for Pipe interfaces, keyed by config name.
702    #[cfg(feature = "iface-pipe")]
703    pub(crate) pipe_runtime: HashMap<String, PipeRuntimeConfigHandle>,
704    /// Runtime-config handles for RNode interfaces, keyed by config name.
705    #[cfg(feature = "iface-rnode")]
706    pub(crate) rnode_runtime: HashMap<String, RNodeRuntimeConfigHandle>,
707    /// Startup/default interface metadata for generic cross-cutting runtime config.
708    pub(crate) interface_runtime_defaults:
709        HashMap<String, rns_core::transport::types::InterfaceInfo>,
710    /// Current IFAC runtime config for static interfaces that support IFAC mutation.
711    pub(crate) interface_ifac_runtime: HashMap<String, IfacRuntimeConfig>,
712    /// Startup/default IFAC runtime config for static interfaces.
713    pub(crate) interface_ifac_runtime_defaults: HashMap<String, IfacRuntimeConfig>,
714    /// Storage for discovered interfaces.
715    pub(crate) discovered_interfaces: crate::discovery::DiscoveredInterfaceStorage,
716    /// Required stamp value for accepting discovered interfaces.
717    pub(crate) discovery_required_value: u8,
718    /// Bounded cache of discovery payloads with previously rejected stamps.
719    pub(crate) discovery_stamp_cache: crate::discovery::DiscoveryStampCache,
720    /// Name hash for interface discovery announces ("rnstransport.discovery.interface").
721    pub(crate) discovery_name_hash: [u8; 10],
722    /// Destination hash for the probe responder (if respond_to_probes is enabled).
723    pub(crate) probe_responder_hash: Option<[u8; 16]>,
724    /// Whether interface discovery is enabled.
725    pub(crate) discover_interfaces: bool,
726    /// Explicit mode for interfaces created from discovery announces.
727    pub(crate) autoconnect_interface_mode: Option<u8>,
728    /// Gravity for interfaces created from discovery announces.
729    pub(crate) autoconnect_interface_gravity: i64,
730    /// Internal announce propagation override for discovered interfaces.
731    pub(crate) autoconnect_announces_to_internal: Option<bool>,
732    /// Announcer for discoverable interfaces (None if nothing to announce).
733    pub(crate) interface_announcer: Option<crate::discovery::InterfaceAnnouncer>,
734    /// Shared async announce verification queue.
735    pub(crate) announce_verify_queue: Arc<Mutex<AnnounceVerifyQueue>>,
736    /// Whether inbound announces should be verified off the driver thread.
737    pub(crate) async_announce_verification: bool,
738    /// Tick counter for periodic discovery cleanup (every ~3600 ticks = ~1 hour).
739    pub(crate) discovery_cleanup_counter: u32,
740    /// Runtime-configurable discovery cleanup interval.
741    pub(crate) discovery_cleanup_interval_ticks: u32,
742    /// Tick counter for periodic MEMSTATS logging (every 300 ticks = ~5 min).
743    pub(crate) memory_stats_counter: u32,
744    /// Tick counter for periodic memory/cache cleanup (every ~3600 ticks = ~1 hour).
745    pub(crate) cache_cleanup_counter: u32,
746    /// Tick counter for incremental announce-cache cleanup scheduling.
747    pub(crate) announce_cache_cleanup_counter: u32,
748    /// Runtime-configurable cleanup interval for known destinations.
749    pub(crate) known_destinations_cleanup_interval_ticks: u32,
750    /// Incremental known-destination cleanup pass, if one is active.
751    known_destination_cleanup: Option<KnownDestinationCleanup>,
752    /// Low-priority ratchet filesystem cleanup associated with the last pass.
753    ratchet_cleanup_handle: Option<std::thread::JoinHandle<()>>,
754    /// Count of known-destination cap evictions since start.
755    pub(crate) known_destinations_cap_evict_count: usize,
756    /// Runtime-configurable interval for starting announce cache cleanup.
757    pub(crate) announce_cache_cleanup_interval_ticks: u32,
758    /// When set, announce cache cleanup is in progress (contains active packet hashes).
759    pub(crate) cache_cleanup_active_hashes: Option<Vec<[u8; 32]>>,
760    /// Directory iterator for incremental announce cache cleanup.
761    pub(crate) cache_cleanup_entries: Option<std::fs::ReadDir>,
762    /// Running total of files removed during current cache cleanup cycle.
763    pub(crate) cache_cleanup_removed: usize,
764    /// Runtime-configurable announce cache cleanup batch size.
765    pub(crate) announce_cache_cleanup_batch_size: usize,
766    /// Runtime-configurable management announce interval.
767    pub(crate) management_announce_interval_secs: f64,
768    /// Startup/default runtime-config values.
769    pub(crate) runtime_config_defaults: RuntimeConfigDefaults,
770    /// Hook slots for the programmable hook system (one per HookPoint).
771    #[cfg(feature = "hooks")]
772    pub(crate) hook_slots: [HookSlot; HookPoint::COUNT],
773    /// Hook manager. None if initialization failed.
774    #[cfg(feature = "hooks")]
775    pub(crate) hook_manager: Option<HookManager>,
776    #[cfg(feature = "hooks")]
777    pub(crate) provider_bridge: Option<ProviderBridge>,
778}
779
780impl Driver {
781    /// Create a new driver.
782    pub fn new(
783        config: TransportConfig,
784        rx: EventReceiver,
785        tx: crate::event::EventSender,
786        callbacks: Box<dyn Callbacks>,
787    ) -> Self {
788        let announce_queue_max_entries = config.announce_queue_max_entries;
789        let tunnel_synth_dest = rns_core::destination::destination_hash(
790            "rnstransport",
791            &["tunnel", "synthesize"],
792            None,
793        );
794        let path_request_dest =
795            rns_core::destination::destination_hash("rnstransport", &["path", "request"], None);
796        let discovery_name_hash = crate::discovery::discovery_name_hash();
797        let mut engine = TransportEngine::new(config);
798        engine.register_destination(tunnel_synth_dest, rns_core::constants::DESTINATION_PLAIN);
799        // Register path request destination so inbound path requests are delivered locally
800        engine.register_destination(path_request_dest, rns_core::constants::DESTINATION_PLAIN);
801        // Note: discovery destination is NOT registered as local — it's a SINGLE destination
802        // whose hash depends on the sender's identity. We match it by name_hash instead.
803        let mut local_destinations = HashMap::new();
804        local_destinations.insert(tunnel_synth_dest, rns_core::constants::DESTINATION_PLAIN);
805        local_destinations.insert(path_request_dest, rns_core::constants::DESTINATION_PLAIN);
806        let runtime_config_defaults = RuntimeConfigDefaults {
807            tick_interval_ms: DEFAULT_TICK_INTERVAL_MS,
808            known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
809            rate_limiter_ttl_secs: DEFAULT_RATE_LIMITER_TTL_SECS,
810            known_destinations_cleanup_interval_ticks:
811                DEFAULT_KNOWN_DESTINATIONS_CLEANUP_INTERVAL_TICKS,
812            announce_cache_cleanup_interval_ticks: DEFAULT_ANNOUNCE_CACHE_CLEANUP_INTERVAL_TICKS,
813            announce_cache_cleanup_batch_size: DEFAULT_ANNOUNCE_CACHE_CLEANUP_BATCH_SIZE,
814            discovery_cleanup_interval_ticks: DEFAULT_DISCOVERY_CLEANUP_INTERVAL_TICKS,
815            management_announce_interval_secs: DEFAULT_MANAGEMENT_ANNOUNCE_INTERVAL_SECS,
816            direct_connect_policy: crate::event::HolePunchPolicy::default(),
817            #[cfg(feature = "hooks")]
818            provider_queue_max_events: crate::provider_bridge::ProviderBridgeConfig::default()
819                .queue_max_events,
820            #[cfg(feature = "hooks")]
821            provider_queue_max_bytes: crate::provider_bridge::ProviderBridgeConfig::default()
822                .queue_max_bytes,
823        };
824        Driver {
825            engine,
826            interfaces: HashMap::new(),
827            rng: OsRng,
828            rx,
829            callbacks,
830            started: time::now(),
831            lifecycle_state: LifecycleState::Active,
832            drain_started_at: None,
833            drain_deadline: None,
834            listener_controls: Vec::new(),
835            announce_cache: None,
836            tunnel_synth_dest,
837            transport_identity: None,
838            link_manager: LinkManager::new(),
839            management_config: Default::default(),
840            last_management_announce: 0.0,
841            initial_announce_sent: false,
842            known_destinations: HashMap::new(),
843            ratchet_store: None,
844            ratchet_expiry_secs: rns_core::constants::RATCHET_EXPIRY as f64,
845            known_destinations_ttl: DEFAULT_KNOWN_DESTINATIONS_TTL,
846            known_destinations_max_entries: DEFAULT_KNOWN_DESTINATIONS_MAX_ENTRIES,
847            rate_limiter_ttl_secs: DEFAULT_RATE_LIMITER_TTL_SECS,
848            path_request_dest,
849            proof_strategies: HashMap::new(),
850            sent_packets: HashMap::new(),
851            completed_proofs: HashMap::new(),
852            local_destinations,
853            shared_announces: HashMap::new(),
854            shared_reconnect_pending: HashMap::new(),
855            holepunch_manager: HolePunchManager::new(
856                vec![],
857                rns_core::holepunch::ProbeProtocol::Rnsp,
858                None,
859            ),
860            event_tx: tx,
861            interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
862            announce_rate_defaults: AnnounceRateDefaults::default(),
863            ingress_control_defaults: rns_core::transport::types::IngressControlConfig::enabled(),
864            tick_interval_ms: Arc::new(AtomicU64::new(DEFAULT_TICK_INTERVAL_MS)),
865            #[cfg(feature = "iface-backbone")]
866            backbone_runtime: HashMap::new(),
867            #[cfg(feature = "iface-backbone")]
868            backbone_peer_state: HashMap::new(),
869            #[cfg(feature = "iface-backbone")]
870            backbone_client_runtime: HashMap::new(),
871            #[cfg(feature = "iface-backbone")]
872            backbone_discovery_runtime: HashMap::new(),
873            #[cfg(feature = "iface-backbone")]
874            backbone_peer_pool: None,
875            #[cfg(feature = "iface-backbone")]
876            next_dynamic_interface_id: Arc::new(AtomicU64::new(10000)),
877            #[cfg(feature = "iface-tcp")]
878            tcp_server_runtime: HashMap::new(),
879            #[cfg(feature = "iface-tcp")]
880            tcp_client_runtime: HashMap::new(),
881            #[cfg(feature = "iface-tcp")]
882            tcp_server_discovery_runtime: HashMap::new(),
883            #[cfg(feature = "iface-udp")]
884            udp_runtime: HashMap::new(),
885            #[cfg(feature = "iface-auto")]
886            auto_runtime: HashMap::new(),
887            #[cfg(feature = "iface-i2p")]
888            i2p_runtime: HashMap::new(),
889            #[cfg(feature = "iface-pipe")]
890            pipe_runtime: HashMap::new(),
891            #[cfg(feature = "iface-rnode")]
892            rnode_runtime: HashMap::new(),
893            interface_runtime_defaults: HashMap::new(),
894            interface_ifac_runtime: HashMap::new(),
895            interface_ifac_runtime_defaults: HashMap::new(),
896            discovered_interfaces: crate::discovery::DiscoveredInterfaceStorage::new(
897                std::env::temp_dir().join("rns-discovered-interfaces"),
898            ),
899            discovery_required_value: crate::discovery::DEFAULT_STAMP_VALUE,
900            discovery_stamp_cache: crate::discovery::DiscoveryStampCache::default(),
901            discovery_name_hash,
902            probe_responder_hash: None,
903            discover_interfaces: false,
904            autoconnect_interface_mode: None,
905            autoconnect_interface_gravity: 0,
906            autoconnect_announces_to_internal: None,
907            interface_announcer: None,
908            announce_verify_queue: Arc::new(Mutex::new(AnnounceVerifyQueue::new(
909                announce_queue_max_entries,
910            ))),
911            async_announce_verification: false,
912            discovery_cleanup_counter: 0,
913            discovery_cleanup_interval_ticks: runtime_config_defaults
914                .discovery_cleanup_interval_ticks,
915            memory_stats_counter: 0,
916            cache_cleanup_counter: 0,
917            announce_cache_cleanup_counter: 0,
918            known_destinations_cleanup_interval_ticks: runtime_config_defaults
919                .known_destinations_cleanup_interval_ticks,
920            known_destination_cleanup: None,
921            ratchet_cleanup_handle: None,
922            known_destinations_cap_evict_count: 0,
923            announce_cache_cleanup_interval_ticks: runtime_config_defaults
924                .announce_cache_cleanup_interval_ticks,
925            cache_cleanup_active_hashes: None,
926            cache_cleanup_entries: None,
927            cache_cleanup_removed: 0,
928            announce_cache_cleanup_batch_size: runtime_config_defaults
929                .announce_cache_cleanup_batch_size,
930            management_announce_interval_secs: runtime_config_defaults
931                .management_announce_interval_secs,
932            runtime_config_defaults,
933            #[cfg(feature = "hooks")]
934            hook_slots: create_hook_slots(),
935            #[cfg(feature = "hooks")]
936            hook_manager: HookManager::new().ok(),
937            #[cfg(feature = "hooks")]
938            provider_bridge: None,
939        }
940    }
941
942    pub fn set_announce_verify_queue_config(
943        &mut self,
944        max_entries: usize,
945        max_bytes: usize,
946        max_stale_secs: f64,
947        overflow_policy: OverflowPolicy,
948    ) {
949        self.announce_verify_queue = Arc::new(Mutex::new(AnnounceVerifyQueue::with_limits(
950            max_entries,
951            max_bytes,
952            max_stale_secs,
953            overflow_policy,
954        )));
955    }
956
957    pub fn set_announce_rate_defaults(&mut self, defaults: AnnounceRateDefaults) {
958        self.announce_rate_defaults = defaults;
959    }
960
961    pub fn set_ingress_control_defaults(
962        &mut self,
963        defaults: rns_core::transport::types::IngressControlConfig,
964    ) {
965        self.ingress_control_defaults = defaults;
966    }
967
968    pub(crate) fn apply_announce_rate_defaults(
969        &self,
970        info: &mut rns_core::transport::types::InterfaceInfo,
971    ) {
972        if !self.engine.transport_enabled() || info.announce_rate_target.is_some() {
973            return;
974        }
975
976        let Some(target) = self.announce_rate_defaults.target else {
977            return;
978        };
979
980        info.announce_rate_target = Some(target);
981        if info.announce_rate_grace == 0 {
982            info.announce_rate_grace = self.announce_rate_defaults.grace;
983        }
984        if info.announce_rate_penalty == 0.0 {
985            info.announce_rate_penalty = self.announce_rate_defaults.penalty;
986        }
987    }
988
989    pub(crate) fn apply_ingress_control_defaults(
990        &self,
991        info: &mut rns_core::transport::types::InterfaceInfo,
992    ) {
993        if !self.engine.transport_enabled() {
994            return;
995        }
996
997        let baseline = if info.ingress_control.enabled {
998            rns_core::transport::types::IngressControlConfig::enabled()
999        } else {
1000            rns_core::transport::types::IngressControlConfig::disabled()
1001        };
1002        let defaults = self.ingress_control_defaults;
1003        let ic = &mut info.ingress_control;
1004
1005        if ic.max_held_announces == baseline.max_held_announces {
1006            ic.max_held_announces = defaults.max_held_announces;
1007        }
1008        if ic.burst_hold == baseline.burst_hold {
1009            ic.burst_hold = defaults.burst_hold;
1010        }
1011        if ic.burst_freq_new == baseline.burst_freq_new {
1012            ic.burst_freq_new = defaults.burst_freq_new;
1013        }
1014        if ic.burst_freq == baseline.burst_freq {
1015            ic.burst_freq = defaults.burst_freq;
1016        }
1017        if ic.pr_burst_freq_new == baseline.pr_burst_freq_new {
1018            ic.pr_burst_freq_new = defaults.pr_burst_freq_new;
1019        }
1020        if ic.pr_burst_freq == baseline.pr_burst_freq {
1021            ic.pr_burst_freq = defaults.pr_burst_freq;
1022        }
1023        if ic.new_time == baseline.new_time {
1024            ic.new_time = defaults.new_time;
1025        }
1026        if ic.burst_penalty == baseline.burst_penalty {
1027            ic.burst_penalty = defaults.burst_penalty;
1028        }
1029        if ic.held_release_interval == baseline.held_release_interval {
1030            ic.held_release_interval = defaults.held_release_interval;
1031        }
1032        if ic.egress_enabled == baseline.egress_enabled {
1033            ic.egress_enabled = defaults.egress_enabled;
1034        }
1035        if ic.egress_pr_freq == baseline.egress_pr_freq {
1036            ic.egress_pr_freq = defaults.egress_pr_freq;
1037        }
1038    }
1039
1040    fn wrap_interface_writer(
1041        &self,
1042        interface_id: InterfaceId,
1043        interface_name: &str,
1044        writer: Box<dyn crate::interface::Writer>,
1045    ) -> (
1046        Box<dyn crate::interface::Writer>,
1047        crate::interface::AsyncWriterMetrics,
1048    ) {
1049        crate::interface::wrap_async_writer(
1050            writer,
1051            interface_id,
1052            interface_name,
1053            self.event_tx.clone(),
1054            self.interface_writer_queue_capacity,
1055        )
1056    }
1057
1058    #[cfg(feature = "hooks")]
1059    fn provider_events_enabled(&self) -> bool {
1060        self.provider_bridge.is_some()
1061    }
1062
1063    #[cfg(feature = "hooks")]
1064    fn run_backbone_peer_hook(
1065        &mut self,
1066        attach_point: &str,
1067        point: HookPoint,
1068        event: &BackbonePeerHookEvent,
1069    ) {
1070        let ctx = backbone_peer_hook_context(event);
1071        let now = time::now();
1072        let engine_ref = EngineRef {
1073            engine: &self.engine,
1074            interfaces: &self.interfaces,
1075            link_manager: &self.link_manager,
1076            now,
1077        };
1078        let provider_events_enabled = self.provider_events_enabled();
1079        if let Some(ref e) = run_hook_inner(
1080            &mut self.hook_slots[point as usize].programs,
1081            &self.hook_manager,
1082            &engine_ref,
1083            &ctx,
1084            now,
1085            provider_events_enabled,
1086        ) {
1087            self.forward_hook_side_effects(attach_point, e);
1088        }
1089    }
1090
1091    #[cfg(feature = "iface-backbone")]
1092    fn make_discoverable_interface(
1093        runtime: &BackboneDiscoveryRuntimeHandle,
1094    ) -> crate::discovery::DiscoverableInterface {
1095        crate::discovery::DiscoverableInterface {
1096            interface_name: runtime.interface_name.clone(),
1097            config: runtime.current.config.clone(),
1098            transport_enabled: runtime.current.transport_enabled,
1099            ifac_netname: runtime.current.ifac_netname.clone(),
1100            ifac_netkey: runtime.current.ifac_netkey.clone(),
1101        }
1102    }
1103
1104    #[cfg(feature = "iface-backbone")]
1105    fn sync_backbone_discovery_runtime(
1106        &mut self,
1107        interface_name: &str,
1108    ) -> Result<(), RuntimeConfigError> {
1109        let handle = self
1110            .backbone_discovery_runtime
1111            .get(interface_name)
1112            .ok_or(RuntimeConfigError {
1113                code: RuntimeConfigErrorCode::NotFound,
1114                message: format!("backbone interface '{}' not found", interface_name),
1115            })?
1116            .clone();
1117
1118        if handle.current.discoverable {
1119            let iface = Self::make_discoverable_interface(&handle);
1120            if let Some(announcer) = self.interface_announcer.as_mut() {
1121                announcer.upsert_interface(iface);
1122            } else if let Some(identity) = self.transport_identity.as_ref() {
1123                self.interface_announcer = Some(crate::discovery::InterfaceAnnouncer::new(
1124                    *identity.hash(),
1125                    vec![iface],
1126                ));
1127            }
1128        } else if let Some(announcer) = self.interface_announcer.as_mut() {
1129            announcer.remove_interface(interface_name);
1130            if announcer.is_empty() {
1131                self.interface_announcer = None;
1132            }
1133        }
1134
1135        Ok(())
1136    }
1137
1138    #[cfg(feature = "iface-tcp")]
1139    fn make_tcp_server_discoverable_interface(
1140        runtime: &TcpServerDiscoveryRuntimeHandle,
1141    ) -> crate::discovery::DiscoverableInterface {
1142        crate::discovery::DiscoverableInterface {
1143            interface_name: runtime.interface_name.clone(),
1144            config: runtime.current.config.clone(),
1145            transport_enabled: runtime.current.transport_enabled,
1146            ifac_netname: runtime.current.ifac_netname.clone(),
1147            ifac_netkey: runtime.current.ifac_netkey.clone(),
1148        }
1149    }
1150
1151    #[cfg(feature = "iface-tcp")]
1152    fn sync_tcp_server_discovery_runtime(
1153        &mut self,
1154        interface_name: &str,
1155    ) -> Result<(), RuntimeConfigError> {
1156        let handle = self
1157            .tcp_server_discovery_runtime
1158            .get(interface_name)
1159            .ok_or(RuntimeConfigError {
1160                code: RuntimeConfigErrorCode::NotFound,
1161                message: format!("tcp server interface '{}' not found", interface_name),
1162            })?
1163            .clone();
1164
1165        if handle.current.discoverable {
1166            let iface = Self::make_tcp_server_discoverable_interface(&handle);
1167            if let Some(announcer) = self.interface_announcer.as_mut() {
1168                announcer.upsert_interface(iface);
1169            } else if let Some(identity) = self.transport_identity.as_ref() {
1170                self.interface_announcer = Some(crate::discovery::InterfaceAnnouncer::new(
1171                    *identity.hash(),
1172                    vec![iface],
1173                ));
1174            }
1175        } else if let Some(announcer) = self.interface_announcer.as_mut() {
1176            announcer.remove_interface(interface_name);
1177            if announcer.is_empty() {
1178                self.interface_announcer = None;
1179            }
1180        }
1181
1182        Ok(())
1183    }
1184
1185    #[cfg(feature = "hooks")]
1186    fn update_hook_program<F>(
1187        &mut self,
1188        name: &str,
1189        attach_point: &str,
1190        mut update: F,
1191    ) -> Result<(), String>
1192    where
1193        F: FnMut(&mut rns_hooks::LoadedProgram),
1194    {
1195        let point_idx = crate::config::parse_hook_point(attach_point)
1196            .ok_or_else(|| format!("unknown hook point '{}'", attach_point))?;
1197        let program = self.hook_slots[point_idx]
1198            .programs
1199            .iter_mut()
1200            .find(|program| program.name == name)
1201            .ok_or_else(|| format!("hook '{}' not found at point '{}'", name, attach_point))?;
1202        update(program);
1203        Ok(())
1204    }
1205
1206    pub(crate) fn set_tick_interval_handle(&mut self, tick_interval_ms: Arc<AtomicU64>) {
1207        self.tick_interval_ms = tick_interval_ms;
1208    }
1209
1210    #[allow(dead_code)]
1211    pub(crate) fn set_packet_hashlist_max_entries(&mut self, max_entries: usize) {
1212        self.engine.set_packet_hashlist_max_entries(max_entries);
1213    }
1214
1215    #[cfg(feature = "hooks")]
1216    fn forward_hook_side_effects(&mut self, attach_point: &str, exec: &rns_hooks::ExecuteResult) {
1217        if !exec.injected_actions.is_empty() {
1218            self.dispatch_all(convert_injected_actions(exec.injected_actions.clone()));
1219        }
1220        if let Some(ref bridge) = self.provider_bridge {
1221            for event in &exec.provider_events {
1222                bridge.emit_event(
1223                    attach_point,
1224                    event.hook_name.clone(),
1225                    event.payload_type.clone(),
1226                    event.payload.clone(),
1227                );
1228            }
1229        }
1230    }
1231
1232    #[cfg(feature = "hooks")]
1233    fn collect_hook_side_effects(
1234        &mut self,
1235        attach_point: &str,
1236        exec: &rns_hooks::ExecuteResult,
1237        out: &mut Vec<TransportAction>,
1238    ) {
1239        if !exec.injected_actions.is_empty() {
1240            out.extend(convert_injected_actions(exec.injected_actions.clone()));
1241        }
1242        if let Some(ref bridge) = self.provider_bridge {
1243            for event in &exec.provider_events {
1244                bridge.emit_event(
1245                    attach_point,
1246                    event.hook_name.clone(),
1247                    event.payload_type.clone(),
1248                    event.payload.clone(),
1249                );
1250            }
1251        }
1252    }
1253
1254    /// Set the probe addresses, protocol, and optional device for hole punching.
1255    pub fn set_probe_config(
1256        &mut self,
1257        addrs: Vec<std::net::SocketAddr>,
1258        protocol: rns_core::holepunch::ProbeProtocol,
1259        device: Option<String>,
1260    ) {
1261        self.holepunch_manager = HolePunchManager::new(addrs, protocol, device);
1262    }
1263}