Skip to main content

rns_net/interface/
auto.rs

1//! AutoInterface: Zero-configuration LAN auto-discovery via IPv6 multicast.
2//!
3//! Matches Python `AutoInterface` from `RNS/Interfaces/AutoInterface.py`.
4//!
5//! Thread model (per adopted network interface):
6//!   - Discovery sender: periodically sends discovery token via multicast
7//!   - Discovery receiver (multicast): validates tokens, adds peers
8//!   - Discovery receiver (unicast): validates reverse-peering tokens
9//!   - Data receiver: UDP server receiving unicast data from peers
10//!
11//! Additionally one shared thread:
12//!   - Peer jobs: periodically culls timed-out peers
13
14use std::collections::{HashMap, HashSet, VecDeque};
15use std::io;
16use std::net::{Ipv6Addr, SocketAddrV6, UdpSocket};
17use std::os::unix::io::AsRawFd;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::{Arc, Mutex};
20use std::thread;
21use std::time::{Duration, Instant};
22
23use rns_core::transport::types::InterfaceId;
24
25use crate::event::{Event, EventSender};
26use crate::interface::{lock_or_recover, Writer};
27
28// ── Constants (matching Python AutoInterface) ──────────────────────────────
29
30/// Default UDP port for multicast discovery.
31pub const DEFAULT_DISCOVERY_PORT: u16 = 29716;
32
33/// Default UDP port for unicast data exchange.
34pub const DEFAULT_DATA_PORT: u16 = 42671;
35
36/// Default group identifier.
37pub const DEFAULT_GROUP_ID: &[u8] = b"reticulum";
38
39/// Default IFAC size for AutoInterface (bytes).
40pub const DEFAULT_IFAC_SIZE: usize = 16;
41
42/// Hardware MTU for AutoInterface packets.
43pub const HW_MTU: usize = 1196;
44
45/// Multicast scope: link-local.
46pub const SCOPE_LINK: &str = "2";
47/// Multicast scope: admin-local.
48pub const SCOPE_ADMIN: &str = "4";
49/// Multicast scope: site-local.
50pub const SCOPE_SITE: &str = "5";
51/// Multicast scope: organization-local.
52pub const SCOPE_ORGANISATION: &str = "8";
53/// Multicast scope: global.
54pub const SCOPE_GLOBAL: &str = "e";
55
56/// Permanent multicast address type.
57pub const MULTICAST_PERMANENT_ADDRESS_TYPE: &str = "0";
58/// Temporary multicast address type.
59pub const MULTICAST_TEMPORARY_ADDRESS_TYPE: &str = "1";
60
61/// How long before a peer is considered timed out (seconds).
62pub const PEERING_TIMEOUT: f64 = 22.0;
63
64/// How often to send multicast discovery announcements (seconds).
65pub const ANNOUNCE_INTERVAL: f64 = 1.6;
66
67/// How often to run peer maintenance jobs (seconds).
68pub const PEER_JOB_INTERVAL: f64 = 4.0;
69
70/// Multicast echo timeout (seconds). Used for carrier detection.
71pub const MCAST_ECHO_TIMEOUT: f64 = 6.5;
72
73/// Default bitrate guess for AutoInterface (10 Mbps).
74pub const BITRATE_GUESS: u64 = 10_000_000;
75
76/// Deduplication deque size.
77pub const MULTI_IF_DEQUE_LEN: usize = 48;
78
79/// Deduplication deque entry TTL (seconds).
80pub const MULTI_IF_DEQUE_TTL: f64 = 0.75;
81
82/// Reverse peering interval multiplier (announce_interval * 3.25).
83pub const REVERSE_PEERING_MULTIPLIER: f64 = 3.25;
84
85/// How often AutoInterface rescans local link-local interfaces.
86pub const AUTO_RESCAN_INTERVAL: f64 = 1.25;
87
88/// Default cap on active peers per discovery worker (one adopted network
89/// interface). Discovery tokens from further sources are ignored until an
90/// existing peer times out. Configurable with `max_peers`.
91pub const DEFAULT_MAX_PEERS: usize = 128;
92
93/// New peers each discovery worker admits within one
94/// [`PEER_ADMISSION_WINDOW`].
95pub const MAX_NEW_PEERS_PER_WINDOW: usize = 32;
96
97/// Length of the window over which [`MAX_NEW_PEERS_PER_WINDOW`] applies.
98pub const PEER_ADMISSION_WINDOW: Duration = Duration::from_secs(10);
99
100/// Minimum spacing between warnings about ignored discovery sources, per worker.
101const PEER_REFUSAL_LOG_INTERVAL: Duration = Duration::from_secs(10);
102
103/// Interfaces always ignored.
104pub const ALL_IGNORE_IFS: &[&str] = &["lo0"];
105
106#[cfg_attr(not(any(target_os = "android", test)), allow(dead_code))]
107const ANDROID_IGNORE_IFS: &[&str] = &[
108    "dummy0", "lo", "tun0", "rmnet0", "rmnet1", "rmnet2", "rmnet3", "rmnet4", "rmnet5", "rmnet6",
109    "rmnet7",
110];
111
112// ── Configuration ──────────────────────────────────────────────────────────
113
114/// Configuration for an AutoInterface.
115#[derive(Debug, Clone)]
116pub struct AutoConfig {
117    pub name: String,
118    pub group_id: Vec<u8>,
119    pub discovery_scope: String,
120    pub discovery_port: u16,
121    pub data_port: u16,
122    pub multicast_address_type: String,
123    pub allowed_interfaces: Vec<String>,
124    pub ignored_interfaces: Vec<String>,
125    pub configured_bitrate: u64,
126    /// Maximum number of active peers per adopted network interface
127    /// (`max_peers`, default [`DEFAULT_MAX_PEERS`]). Discovery tokens from
128    /// further sources are ignored until an existing peer times out.
129    pub max_peers: usize,
130    /// Base interface ID. Per-peer IDs will be assigned dynamically.
131    pub interface_id: InterfaceId,
132    pub ingress_control: rns_core::transport::types::IngressControlConfig,
133    pub runtime: Arc<Mutex<AutoRuntime>>,
134}
135
136#[derive(Debug, Clone)]
137pub struct AutoRuntime {
138    pub announce_interval_secs: f64,
139    pub peer_timeout_secs: f64,
140    pub peer_job_interval_secs: f64,
141}
142
143impl AutoRuntime {
144    pub fn from_config(_config: &AutoConfig) -> Self {
145        Self {
146            announce_interval_secs: ANNOUNCE_INTERVAL,
147            peer_timeout_secs: PEERING_TIMEOUT,
148            peer_job_interval_secs: PEER_JOB_INTERVAL,
149        }
150    }
151}
152
153#[derive(Debug, Clone)]
154pub struct AutoRuntimeConfigHandle {
155    pub interface_name: String,
156    pub runtime: Arc<Mutex<AutoRuntime>>,
157    pub startup: AutoRuntime,
158}
159
160impl Default for AutoConfig {
161    fn default() -> Self {
162        let mut config = AutoConfig {
163            name: String::new(),
164            group_id: DEFAULT_GROUP_ID.to_vec(),
165            discovery_scope: SCOPE_LINK.to_string(),
166            discovery_port: DEFAULT_DISCOVERY_PORT,
167            data_port: DEFAULT_DATA_PORT,
168            multicast_address_type: MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string(),
169            allowed_interfaces: Vec::new(),
170            ignored_interfaces: Vec::new(),
171            configured_bitrate: BITRATE_GUESS,
172            max_peers: DEFAULT_MAX_PEERS,
173            interface_id: InterfaceId(0),
174            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
175            runtime: Arc::new(Mutex::new(AutoRuntime {
176                announce_interval_secs: ANNOUNCE_INTERVAL,
177                peer_timeout_secs: PEERING_TIMEOUT,
178                peer_job_interval_secs: PEER_JOB_INTERVAL,
179            })),
180        };
181        let startup = AutoRuntime::from_config(&config);
182        config.runtime = Arc::new(Mutex::new(startup));
183        config
184    }
185}
186
187// ── Multicast address derivation ───────────────────────────────────────────
188
189/// Derive the IPv6 multicast discovery address from group_id, scope, and address type.
190///
191/// Algorithm (matching Python):
192///   1. group_hash = SHA-256(group_id)
193///   2. Build suffix from hash bytes 2..14 as 6 little-endian 16-bit words
194///   3. First word is hardcoded "0"
195///   4. Prefix = "ff" + address_type + scope
196pub fn derive_multicast_address(group_id: &[u8], address_type: &str, scope: &str) -> String {
197    let group_hash = rns_crypto::sha256::sha256(group_id);
198    let g = &group_hash;
199
200    // Build 6 LE 16-bit words from bytes 2..14
201    let w1 = (g[2] as u16) << 8 | g[3] as u16;
202    let w2 = (g[4] as u16) << 8 | g[5] as u16;
203    let w3 = (g[6] as u16) << 8 | g[7] as u16;
204    let w4 = (g[8] as u16) << 8 | g[9] as u16;
205    let w5 = (g[10] as u16) << 8 | g[11] as u16;
206    let w6 = (g[12] as u16) << 8 | g[13] as u16;
207
208    format!(
209        "ff{}{}:0:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
210        address_type, scope, w1, w2, w3, w4, w5, w6
211    )
212}
213
214/// Parse a multicast address string into an Ipv6Addr.
215pub fn parse_multicast_addr(addr: &str) -> Option<Ipv6Addr> {
216    addr.parse::<Ipv6Addr>().ok()
217}
218
219// ── Discovery token ────────────────────────────────────────────────────────
220
221/// Compute the discovery token: SHA-256(group_id + link_local_address_string).
222pub fn compute_discovery_token(group_id: &[u8], link_local_addr: &str) -> [u8; 32] {
223    let mut input = group_id.to_vec();
224    input.extend_from_slice(link_local_addr.as_bytes());
225    rns_crypto::sha256::sha256(&input)
226}
227
228// ── Network interface enumeration ──────────────────────────────────────────
229
230/// Information about a local network interface with an IPv6 link-local address.
231#[derive(Debug, Clone)]
232pub struct LocalInterface {
233    pub name: String,
234    pub link_local_addr: String,
235    pub index: u32,
236}
237
238#[cfg(target_os = "android")]
239fn platform_ignored_interfaces() -> &'static [&'static str] {
240    ANDROID_IGNORE_IFS
241}
242
243#[cfg(not(target_os = "android"))]
244fn platform_ignored_interfaces() -> &'static [&'static str] {
245    &[]
246}
247
248fn should_adopt_interface_name(
249    name: &str,
250    allowed: &[String],
251    ignored: &[String],
252    platform_ignored: &[&str],
253) -> bool {
254    let is_allowed = allowed.iter().any(|a| a == name);
255    let is_system_ignored = ALL_IGNORE_IFS.contains(&name) || platform_ignored.contains(&name);
256
257    if is_system_ignored && !is_allowed {
258        return false;
259    }
260
261    if ignored.iter().any(|ig| ig == name) {
262        return false;
263    }
264
265    if !allowed.is_empty() && !is_allowed {
266        return false;
267    }
268
269    true
270}
271
272/// Enumerate network interfaces that have IPv6 link-local addresses (fe80::/10).
273///
274/// Uses `libc::getifaddrs()`. Filters by allowed/ignored interface lists.
275pub fn enumerate_interfaces(allowed: &[String], ignored: &[String]) -> Vec<LocalInterface> {
276    let mut result = Vec::new();
277    let platform_ignored = platform_ignored_interfaces();
278
279    unsafe {
280        let mut ifaddrs: *mut libc::ifaddrs = std::ptr::null_mut();
281        if libc::getifaddrs(&mut ifaddrs) != 0 {
282            return result;
283        }
284
285        let mut current = ifaddrs;
286        while !current.is_null() {
287            let ifa = &*current;
288            current = ifa.ifa_next;
289
290            // Must have an address
291            if ifa.ifa_addr.is_null() {
292                continue;
293            }
294
295            // Must be AF_INET6
296            if (*ifa.ifa_addr).sa_family as i32 != libc::AF_INET6 {
297                continue;
298            }
299
300            // Get interface name
301            let name = match std::ffi::CStr::from_ptr(ifa.ifa_name).to_str() {
302                Ok(s) => s.to_string(),
303                Err(_) => continue,
304            };
305
306            if !should_adopt_interface_name(&name, allowed, ignored, platform_ignored) {
307                continue;
308            }
309
310            // Extract IPv6 address
311            let sa6 = ifa.ifa_addr as *const libc::sockaddr_in6;
312            let addr_bytes = (*sa6).sin6_addr.s6_addr;
313            let ipv6 = Ipv6Addr::from(addr_bytes);
314
315            // Must be link-local (fe80::/10)
316            let octets = ipv6.octets();
317            if octets[0] != 0xfe || (octets[1] & 0xc0) != 0x80 {
318                continue;
319            }
320
321            // Format the address (drop scope ID, matching Python's descope_linklocal)
322            let addr_str = format!("{}", ipv6);
323
324            // Get interface index
325            let index = libc::if_nametoindex(ifa.ifa_name);
326            if index == 0 {
327                continue;
328            }
329
330            // Avoid duplicates (same interface may appear multiple times)
331            if result.iter().any(|li: &LocalInterface| li.name == name) {
332                continue;
333            }
334
335            result.push(LocalInterface {
336                name,
337                link_local_addr: addr_str,
338                index,
339            });
340        }
341
342        libc::freeifaddrs(ifaddrs);
343    }
344
345    result
346}
347
348mod folded;
349
350// ── Peer and worker tracking ───────────────────────────────────────────────
351
352#[cfg_attr(not(test), allow(dead_code))]
353#[derive(Debug, Clone, PartialEq, Eq, Hash)]
354struct AutoWorkerKey {
355    ifname: String,
356    if_index: u32,
357    link_local_addr: String,
358}
359
360#[allow(dead_code)]
361impl AutoWorkerKey {
362    fn from_local_interface(local: &LocalInterface) -> Self {
363        Self {
364            ifname: local.name.clone(),
365            if_index: local.index,
366            link_local_addr: local.link_local_addr.clone(),
367        }
368    }
369}
370
371#[cfg_attr(not(test), allow(dead_code))]
372#[derive(Debug, Clone, PartialEq, Eq)]
373enum WorkerReconcileAction {
374    Add(AutoWorkerKey),
375    Remove(AutoWorkerKey),
376}
377
378#[cfg_attr(not(test), allow(dead_code))]
379fn reconcile_worker_keys(
380    active: impl IntoIterator<Item = AutoWorkerKey>,
381    desired: impl IntoIterator<Item = AutoWorkerKey>,
382) -> Vec<WorkerReconcileAction> {
383    let active = active.into_iter().collect::<HashSet<_>>();
384    let desired = desired.into_iter().collect::<HashSet<_>>();
385    let mut actions = Vec::new();
386
387    for key in active.difference(&desired) {
388        actions.push(WorkerReconcileAction::Remove(key.clone()));
389    }
390    for key in desired.difference(&active) {
391        actions.push(WorkerReconcileAction::Add(key.clone()));
392    }
393
394    actions.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
395    actions
396}
397
398struct RunningAutoWorker {
399    running: Arc<AtomicBool>,
400}
401
402impl RunningAutoWorker {
403    fn stop(&self) {
404        self.running.store(false, Ordering::Relaxed);
405    }
406}
407
408enum AutoWorkerEntry {
409    Running(RunningAutoWorker),
410    Pending,
411}
412
413impl AutoWorkerEntry {
414    fn stop(&self) {
415        if let AutoWorkerEntry::Running(worker) = self {
416            worker.stop();
417        }
418    }
419}
420
421fn reconcile_auto_workers<F>(
422    workers: &mut HashMap<AutoWorkerKey, AutoWorkerEntry>,
423    desired: Vec<AutoWorkerKey>,
424    mut start_worker: F,
425) where
426    F: FnMut(&AutoWorkerKey) -> io::Result<RunningAutoWorker>,
427{
428    let mut desired_keys = desired.into_iter().collect::<HashSet<_>>();
429    let active_keys = workers.keys().cloned().collect::<Vec<_>>();
430
431    for key in active_keys {
432        if !desired_keys.contains(&key) {
433            if let Some(worker) = workers.remove(&key) {
434                worker.stop();
435            }
436        }
437    }
438
439    let mut desired = desired_keys.drain().collect::<Vec<_>>();
440    desired.sort_by(|a, b| a.ifname.cmp(&b.ifname).then(a.if_index.cmp(&b.if_index)));
441
442    for key in desired {
443        if matches!(workers.get(&key), Some(AutoWorkerEntry::Running(_))) {
444            continue;
445        }
446
447        let entry = match start_worker(&key) {
448            Ok(worker) => AutoWorkerEntry::Running(worker),
449            Err(e) => {
450                log::warn!(
451                    "AutoInterface worker {} failed to start on {}%{}: {}",
452                    key.ifname,
453                    key.link_local_addr,
454                    key.if_index,
455                    e
456                );
457                AutoWorkerEntry::Pending
458            }
459        };
460        workers.insert(key, entry);
461    }
462}
463
464#[derive(Debug, Clone, PartialEq, Eq, Hash)]
465struct PeerKey {
466    link_local_addr: String,
467    if_index: u32,
468}
469
470/// A discovered peer.
471struct AutoPeer {
472    interface_id: InterfaceId,
473    #[allow(dead_code)]
474    link_local_addr: String,
475    #[allow(dead_code)]
476    ifname: String,
477    #[allow(dead_code)]
478    if_index: u32,
479    last_heard: f64,
480}
481
482/// Outcome of asking a discovery worker to admit a new peer.
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484enum PeerAdmissionDecision {
485    Admit,
486    /// The worker already has `max_peers` active peers.
487    AtCapacity,
488    /// The worker admitted [`MAX_NEW_PEERS_PER_WINDOW`] peers in the current window.
489    RateLimited,
490}
491
492/// Per-worker bookkeeping for admitting new peers.
493#[derive(Debug, Default)]
494struct PeerAdmission {
495    window_start: Option<Instant>,
496    admitted_in_window: usize,
497    last_refusal_log: Option<Instant>,
498    refusals_since_log: usize,
499}
500
501impl PeerAdmission {
502    /// Decide whether a new peer may be created, given the worker's current
503    /// number of active peers. An admitted peer counts against the window.
504    fn decide(
505        &mut self,
506        active_peers: usize,
507        max_peers: usize,
508        now: Instant,
509    ) -> PeerAdmissionDecision {
510        if active_peers >= max_peers {
511            return PeerAdmissionDecision::AtCapacity;
512        }
513        let window_elapsed = self
514            .window_start
515            .is_none_or(|start| now.saturating_duration_since(start) >= PEER_ADMISSION_WINDOW);
516        if window_elapsed {
517            self.window_start = Some(now);
518            self.admitted_in_window = 0;
519        }
520        if self.admitted_in_window >= MAX_NEW_PEERS_PER_WINDOW {
521            return PeerAdmissionDecision::RateLimited;
522        }
523        self.admitted_in_window += 1;
524        PeerAdmissionDecision::Admit
525    }
526
527    /// Record a refusal. Returns the number of refusals to report when a
528    /// warning is due, or `None` while warnings are being spaced out.
529    fn note_refusal(&mut self, now: Instant) -> Option<usize> {
530        self.refusals_since_log = self.refusals_since_log.saturating_add(1);
531        let due = self
532            .last_refusal_log
533            .is_none_or(|last| now.saturating_duration_since(last) >= PEER_REFUSAL_LOG_INTERVAL);
534        if !due {
535            return None;
536        }
537        self.last_refusal_log = Some(now);
538        Some(std::mem::take(&mut self.refusals_since_log))
539    }
540}
541
542/// Writer that sends UDP unicast data to a peer.
543struct UdpWriter {
544    socket: UdpSocket,
545    target: SocketAddrV6,
546    /// Called by the driver directly on a non-blocking socket (see
547    /// [`Writer::sends_inline`]), with interface I/O folded.
548    inline: bool,
549}
550
551impl Writer for UdpWriter {
552    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
553        match self.socket.send_to(data, self.target) {
554            // A datagram the socket buffer cannot take is dropped, as the
555            // network may drop it.
556            Err(e) if self.inline && e.kind() == io::ErrorKind::WouldBlock => Ok(()),
557            result => result.map(|_| ()),
558        }
559    }
560
561    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
562        self.socket.send_to(data, self.target).map(|_| ())
563    }
564
565    fn enqueue_confirmed(
566        &mut self,
567        data: &[u8],
568        completion: &mut Option<crate::link_send::Completion>,
569    ) -> io::Result<()> {
570        // A full buffer returns WouldBlock with the completion untouched,
571        // and the driver retries the frame.
572        self.socket.send_to(data, self.target)?;
573        if let Some(completion) = completion.take() {
574            completion.finish(Ok(()));
575        }
576        Ok(())
577    }
578
579    fn sends_inline(&self) -> bool {
580        self.inline
581    }
582}
583
584/// Shared state for the AutoInterface across all threads.
585struct SharedState {
586    /// Known peers, scoped by link-local address and interface index.
587    peers: HashMap<PeerKey, AutoPeer>,
588    /// Our own scoped link-local addresses (for echo detection).
589    link_local_addresses: HashSet<PeerKey>,
590    /// Deduplication deque: (hash, timestamp)
591    dedup_deque: VecDeque<([u8; 32], f64)>,
592    /// Flag set when final_init is done
593    online: bool,
594    /// Next dynamic interface ID
595    next_id: Arc<AtomicU64>,
596    /// Maximum active peers per adopted network interface.
597    max_peers: usize,
598    /// New-peer admission state, keyed by adopted network interface name.
599    peer_admission: HashMap<String, PeerAdmission>,
600}
601
602impl SharedState {
603    fn new(next_id: Arc<AtomicU64>) -> Self {
604        SharedState {
605            peers: HashMap::new(),
606            link_local_addresses: HashSet::new(),
607            dedup_deque: VecDeque::new(),
608            online: false,
609            next_id,
610            max_peers: DEFAULT_MAX_PEERS,
611            peer_admission: HashMap::new(),
612        }
613    }
614
615    /// Number of active peers discovered on the adopted interface `ifname`.
616    fn active_peers_on(&self, ifname: &str) -> usize {
617        self.peers
618            .values()
619            .filter(|peer| peer.ifname == ifname)
620            .count()
621    }
622
623    /// Decide whether the worker on `ifname` may create one more peer.
624    fn admit_new_peer(&mut self, ifname: &str, now: Instant) -> PeerAdmissionDecision {
625        let active_peers = self.active_peers_on(ifname);
626        let max_peers = self.max_peers;
627        self.peer_admission
628            .entry(ifname.to_string())
629            .or_default()
630            .decide(active_peers, max_peers, now)
631    }
632
633    /// Record an ignored discovery source for `ifname`; see [`PeerAdmission::note_refusal`].
634    fn note_peer_refusal(&mut self, ifname: &str, now: Instant) -> Option<usize> {
635        self.peer_admission
636            .entry(ifname.to_string())
637            .or_default()
638            .note_refusal(now)
639    }
640
641    /// Remove peers not heard from within `peer_timeout_secs`, returning them.
642    fn remove_timed_out_peers(
643        &mut self,
644        now: f64,
645        peer_timeout_secs: f64,
646    ) -> Vec<(PeerKey, InterfaceId)> {
647        let timed_out = self
648            .peers
649            .iter()
650            .filter(|(_, peer)| now > peer.last_heard + peer_timeout_secs)
651            .map(|(peer_key, peer)| (peer_key.clone(), peer.interface_id))
652            .collect::<Vec<_>>();
653        for (peer_key, _) in &timed_out {
654            self.peers.remove(peer_key);
655        }
656        timed_out
657    }
658
659    /// Check dedup deque for a data hash.
660    fn is_duplicate(&self, hash: &[u8; 32], now: f64) -> bool {
661        for (h, ts) in &self.dedup_deque {
662            if h == hash && now < *ts + MULTI_IF_DEQUE_TTL {
663                return true;
664            }
665        }
666        false
667    }
668
669    /// Add to dedup deque, trimming to max length.
670    fn add_dedup(&mut self, hash: [u8; 32], now: f64) {
671        self.dedup_deque.push_back((hash, now));
672        while self.dedup_deque.len() > MULTI_IF_DEQUE_LEN {
673            self.dedup_deque.pop_front();
674        }
675    }
676
677    /// Refresh a peer's last_heard timestamp.
678    fn refresh_peer(&mut self, key: &PeerKey, now: f64) {
679        if let Some(peer) = self.peers.get_mut(key) {
680            peer.last_heard = now;
681        }
682    }
683}
684
685fn peer_key_from_src(src_addr: &SocketAddrV6, fallback_if_index: u32) -> PeerKey {
686    let if_index = match src_addr.scope_id() {
687        0 => fallback_if_index,
688        scope_id => scope_id,
689    };
690
691    PeerKey {
692        link_local_addr: src_addr.ip().to_string(),
693        if_index,
694    }
695}
696
697fn peer_target_addr(peer_key: &PeerKey, data_port: u16) -> io::Result<SocketAddrV6> {
698    let peer_ip = peer_key.link_local_addr.parse::<Ipv6Addr>().map_err(|e| {
699        io::Error::new(io::ErrorKind::InvalidInput, format!("bad peer IPv6: {}", e))
700    })?;
701
702    Ok(SocketAddrV6::new(peer_ip, data_port, 0, peer_key.if_index))
703}
704
705fn remove_peers_for_worker(
706    shared: &Arc<Mutex<SharedState>>,
707    tx: &EventSender,
708    key: &AutoWorkerKey,
709) {
710    for iface_id in take_peers_for_worker(shared, key) {
711        let _ = tx.send(Event::InterfaceDown(iface_id));
712    }
713}
714
715/// Forget the peers found through the worker for `key`, returning their
716/// interfaces, which the caller reports down.
717fn take_peers_for_worker(
718    shared: &Arc<Mutex<SharedState>>,
719    key: &AutoWorkerKey,
720) -> Vec<InterfaceId> {
721    let removed = {
722        let mut state = lock_or_recover(shared, "auto shared state");
723        let removed = state
724            .peers
725            .iter()
726            .filter_map(|(peer_key, peer)| {
727                if peer.if_index == key.if_index && peer.ifname == key.ifname {
728                    Some((peer_key.clone(), peer.interface_id))
729                } else {
730                    None
731                }
732            })
733            .collect::<Vec<_>>();
734
735        for (peer_key, _) in &removed {
736            state.peers.remove(peer_key);
737        }
738        state.peer_admission.remove(&key.ifname);
739
740        removed
741    };
742
743    removed.into_iter().map(|(_, iface_id)| iface_id).collect()
744}
745
746// ── Start function ─────────────────────────────────────────────────────────
747
748impl AutoWorkerContext {
749    fn discovery_handler(&self, key: &AutoWorkerKey) -> DiscoveryHandler {
750        DiscoveryHandler {
751            group_id: self.group_id.clone(),
752            shared: self.shared.clone(),
753            name: self.name.clone(),
754            ifname: key.ifname.clone(),
755            if_index: key.if_index,
756            data_port: self.data_port,
757            configured_bitrate: self.configured_bitrate,
758            ingress_control: self.ingress_control,
759            runtime: self.runtime.clone(),
760            dynamic_template: self.dynamic_template.clone(),
761            underlay_mark: self.underlay_mark,
762            inline: crate::engine::FOLD_IO,
763        }
764    }
765}
766
767#[derive(Clone)]
768struct AutoWorkerContext {
769    group_id: Vec<u8>,
770    mcast_ip: Ipv6Addr,
771    discovery_port: u16,
772    unicast_discovery_port: u16,
773    data_port: u16,
774    name: String,
775    configured_bitrate: u64,
776    ingress_control: rns_core::transport::types::IngressControlConfig,
777    runtime: Arc<Mutex<AutoRuntime>>,
778    shared: Arc<Mutex<SharedState>>,
779    tx: EventSender,
780    dynamic_template: Option<super::DynamicInterfaceTemplate>,
781    underlay_mark: Option<u32>,
782}
783
784/// Start an AutoInterface. Discovers local IPv6 link-local interfaces,
785/// sets up multicast discovery, and creates UDP data servers.
786///
787/// Returns a vec of (InterfaceId, Writer) for each initial peer (typically empty
788/// since peers are discovered dynamically via InterfaceUp events).
789pub fn start(
790    config: AutoConfig,
791    tx: EventSender,
792    next_dynamic_id: Arc<AtomicU64>,
793) -> io::Result<()> {
794    start_with_template(config, tx, next_dynamic_id, None, None).map(|_| ())
795}
796
797fn start_with_template(
798    config: AutoConfig,
799    tx: EventSender,
800    next_dynamic_id: Arc<AtomicU64>,
801    dynamic_template: Option<super::DynamicInterfaceTemplate>,
802    underlay_mark: Option<u32>,
803) -> io::Result<super::ListenerControl> {
804    let group_id = config.group_id.clone();
805    let mcast_addr_str = derive_multicast_address(
806        &group_id,
807        &config.multicast_address_type,
808        &config.discovery_scope,
809    );
810
811    let mcast_ip = match parse_multicast_addr(&mcast_addr_str) {
812        Some(ip) => ip,
813        None => {
814            return Err(io::Error::new(
815                io::ErrorKind::InvalidData,
816                format!("Invalid multicast address: {}", mcast_addr_str),
817            ));
818        }
819    };
820
821    {
822        let startup = AutoRuntime::from_config(&config);
823        *lock_or_recover(&config.runtime, "auto runtime") = startup;
824    }
825    let runtime = Arc::clone(&config.runtime);
826    let mut shared_state = SharedState::new(next_dynamic_id);
827    shared_state.max_peers = config.max_peers;
828    let shared = Arc::new(Mutex::new(shared_state));
829    let running = Arc::new(AtomicBool::new(true));
830
831    let worker_context = AutoWorkerContext {
832        group_id,
833        mcast_ip,
834        discovery_port: config.discovery_port,
835        unicast_discovery_port: config.discovery_port + 1,
836        data_port: config.data_port,
837        name: config.name.clone(),
838        configured_bitrate: config.configured_bitrate,
839        ingress_control: config.ingress_control,
840        runtime: runtime.clone(),
841        shared: shared.clone(),
842        tx: tx.clone(),
843        dynamic_template,
844        underlay_mark,
845    };
846
847    if crate::engine::FOLD_IO {
848        folded::start(
849            config.interface_id,
850            worker_context,
851            config.allowed_interfaces.clone(),
852            config.ignored_interfaces.clone(),
853            running.clone(),
854        )?;
855    } else {
856        start_threads(&config, worker_context, &shared, &runtime, &running)?;
857    }
858
859    let announce_interval = lock_or_recover(&runtime, "auto runtime").announce_interval_secs;
860    let peering_wait = Duration::from_secs_f64(announce_interval * 1.2);
861    thread::sleep(peering_wait);
862
863    {
864        let mut state = lock_or_recover(&shared, "auto shared state");
865        state.online = true;
866    }
867
868    log::info!("[{}] AutoInterface online", config.name);
869
870    Ok(super::ListenerControl::with_running_flag(running))
871}
872
873/// Run an AutoInterface on threads of its own: a supervisor that adopts
874/// network interfaces (each served by its own threads) and a peer-jobs
875/// thread that culls silent peers.
876fn start_threads(
877    config: &AutoConfig,
878    worker_context: AutoWorkerContext,
879    shared: &Arc<Mutex<SharedState>>,
880    runtime: &Arc<Mutex<AutoRuntime>>,
881    running: &Arc<AtomicBool>,
882) -> io::Result<()> {
883    {
884        let shared = shared.clone();
885        let tx = worker_context.tx.clone();
886        let running = running.clone();
887        let name = config.name.clone();
888        let runtime = runtime.clone();
889
890        thread::Builder::new()
891            .name(format!("auto-peer-jobs-{}", name))
892            .spawn(move || {
893                peer_jobs_loop(shared, tx, runtime, &running, &name);
894            })?;
895    }
896
897    {
898        let allowed = config.allowed_interfaces.clone();
899        let ignored = config.ignored_interfaces.clone();
900        let running = running.clone();
901        let name = config.name.clone();
902        thread::Builder::new()
903            .name(format!("auto-supervisor-{}", name))
904            .spawn(move || {
905                auto_supervisor_loop(allowed, ignored, worker_context, running, &name);
906            })?;
907    }
908
909    Ok(())
910}
911
912fn auto_supervisor_loop(
913    allowed_interfaces: Vec<String>,
914    ignored_interfaces: Vec<String>,
915    context: AutoWorkerContext,
916    running: Arc<AtomicBool>,
917    name: &str,
918) {
919    let mut workers: HashMap<AutoWorkerKey, AutoWorkerEntry> = HashMap::new();
920
921    while running.load(Ordering::Relaxed) {
922        let interfaces = enumerate_interfaces(&allowed_interfaces, &ignored_interfaces);
923        let desired = interfaces
924            .iter()
925            .map(AutoWorkerKey::from_local_interface)
926            .collect::<Vec<_>>();
927
928        {
929            let mut state = lock_or_recover(&context.shared, "auto shared state");
930            state.link_local_addresses = desired
931                .iter()
932                .map(|key| PeerKey {
933                    link_local_addr: key.link_local_addr.clone(),
934                    if_index: key.if_index,
935                })
936                .collect();
937        }
938
939        let removed_workers = reconcile_worker_keys(workers.keys().cloned(), desired.clone())
940            .into_iter()
941            .filter_map(|action| match action {
942                WorkerReconcileAction::Remove(key) => Some(key),
943                WorkerReconcileAction::Add(_) => None,
944            })
945            .collect::<Vec<_>>();
946        for key in &removed_workers {
947            remove_peers_for_worker(&context.shared, &context.tx, key);
948        }
949
950        if desired.is_empty() && workers.is_empty() {
951            log::warn!("[{}] No suitable IPv6 link-local interfaces found", name);
952        }
953
954        reconcile_auto_workers(&mut workers, desired, |key| {
955            start_auto_worker(key, &context)
956        });
957
958        thread::sleep(Duration::from_secs_f64(AUTO_RESCAN_INTERVAL));
959    }
960
961    for (key, worker) in workers {
962        remove_peers_for_worker(&context.shared, &context.tx, &key);
963        worker.stop();
964    }
965}
966
967fn start_auto_worker(
968    key: &AutoWorkerKey,
969    context: &AutoWorkerContext,
970) -> io::Result<RunningAutoWorker> {
971    let underlay_mark = context.underlay_mark;
972    let mcast_socket = create_multicast_recv_socket(
973        &context.mcast_ip,
974        context.discovery_port,
975        key.if_index,
976        context.underlay_mark,
977    )?;
978    let unicast_socket = create_unicast_recv_socket(
979        &key.link_local_addr,
980        context.unicast_discovery_port,
981        key.if_index,
982        context.underlay_mark,
983    )?;
984    let data_socket = create_data_recv_socket(
985        &key.link_local_addr,
986        context.data_port,
987        key.if_index,
988        context.underlay_mark,
989    )?;
990
991    let worker_running = Arc::new(AtomicBool::new(true));
992
993    {
994        let group_id = context.group_id.clone();
995        let link_local = key.link_local_addr.clone();
996        let running = worker_running.clone();
997        let name = context.name.clone();
998        let runtime = context.runtime.clone();
999        let mcast_ip = context.mcast_ip;
1000        let discovery_port = context.discovery_port;
1001        let if_index = key.if_index;
1002        thread::Builder::new()
1003            .name(format!("auto-disc-tx-{}", key.ifname))
1004            .spawn(move || {
1005                discovery_sender_loop(DiscoverySenderContext {
1006                    group_id,
1007                    link_local_addr: link_local,
1008                    mcast_ip,
1009                    discovery_port,
1010                    if_index,
1011                    runtime,
1012                    running,
1013                    name,
1014                    underlay_mark,
1015                });
1016            })?;
1017    }
1018
1019    let handler = context.discovery_handler(key);
1020    for (socket, thread_name) in [
1021        (mcast_socket, format!("auto-disc-rx-{}", key.ifname)),
1022        (unicast_socket, format!("auto-udisc-rx-{}", key.ifname)),
1023    ] {
1024        let handler = handler.clone();
1025        let tx = context.tx.clone();
1026        let running = worker_running.clone();
1027        thread::Builder::new()
1028            .name(thread_name)
1029            .spawn(move || discovery_receiver_loop(socket, handler, tx, running))?;
1030    }
1031
1032    {
1033        let shared = context.shared.clone();
1034        let tx = context.tx.clone();
1035        let running = worker_running.clone();
1036        let name = context.name.clone();
1037        let if_index = key.if_index;
1038        thread::Builder::new()
1039            .name(format!("auto-data-rx-{}", key.ifname))
1040            .spawn(move || {
1041                data_receiver_loop(data_socket, shared, tx, &running, &name, if_index);
1042            })?;
1043    }
1044
1045    log::info!(
1046        "[{}] AutoInterface worker online on {} {}%{}",
1047        context.name,
1048        key.ifname,
1049        key.link_local_addr,
1050        key.if_index
1051    );
1052
1053    Ok(RunningAutoWorker {
1054        running: worker_running,
1055    })
1056}
1057
1058// ── Socket creation helpers ────────────────────────────────────────────────
1059
1060fn create_multicast_recv_socket(
1061    mcast_ip: &Ipv6Addr,
1062    port: u16,
1063    if_index: u32,
1064    underlay_mark: Option<u32>,
1065) -> io::Result<UdpSocket> {
1066    let socket = socket2::Socket::new(
1067        socket2::Domain::IPV6,
1068        socket2::Type::DGRAM,
1069        Some(socket2::Protocol::UDP),
1070    )?;
1071    super::apply_underlay_mark(socket.as_raw_fd(), underlay_mark)?;
1072
1073    socket.set_reuse_address(true)?;
1074    #[cfg(not(target_os = "windows"))]
1075    socket.set_reuse_port(true)?;
1076
1077    // Bind to [::]:port on the specific interface
1078    let bind_addr = SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, port, 0, if_index);
1079    socket.bind(&bind_addr.into())?;
1080
1081    // Join multicast group on the specific interface
1082    socket.join_multicast_v6(mcast_ip, if_index)?;
1083
1084    socket.set_nonblocking(false)?;
1085    let std_socket: UdpSocket = socket.into();
1086    std_socket.set_read_timeout(Some(Duration::from_secs(2)))?;
1087    Ok(std_socket)
1088}
1089
1090fn create_unicast_recv_socket(
1091    link_local: &str,
1092    port: u16,
1093    if_index: u32,
1094    underlay_mark: Option<u32>,
1095) -> io::Result<UdpSocket> {
1096    let ip: Ipv6Addr = link_local
1097        .parse()
1098        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad IPv6: {}", e)))?;
1099
1100    let socket = socket2::Socket::new(
1101        socket2::Domain::IPV6,
1102        socket2::Type::DGRAM,
1103        Some(socket2::Protocol::UDP),
1104    )?;
1105    super::apply_underlay_mark(socket.as_raw_fd(), underlay_mark)?;
1106
1107    socket.set_reuse_address(true)?;
1108    #[cfg(not(target_os = "windows"))]
1109    socket.set_reuse_port(true)?;
1110
1111    let bind_addr = SocketAddrV6::new(ip, port, 0, if_index);
1112    socket.bind(&bind_addr.into())?;
1113
1114    socket.set_nonblocking(false)?;
1115    let std_socket: UdpSocket = socket.into();
1116    std_socket.set_read_timeout(Some(Duration::from_secs(2)))?;
1117    Ok(std_socket)
1118}
1119
1120fn create_data_recv_socket(
1121    link_local: &str,
1122    port: u16,
1123    if_index: u32,
1124    underlay_mark: Option<u32>,
1125) -> io::Result<UdpSocket> {
1126    let ip: Ipv6Addr = link_local
1127        .parse()
1128        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad IPv6: {}", e)))?;
1129
1130    let socket = socket2::Socket::new(
1131        socket2::Domain::IPV6,
1132        socket2::Type::DGRAM,
1133        Some(socket2::Protocol::UDP),
1134    )?;
1135    super::apply_underlay_mark(socket.as_raw_fd(), underlay_mark)?;
1136
1137    socket.set_reuse_address(true)?;
1138    #[cfg(not(target_os = "windows"))]
1139    socket.set_reuse_port(true)?;
1140
1141    let bind_addr = SocketAddrV6::new(ip, port, 0, if_index);
1142    socket.bind(&bind_addr.into())?;
1143
1144    socket.set_nonblocking(false)?;
1145    let std_socket: UdpSocket = socket.into();
1146    std_socket.set_read_timeout(Some(Duration::from_secs(2)))?;
1147    Ok(std_socket)
1148}
1149
1150// ── Thread loops ───────────────────────────────────────────────────────────
1151
1152/// Discovery sender: periodically sends discovery token via multicast.
1153struct DiscoverySenderContext {
1154    group_id: Vec<u8>,
1155    link_local_addr: String,
1156    mcast_ip: Ipv6Addr,
1157    discovery_port: u16,
1158    if_index: u32,
1159    runtime: Arc<Mutex<AutoRuntime>>,
1160    running: Arc<AtomicBool>,
1161    name: String,
1162    underlay_mark: Option<u32>,
1163}
1164
1165fn discovery_sender_loop(context: DiscoverySenderContext) {
1166    let DiscoverySenderContext {
1167        group_id,
1168        link_local_addr,
1169        mcast_ip,
1170        discovery_port,
1171        if_index,
1172        runtime,
1173        running,
1174        name,
1175        underlay_mark,
1176    } = context;
1177    let token = compute_discovery_token(&group_id, &link_local_addr);
1178
1179    while running.load(Ordering::Relaxed) {
1180        // Create a fresh socket for each send (matches Python)
1181        if let Ok(socket) = UdpSocket::bind("[::]:0") {
1182            if let Err(error) = super::apply_underlay_mark(socket.as_raw_fd(), underlay_mark) {
1183                log::error!("[{}] failed to mark discovery socket: {}", name, error);
1184                return;
1185            }
1186            // Set multicast interface
1187            let if_bytes = if_index.to_ne_bytes();
1188            unsafe {
1189                libc::setsockopt(
1190                    socket_fd(&socket),
1191                    libc::IPPROTO_IPV6,
1192                    libc::IPV6_MULTICAST_IF,
1193                    if_bytes.as_ptr() as *const libc::c_void,
1194                    4,
1195                );
1196            }
1197
1198            let target = SocketAddrV6::new(mcast_ip, discovery_port, 0, 0);
1199            if let Err(e) = socket.send_to(&token, target) {
1200                log::debug!("[{}] multicast send error: {}", name, e);
1201            }
1202        }
1203
1204        let sleep_dur = Duration::from_secs_f64(
1205            lock_or_recover(&runtime, "auto runtime")
1206                .announce_interval_secs
1207                .max(0.1),
1208        );
1209        thread::sleep(sleep_dur);
1210    }
1211}
1212
1213/// What handling a discovery token needs: validating it and admitting the
1214/// peer that sent it.
1215#[derive(Clone)]
1216struct DiscoveryHandler {
1217    group_id: Vec<u8>,
1218    shared: Arc<Mutex<SharedState>>,
1219    name: String,
1220    ifname: String,
1221    if_index: u32,
1222    data_port: u16,
1223    configured_bitrate: u64,
1224    ingress_control: rns_core::transport::types::IngressControlConfig,
1225    runtime: Arc<Mutex<AutoRuntime>>,
1226    dynamic_template: Option<super::DynamicInterfaceTemplate>,
1227    underlay_mark: Option<u32>,
1228    /// Whether new peers' writers are called by the driver directly.
1229    inline: bool,
1230}
1231
1232impl DiscoveryHandler {
1233    /// Handle one discovery datagram, returning the event that brings up
1234    /// a newly admitted peer.
1235    fn handle(&self, datagram: &[u8], src: std::net::SocketAddr) -> Option<Event> {
1236        if datagram.len() < 32 {
1237            return None;
1238        }
1239        // Extract source IPv6 address
1240        let std::net::SocketAddr::V6(src_addr) = src else {
1241            return None;
1242        };
1243        let peer_key = peer_key_from_src(&src_addr, self.if_index);
1244        let peering_hash = &datagram[..32];
1245        let expected = compute_discovery_token(&self.group_id, &peer_key.link_local_addr);
1246        if peering_hash != expected {
1247            log::debug!(
1248                "[{}] invalid peering hash from {}",
1249                self.name,
1250                peer_key.link_local_addr
1251            );
1252            return None;
1253        }
1254
1255        {
1256            // Peers are accepted before the interface is fully online, for
1257            // initial peering (Python processes after final_init_done).
1258            let mut state = lock_or_recover(&self.shared, "auto shared state");
1259            // Multicast echo from ourselves.
1260            if state.link_local_addresses.contains(&peer_key) {
1261                return None;
1262            }
1263            // Already known.
1264            if state.peers.contains_key(&peer_key) {
1265                state.refresh_peer(&peer_key, crate::time::now());
1266                return None;
1267            }
1268        }
1269
1270        // New peer! Create a data writer to send to them.
1271        add_peer(AddPeerContext {
1272            shared: &self.shared,
1273            peer_key: &peer_key,
1274            data_port: self.data_port,
1275            name: &self.name,
1276            ifname: &self.ifname,
1277            configured_bitrate: self.configured_bitrate,
1278            ingress_control: self.ingress_control,
1279            runtime: &self.runtime,
1280            dynamic_template: self.dynamic_template.as_ref(),
1281            underlay_mark: self.underlay_mark,
1282            inline: self.inline,
1283        })
1284    }
1285}
1286
1287/// Discovery receiver: listens for discovery tokens and adds peers.
1288fn discovery_receiver_loop(
1289    socket: UdpSocket,
1290    handler: DiscoveryHandler,
1291    tx: EventSender,
1292    running: Arc<AtomicBool>,
1293) {
1294    let mut buf = [0u8; 1024];
1295
1296    while running.load(Ordering::Relaxed) {
1297        match socket.recv_from(&mut buf) {
1298            Ok((n, src)) => {
1299                if let Some(event) = handler.handle(&buf[..n], src) {
1300                    let _ = tx.send(event);
1301                }
1302            }
1303            Err(ref e)
1304                if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut =>
1305            {
1306                // Timeout, loop again
1307                continue;
1308            }
1309            Err(e) => {
1310                log::warn!("[{}] discovery recv error: {}", handler.name, e);
1311                if !running.load(Ordering::Relaxed) {
1312                    return;
1313                }
1314                thread::sleep(Duration::from_millis(100));
1315            }
1316        }
1317    }
1318}
1319
1320/// Add a new peer, creating a writer and emitting InterfaceUp.
1321struct AddPeerContext<'a> {
1322    shared: &'a Arc<Mutex<SharedState>>,
1323    peer_key: &'a PeerKey,
1324    data_port: u16,
1325    name: &'a str,
1326    ifname: &'a str,
1327    configured_bitrate: u64,
1328    ingress_control: rns_core::transport::types::IngressControlConfig,
1329    runtime: &'a Arc<Mutex<AutoRuntime>>,
1330    dynamic_template: Option<&'a super::DynamicInterfaceTemplate>,
1331    underlay_mark: Option<u32>,
1332    inline: bool,
1333}
1334
1335/// Admit a new peer, returning the event that brings up its interface.
1336fn add_peer(context: AddPeerContext<'_>) -> Option<Event> {
1337    let AddPeerContext {
1338        shared,
1339        peer_key,
1340        data_port,
1341        name,
1342        ifname,
1343        configured_bitrate,
1344        ingress_control,
1345        runtime: _runtime,
1346        dynamic_template,
1347        underlay_mark,
1348        inline,
1349    } = context;
1350
1351    {
1352        let mut state = lock_or_recover(shared, "auto shared state");
1353        if state.peers.contains_key(peer_key) {
1354            state.refresh_peer(peer_key, crate::time::now());
1355            return None;
1356        }
1357        let now = Instant::now();
1358        let decision = state.admit_new_peer(ifname, now);
1359        if decision != PeerAdmissionDecision::Admit {
1360            let report = state.note_peer_refusal(ifname, now);
1361            let max_peers = state.max_peers;
1362            drop(state);
1363            log_peer_refusal(name, ifname, peer_key, decision, max_peers, report);
1364            return None;
1365        }
1366    }
1367
1368    // Create UDP writer to send data to this peer
1369    let send_socket = match UdpSocket::bind("[::]:0") {
1370        Ok(s) => s,
1371        Err(e) => {
1372            log::warn!(
1373                "[{}] failed to create writer for peer {}: {}",
1374                name,
1375                peer_key.link_local_addr,
1376                e
1377            );
1378            return None;
1379        }
1380    };
1381    if let Err(error) = super::apply_underlay_mark(send_socket.as_raw_fd(), underlay_mark) {
1382        log::error!("[{}] failed to mark peer socket: {}", name, error);
1383        return None;
1384    }
1385    if inline {
1386        if let Err(error) = send_socket.set_nonblocking(true) {
1387            log::warn!("[{}] failed to set up peer socket: {}", name, error);
1388            return None;
1389        }
1390    }
1391
1392    let target = match peer_target_addr(peer_key, data_port) {
1393        Ok(target) => target,
1394        Err(e) => {
1395            log::warn!(
1396                "[{}] failed to create target for peer {}%{}: {}",
1397                name,
1398                peer_key.link_local_addr,
1399                peer_key.if_index,
1400                e
1401            );
1402            return None;
1403        }
1404    };
1405
1406    let mut state = lock_or_recover(shared, "auto shared state");
1407
1408    // Double-check not already added (race)
1409    if state.peers.contains_key(peer_key) {
1410        state.refresh_peer(peer_key, crate::time::now());
1411        return None;
1412    }
1413    // Both discovery receivers of a worker may admit concurrently; recheck the cap.
1414    if state.active_peers_on(ifname) >= state.max_peers {
1415        let report = state.note_peer_refusal(ifname, Instant::now());
1416        let max_peers = state.max_peers;
1417        drop(state);
1418        log_peer_refusal(
1419            name,
1420            ifname,
1421            peer_key,
1422            PeerAdmissionDecision::AtCapacity,
1423            max_peers,
1424            report,
1425        );
1426        return None;
1427    }
1428
1429    let peer_id = InterfaceId(state.next_id.fetch_add(1, Ordering::Relaxed));
1430
1431    // Create a boxed writer for the driver
1432    let driver_writer: Box<dyn Writer> = Box::new(UdpWriter {
1433        socket: send_socket,
1434        target,
1435        inline,
1436    });
1437
1438    let peer_info = rns_core::transport::types::InterfaceInfo {
1439        stamp_costs: Default::default(),
1440        id: peer_id,
1441        name: format!(
1442            "{}:{}%{}",
1443            name, peer_key.link_local_addr, peer_key.if_index
1444        ),
1445        mode: rns_core::constants::MODE_FULL,
1446        gravity: 0,
1447        recursive_prs: false,
1448        announces_from_internal: true,
1449        announces_to_internal: None,
1450        out_capable: true,
1451        in_capable: true,
1452        bitrate: Some(configured_bitrate),
1453        airtime_profile: None,
1454        announce_rate_target: None,
1455        announce_rate_grace: 0,
1456        announce_rate_penalty: 0.0,
1457        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1458        is_local_client: false,
1459        wants_tunnel: false,
1460        tunnel_id: None,
1461        mtu: HW_MTU as u32,
1462        ia_freq: 0.0,
1463        ip_freq: 0.0,
1464        op_freq: 0.0,
1465        op_samples: 0,
1466        started: 0.0,
1467        ingress_control,
1468    };
1469
1470    let now = crate::time::now();
1471    state.peers.insert(
1472        peer_key.clone(),
1473        AutoPeer {
1474            interface_id: peer_id,
1475            link_local_addr: peer_key.link_local_addr.clone(),
1476            ifname: ifname.to_string(),
1477            if_index: peer_key.if_index,
1478            last_heard: now,
1479        },
1480    );
1481
1482    log::info!(
1483        "[{}] Peer discovered: {} (id={})",
1484        name,
1485        peer_key.link_local_addr,
1486        peer_id.0
1487    );
1488
1489    // Notify driver of new dynamic interface
1490    let event = if let Some(template) = dynamic_template {
1491        Event::DynamicInterfaceUp {
1492            id: peer_id,
1493            writer: driver_writer,
1494            registration: Box::new(template.registration(peer_info)),
1495        }
1496    } else {
1497        Event::InterfaceUp(peer_id, Some(driver_writer), Some(Box::new(peer_info)))
1498    };
1499    Some(event)
1500}
1501
1502/// Warn about an ignored discovery source when `report` says a warning is due.
1503fn log_peer_refusal(
1504    name: &str,
1505    ifname: &str,
1506    peer_key: &PeerKey,
1507    decision: PeerAdmissionDecision,
1508    max_peers: usize,
1509    report: Option<usize>,
1510) {
1511    let Some(ignored) = report else {
1512        return;
1513    };
1514    let reason = match decision {
1515        PeerAdmissionDecision::AtCapacity => format!("{} active peers is the limit", max_peers),
1516        PeerAdmissionDecision::RateLimited => format!(
1517            "at most {} new peers per {}s",
1518            MAX_NEW_PEERS_PER_WINDOW,
1519            PEER_ADMISSION_WINDOW.as_secs()
1520        ),
1521        PeerAdmissionDecision::Admit => return,
1522    };
1523    log::warn!(
1524        "[{}] ignoring new peer {}%{} on {} ({}); {} new peer(s) ignored since the last warning",
1525        name,
1526        peer_key.link_local_addr,
1527        peer_key.if_index,
1528        ifname,
1529        reason,
1530        ignored
1531    );
1532}
1533
1534/// Handle one datagram on a data socket, returning the frame it carries
1535/// for its peer's interface, or `None` if it is dropped.
1536fn handle_data(
1537    shared: &Mutex<SharedState>,
1538    if_index: u32,
1539    data: &[u8],
1540    src: std::net::SocketAddr,
1541) -> Option<Event> {
1542    if data.is_empty() {
1543        return None;
1544    }
1545    let std::net::SocketAddr::V6(src_addr) = src else {
1546        return None;
1547    };
1548    let peer_key = peer_key_from_src(&src_addr, if_index);
1549
1550    // Only data from known peers is considered (as in Python, which looks
1551    // up the spawned peer before deduplicating).
1552    let mut state = lock_or_recover(shared, "auto shared state");
1553    if !state.online || !state.peers.contains_key(&peer_key) {
1554        return None;
1555    }
1556    let now = crate::time::now();
1557    let data_hash = rns_crypto::sha256::sha256(data);
1558    // Deduplication
1559    if state.is_duplicate(&data_hash, now) {
1560        return None;
1561    }
1562    state.add_dedup(data_hash, now);
1563    state.refresh_peer(&peer_key, now);
1564    let iface_id = state.peers.get(&peer_key)?.interface_id;
1565    Some(Event::Frame {
1566        interface_id: iface_id,
1567        data: data.into(),
1568        rssi: None,
1569        snr: None,
1570    })
1571}
1572
1573/// Data receiver: receives unicast UDP data from peers and dispatches as frames.
1574fn data_receiver_loop(
1575    socket: UdpSocket,
1576    shared: Arc<Mutex<SharedState>>,
1577    tx: EventSender,
1578    running: &AtomicBool,
1579    name: &str,
1580    if_index: u32,
1581) {
1582    let mut buf = [0u8; HW_MTU + 64]; // a bit extra
1583
1584    while running.load(Ordering::Relaxed) {
1585        match socket.recv_from(&mut buf) {
1586            Ok((n, src)) => {
1587                if let Some(frame) = handle_data(&shared, if_index, &buf[..n], src) {
1588                    if tx.send(frame).is_err() {
1589                        return;
1590                    }
1591                }
1592            }
1593            Err(ref e)
1594                if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut =>
1595            {
1596                continue;
1597            }
1598            Err(e) => {
1599                log::warn!("[{}] data recv error: {}", name, e);
1600                if !running.load(Ordering::Relaxed) {
1601                    return;
1602                }
1603                thread::sleep(Duration::from_millis(100));
1604            }
1605        }
1606    }
1607}
1608
1609/// Peer jobs: periodically cull timed-out peers.
1610fn peer_jobs_loop(
1611    shared: Arc<Mutex<SharedState>>,
1612    tx: EventSender,
1613    runtime: Arc<Mutex<AutoRuntime>>,
1614    running: &AtomicBool,
1615    name: &str,
1616) {
1617    while running.load(Ordering::Relaxed) {
1618        let interval = Duration::from_secs_f64(
1619            lock_or_recover(&runtime, "auto runtime")
1620                .peer_job_interval_secs
1621                .max(0.1),
1622        );
1623        thread::sleep(interval);
1624
1625        let now = crate::time::now();
1626        let peer_timeout_secs = lock_or_recover(&runtime, "auto runtime").peer_timeout_secs;
1627
1628        let timed_out = lock_or_recover(&shared, "auto shared state")
1629            .remove_timed_out_peers(now, peer_timeout_secs);
1630
1631        for (peer_key, iface_id) in &timed_out {
1632            log::info!(
1633                "[{}] Peer timed out: {}%{}",
1634                name,
1635                peer_key.link_local_addr,
1636                peer_key.if_index
1637            );
1638            let _ = tx.send(Event::InterfaceDown(*iface_id));
1639        }
1640    }
1641}
1642
1643// ── Helper ─────────────────────────────────────────────────────────────────
1644
1645/// Get the raw file descriptor from a UdpSocket (for setsockopt).
1646#[cfg(unix)]
1647fn socket_fd(socket: &UdpSocket) -> i32 {
1648    use std::os::unix::io::AsRawFd;
1649    socket.as_raw_fd()
1650}
1651
1652#[cfg(not(unix))]
1653fn socket_fd(_socket: &UdpSocket) -> i32 {
1654    0
1655}
1656
1657// ── Factory implementation ─────────────────────────────────────────────────
1658
1659use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
1660
1661/// Factory for `AutoInterface`.
1662pub struct AutoFactory;
1663
1664impl InterfaceFactory for AutoFactory {
1665    fn type_name(&self) -> &str {
1666        "AutoInterface"
1667    }
1668
1669    fn parse_config(
1670        &self,
1671        name: &str,
1672        id: InterfaceId,
1673        params: &HashMap<String, String>,
1674    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1675        let group_id = params
1676            .get("group_id")
1677            .map(|v| v.as_bytes().to_vec())
1678            .unwrap_or_else(|| DEFAULT_GROUP_ID.to_vec());
1679
1680        let discovery_scope = params
1681            .get("discovery_scope")
1682            .map(|v| match v.to_lowercase().as_str() {
1683                "link" => SCOPE_LINK.to_string(),
1684                "admin" => SCOPE_ADMIN.to_string(),
1685                "site" => SCOPE_SITE.to_string(),
1686                "organisation" | "organization" => SCOPE_ORGANISATION.to_string(),
1687                "global" => SCOPE_GLOBAL.to_string(),
1688                _ => v.clone(),
1689            })
1690            .unwrap_or_else(|| SCOPE_LINK.to_string());
1691
1692        let discovery_port = params
1693            .get("discovery_port")
1694            .and_then(|v| v.parse().ok())
1695            .unwrap_or(DEFAULT_DISCOVERY_PORT);
1696
1697        let data_port = params
1698            .get("data_port")
1699            .and_then(|v| v.parse().ok())
1700            .unwrap_or(DEFAULT_DATA_PORT);
1701
1702        let multicast_address_type = params
1703            .get("multicast_address_type")
1704            .map(|v| match v.to_lowercase().as_str() {
1705                "permanent" => MULTICAST_PERMANENT_ADDRESS_TYPE.to_string(),
1706                "temporary" => MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string(),
1707                _ => v.clone(),
1708            })
1709            .unwrap_or_else(|| MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string());
1710
1711        let configured_bitrate = params
1712            .get("configured_bitrate")
1713            .or_else(|| params.get("bitrate"))
1714            .and_then(|v| v.parse().ok())
1715            .unwrap_or(BITRATE_GUESS);
1716
1717        let allowed_interfaces = params
1718            .get("devices")
1719            .or_else(|| params.get("allowed_interfaces"))
1720            .map(|v| {
1721                v.split(',')
1722                    .map(|s| s.trim().to_string())
1723                    .filter(|s| !s.is_empty())
1724                    .collect()
1725            })
1726            .unwrap_or_default();
1727
1728        let ignored_interfaces = params
1729            .get("ignored_devices")
1730            .or_else(|| params.get("ignored_interfaces"))
1731            .map(|v| {
1732                v.split(',')
1733                    .map(|s| s.trim().to_string())
1734                    .filter(|s| !s.is_empty())
1735                    .collect()
1736            })
1737            .unwrap_or_default();
1738
1739        let max_peers = params
1740            .get("max_peers")
1741            .and_then(|v| v.parse::<usize>().ok())
1742            .filter(|max_peers| *max_peers > 0)
1743            .unwrap_or(DEFAULT_MAX_PEERS);
1744
1745        Ok(Box::new(AutoConfig {
1746            name: name.to_string(),
1747            group_id,
1748            discovery_scope,
1749            discovery_port,
1750            data_port,
1751            multicast_address_type,
1752            allowed_interfaces,
1753            ignored_interfaces,
1754            configured_bitrate,
1755            max_peers,
1756            interface_id: id,
1757            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
1758            runtime: Arc::new(Mutex::new(AutoRuntime {
1759                announce_interval_secs: ANNOUNCE_INTERVAL,
1760                peer_timeout_secs: PEERING_TIMEOUT,
1761                peer_job_interval_secs: PEER_JOB_INTERVAL,
1762            })),
1763        }))
1764    }
1765
1766    fn start(
1767        &self,
1768        config: Box<dyn InterfaceConfigData>,
1769        ctx: StartContext,
1770    ) -> std::io::Result<StartResult> {
1771        let mut auto_config = *config.into_any().downcast::<AutoConfig>().map_err(|_| {
1772            std::io::Error::new(std::io::ErrorKind::InvalidData, "wrong config type")
1773        })?;
1774
1775        auto_config.ingress_control = ctx.ingress_control;
1776        let parent_id = auto_config.interface_id;
1777        let control = start_with_template(
1778            auto_config,
1779            ctx.tx,
1780            ctx.next_dynamic_id,
1781            Some(super::DynamicInterfaceTemplate {
1782                parent_id,
1783                interface_type: "AutoInterfacePeer".into(),
1784                ifac: ctx.ifac,
1785                mode: ctx.mode,
1786                gravity: ctx.gravity,
1787                recursive_prs: ctx.recursive_prs,
1788                announces_from_internal: ctx.announces_from_internal,
1789                announces_to_internal: ctx.announces_to_internal,
1790                stamp_costs: ctx.stamp_costs,
1791            }),
1792            ctx.underlay_mark,
1793        )?;
1794        Ok(StartResult::Listener {
1795            control: Some(control),
1796        })
1797    }
1798}
1799
1800pub(crate) fn auto_runtime_handle_from_config(config: &AutoConfig) -> AutoRuntimeConfigHandle {
1801    AutoRuntimeConfigHandle {
1802        interface_name: config.name.clone(),
1803        runtime: Arc::clone(&config.runtime),
1804        startup: AutoRuntime::from_config(config),
1805    }
1806}
1807
1808// ── Tests ──────────────────────────────────────────────────────────────────
1809
1810#[cfg(test)]
1811mod tests {
1812    use super::*;
1813
1814    #[test]
1815    fn listener_control_stops_auto_supervisor() {
1816        let running = Arc::new(AtomicBool::new(true));
1817        let control = super::super::ListenerControl::with_running_flag(Arc::clone(&running));
1818
1819        control.request_stop();
1820
1821        assert!(!running.load(Ordering::Relaxed));
1822        assert!(control.should_stop());
1823    }
1824
1825    // ── Multicast address derivation ──────────────────────────────────
1826
1827    #[test]
1828    fn multicast_address_default_group() {
1829        // Python vector: ff12:0:d70b:fb1c:16e4:5e39:485e:31e1
1830        let addr = derive_multicast_address(
1831            DEFAULT_GROUP_ID,
1832            MULTICAST_TEMPORARY_ADDRESS_TYPE,
1833            SCOPE_LINK,
1834        );
1835        assert_eq!(addr, "ff12:0:d70b:fb1c:16e4:5e39:485e:31e1");
1836    }
1837
1838    #[test]
1839    fn multicast_address_custom_group() {
1840        let addr =
1841            derive_multicast_address(b"testgroup", MULTICAST_TEMPORARY_ADDRESS_TYPE, SCOPE_LINK);
1842        // Just verify format
1843        assert!(addr.starts_with("ff12:0:"));
1844        // Must be different from default
1845        assert_ne!(addr, "ff12:0:d70b:fb1c:16e4:5e39:485e:31e1");
1846    }
1847
1848    #[test]
1849    fn multicast_address_scope_admin() {
1850        let addr = derive_multicast_address(
1851            DEFAULT_GROUP_ID,
1852            MULTICAST_TEMPORARY_ADDRESS_TYPE,
1853            SCOPE_ADMIN,
1854        );
1855        assert!(addr.starts_with("ff14:0:"));
1856    }
1857
1858    #[test]
1859    fn multicast_address_permanent_type() {
1860        let addr = derive_multicast_address(
1861            DEFAULT_GROUP_ID,
1862            MULTICAST_PERMANENT_ADDRESS_TYPE,
1863            SCOPE_LINK,
1864        );
1865        assert!(addr.starts_with("ff02:0:"));
1866    }
1867
1868    #[test]
1869    fn multicast_address_parseable() {
1870        let addr = derive_multicast_address(
1871            DEFAULT_GROUP_ID,
1872            MULTICAST_TEMPORARY_ADDRESS_TYPE,
1873            SCOPE_LINK,
1874        );
1875        let ip = parse_multicast_addr(&addr);
1876        assert!(ip.is_some());
1877        assert!(ip.unwrap().is_multicast());
1878    }
1879
1880    // ── Discovery token ──────────────────────────────────────────────
1881
1882    #[test]
1883    fn discovery_token_interop() {
1884        // Python vector: fe80::1 → 97b25576749ea936b0d8a8536ffaf442d157cf47d460dcf13c48b7bd18b6c163
1885        let token = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::1");
1886        let expected = "97b25576749ea936b0d8a8536ffaf442d157cf47d460dcf13c48b7bd18b6c163";
1887        let got = token
1888            .iter()
1889            .map(|b| format!("{:02x}", b))
1890            .collect::<String>();
1891        assert_eq!(got, expected);
1892    }
1893
1894    #[test]
1895    fn discovery_token_interop_2() {
1896        // Python vector: fe80::dead:beef:1234:5678
1897        let token = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::dead:beef:1234:5678");
1898        let expected = "46b6ec7595504b6a35f06bd4bfff71567fb82fcf2706cd361bab20409c42d072";
1899        let got = token
1900            .iter()
1901            .map(|b| format!("{:02x}", b))
1902            .collect::<String>();
1903        assert_eq!(got, expected);
1904    }
1905
1906    #[test]
1907    fn discovery_token_different_groups() {
1908        let t1 = compute_discovery_token(b"reticulum", "fe80::1");
1909        let t2 = compute_discovery_token(b"othergroup", "fe80::1");
1910        assert_ne!(t1, t2);
1911    }
1912
1913    #[test]
1914    fn discovery_token_different_addrs() {
1915        let t1 = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::1");
1916        let t2 = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::2");
1917        assert_ne!(t1, t2);
1918    }
1919
1920    // ── Deduplication ────────────────────────────────────────────────
1921
1922    #[test]
1923    fn dedup_basic() {
1924        let next_id = Arc::new(AtomicU64::new(1));
1925        let mut state = SharedState::new(next_id);
1926
1927        let hash = [0xAA; 32];
1928        let now = 1000.0;
1929
1930        assert!(!state.is_duplicate(&hash, now));
1931        state.add_dedup(hash, now);
1932        assert!(state.is_duplicate(&hash, now));
1933    }
1934
1935    #[test]
1936    fn dedup_expired() {
1937        let next_id = Arc::new(AtomicU64::new(1));
1938        let mut state = SharedState::new(next_id);
1939
1940        let hash = [0xBB; 32];
1941        state.add_dedup(hash, 1000.0);
1942
1943        // Within TTL
1944        assert!(state.is_duplicate(&hash, 1000.5));
1945        // Expired
1946        assert!(!state.is_duplicate(&hash, 1001.0));
1947    }
1948
1949    #[test]
1950    fn dedup_max_length() {
1951        let next_id = Arc::new(AtomicU64::new(1));
1952        let mut state = SharedState::new(next_id);
1953
1954        // Fill beyond max
1955        for i in 0..MULTI_IF_DEQUE_LEN + 10 {
1956            let mut hash = [0u8; 32];
1957            hash[0] = (i & 0xFF) as u8;
1958            hash[1] = ((i >> 8) & 0xFF) as u8;
1959            state.add_dedup(hash, 1000.0);
1960        }
1961
1962        assert_eq!(state.dedup_deque.len(), MULTI_IF_DEQUE_LEN);
1963    }
1964
1965    // ── Peer tracking ────────────────────────────────────────────────
1966
1967    #[test]
1968    fn peer_refresh() {
1969        let next_id = Arc::new(AtomicU64::new(100));
1970        let mut state = SharedState::new(next_id);
1971        let key = peer_key("fe80::1", 2);
1972
1973        state.peers.insert(
1974            key.clone(),
1975            AutoPeer {
1976                interface_id: InterfaceId(100),
1977                link_local_addr: key.link_local_addr.clone(),
1978                ifname: "eth0".to_string(),
1979                if_index: key.if_index,
1980                last_heard: 1000.0,
1981            },
1982        );
1983
1984        state.refresh_peer(&key, 2000.0);
1985        assert_eq!(state.peers[&key].last_heard, 2000.0);
1986    }
1987
1988    #[test]
1989    fn peer_not_found_refresh() {
1990        let next_id = Arc::new(AtomicU64::new(100));
1991        let mut state = SharedState::new(next_id);
1992        // Should not panic
1993        state.refresh_peer(&peer_key("fe80::999", 4), 1000.0);
1994    }
1995
1996    fn peer_key(addr: &str, if_index: u32) -> PeerKey {
1997        PeerKey {
1998            link_local_addr: addr.to_string(),
1999            if_index,
2000        }
2001    }
2002
2003    #[test]
2004    fn peer_key_distinguishes_same_link_local_on_different_interfaces() {
2005        let next_id = Arc::new(AtomicU64::new(100));
2006        let mut state = SharedState::new(next_id);
2007        let eth = peer_key("fe80::1", 2);
2008        let wlan = peer_key("fe80::1", 4);
2009
2010        state.peers.insert(
2011            eth.clone(),
2012            AutoPeer {
2013                interface_id: InterfaceId(100),
2014                link_local_addr: eth.link_local_addr.clone(),
2015                ifname: "eth0".to_string(),
2016                if_index: eth.if_index,
2017                last_heard: 1000.0,
2018            },
2019        );
2020        state.peers.insert(
2021            wlan.clone(),
2022            AutoPeer {
2023                interface_id: InterfaceId(101),
2024                link_local_addr: wlan.link_local_addr.clone(),
2025                ifname: "wlan0".to_string(),
2026                if_index: wlan.if_index,
2027                last_heard: 1000.0,
2028            },
2029        );
2030
2031        assert_eq!(state.peers.len(), 2);
2032        assert_eq!(state.peers[&eth].interface_id, InterfaceId(100));
2033        assert_eq!(state.peers[&wlan].interface_id, InterfaceId(101));
2034    }
2035
2036    #[test]
2037    fn peer_target_addr_uses_scope_id() {
2038        let target = peer_target_addr(&peer_key("fe80::1", 7), 42671).unwrap();
2039
2040        assert_eq!(target.port(), 42671);
2041        assert_eq!(target.scope_id(), 7);
2042    }
2043
2044    #[test]
2045    fn remove_peers_for_worker_emits_interface_down() {
2046        let next_id = Arc::new(AtomicU64::new(100));
2047        let shared = Arc::new(Mutex::new(SharedState::new(next_id)));
2048        let worker = worker_key("wlan0", 4, "fe80::abcd");
2049        let removed = peer_key("fe80::1", 4);
2050        let kept = peer_key("fe80::1", 5);
2051        let (tx, rx) = crate::event::channel_with_capacity(4);
2052
2053        {
2054            let mut state = lock_or_recover(&shared, "test auto shared state");
2055            state.peers.insert(
2056                removed.clone(),
2057                AutoPeer {
2058                    interface_id: InterfaceId(100),
2059                    link_local_addr: removed.link_local_addr.clone(),
2060                    ifname: "wlan0".to_string(),
2061                    if_index: removed.if_index,
2062                    last_heard: 1000.0,
2063                },
2064            );
2065            state.peers.insert(
2066                kept.clone(),
2067                AutoPeer {
2068                    interface_id: InterfaceId(101),
2069                    link_local_addr: kept.link_local_addr.clone(),
2070                    ifname: "wlan1".to_string(),
2071                    if_index: kept.if_index,
2072                    last_heard: 1000.0,
2073                },
2074            );
2075        }
2076
2077        remove_peers_for_worker(&shared, &tx, &worker);
2078
2079        let state = lock_or_recover(&shared, "test auto shared state");
2080        assert!(!state.peers.contains_key(&removed));
2081        assert!(state.peers.contains_key(&kept));
2082        drop(state);
2083
2084        assert!(matches!(
2085            rx.recv_timeout(std::time::Duration::from_secs(1)).unwrap(),
2086            Event::InterfaceDown(InterfaceId(100))
2087        ));
2088        assert!(rx.try_recv().is_err());
2089    }
2090
2091    // ── Network interface enumeration ────────────────────────────────
2092
2093    #[test]
2094    fn enumerate_returns_vec() {
2095        // This test just verifies the function runs without crashing.
2096        // Results depend on the system's network configuration.
2097        let interfaces = enumerate_interfaces(&[], &[]);
2098        // On CI/test machines, we may or may not have IPv6 link-local
2099        for iface in &interfaces {
2100            assert!(!iface.name.is_empty());
2101            assert!(iface.link_local_addr.starts_with("fe80"));
2102            assert!(iface.index > 0);
2103        }
2104    }
2105
2106    #[test]
2107    fn enumerate_with_ignored() {
2108        // Ignore everything
2109        let interfaces = enumerate_interfaces(
2110            &[],
2111            &[
2112                "lo".to_string(),
2113                "eth0".to_string(),
2114                "wlan0".to_string(),
2115                "enp0s3".to_string(),
2116                "docker0".to_string(),
2117            ],
2118        );
2119        // May still have some interfaces, but known ones should be filtered
2120        for iface in &interfaces {
2121            assert_ne!(iface.name, "lo");
2122            assert_ne!(iface.name, "eth0");
2123            assert_ne!(iface.name, "wlan0");
2124        }
2125    }
2126
2127    #[test]
2128    fn enumerate_with_allowed_nonexistent() {
2129        // Only allow an interface that doesn't exist
2130        let interfaces = enumerate_interfaces(&["nonexistent_if_12345".to_string()], &[]);
2131        assert!(interfaces.is_empty());
2132    }
2133
2134    fn worker_key(name: &str, index: u32, addr: &str) -> AutoWorkerKey {
2135        AutoWorkerKey {
2136            ifname: name.to_string(),
2137            if_index: index,
2138            link_local_addr: addr.to_string(),
2139        }
2140    }
2141
2142    #[test]
2143    fn reconcile_worker_keys_adds_new_interface() {
2144        let desired = vec![worker_key("wlan0", 4, "fe80::1")];
2145
2146        assert_eq!(
2147            reconcile_worker_keys(Vec::new(), desired.clone()),
2148            vec![WorkerReconcileAction::Add(desired[0].clone())]
2149        );
2150    }
2151
2152    #[test]
2153    fn reconcile_worker_keys_keeps_unchanged_interface() {
2154        let key = worker_key("wlan0", 4, "fe80::1");
2155
2156        assert!(reconcile_worker_keys(vec![key.clone()], vec![key]).is_empty());
2157    }
2158
2159    #[test]
2160    fn reconcile_worker_keys_removes_missing_interface() {
2161        let active = vec![worker_key("wlan0", 4, "fe80::1")];
2162
2163        assert_eq!(
2164            reconcile_worker_keys(active.clone(), Vec::new()),
2165            vec![WorkerReconcileAction::Remove(active[0].clone())]
2166        );
2167    }
2168
2169    #[test]
2170    fn reconcile_worker_keys_replaces_changed_interface_identity() {
2171        let old = worker_key("wlan0", 4, "fe80::1");
2172        let new = worker_key("wlan0", 9, "fe80::2");
2173        let actions = reconcile_worker_keys(vec![old.clone()], vec![new.clone()]);
2174
2175        assert!(actions.contains(&WorkerReconcileAction::Remove(old)));
2176        assert!(actions.contains(&WorkerReconcileAction::Add(new)));
2177        assert_eq!(actions.len(), 2);
2178    }
2179
2180    #[test]
2181    fn reconcile_auto_workers_starts_new_worker() {
2182        let key = worker_key("wlan0", 4, "fe80::1");
2183        let mut workers = HashMap::new();
2184        let mut starts = Vec::new();
2185
2186        reconcile_auto_workers(&mut workers, vec![key.clone()], |key| {
2187            starts.push(key.clone());
2188            Ok(RunningAutoWorker {
2189                running: Arc::new(AtomicBool::new(true)),
2190            })
2191        });
2192
2193        assert_eq!(starts, vec![key.clone()]);
2194        assert!(matches!(
2195            workers.get(&key),
2196            Some(AutoWorkerEntry::Running(_))
2197        ));
2198    }
2199
2200    #[test]
2201    fn reconcile_auto_workers_stops_removed_worker() {
2202        let key = worker_key("wlan0", 4, "fe80::1");
2203        let running = Arc::new(AtomicBool::new(true));
2204        let mut workers = HashMap::from([(
2205            key.clone(),
2206            AutoWorkerEntry::Running(RunningAutoWorker {
2207                running: running.clone(),
2208            }),
2209        )]);
2210
2211        reconcile_auto_workers(&mut workers, Vec::new(), |_| unreachable!());
2212
2213        assert!(!running.load(Ordering::Relaxed));
2214        assert!(!workers.contains_key(&key));
2215    }
2216
2217    #[test]
2218    fn reconcile_auto_workers_leaves_unchanged_worker_running() {
2219        let key = worker_key("wlan0", 4, "fe80::1");
2220        let running = Arc::new(AtomicBool::new(true));
2221        let mut workers = HashMap::from([(
2222            key.clone(),
2223            AutoWorkerEntry::Running(RunningAutoWorker {
2224                running: running.clone(),
2225            }),
2226        )]);
2227        let mut starts = 0;
2228
2229        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
2230            starts += 1;
2231            unreachable!()
2232        });
2233
2234        assert_eq!(starts, 0);
2235        assert!(running.load(Ordering::Relaxed));
2236        assert!(workers.contains_key(&key));
2237    }
2238
2239    #[test]
2240    fn reconcile_auto_workers_records_pending_start_failure() {
2241        let key = worker_key("wlan0", 4, "fe80::1");
2242        let mut workers = HashMap::new();
2243
2244        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
2245            Err(io::Error::new(io::ErrorKind::AddrInUse, "busy"))
2246        });
2247
2248        assert!(matches!(workers.get(&key), Some(AutoWorkerEntry::Pending)));
2249    }
2250
2251    #[test]
2252    fn reconcile_auto_workers_retries_pending_worker() {
2253        let key = worker_key("wlan0", 4, "fe80::1");
2254        let running = Arc::new(AtomicBool::new(true));
2255        let mut workers = HashMap::from([(key.clone(), AutoWorkerEntry::Pending)]);
2256        let mut starts = 0;
2257
2258        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
2259            starts += 1;
2260            Ok(RunningAutoWorker {
2261                running: running.clone(),
2262            })
2263        });
2264
2265        assert_eq!(starts, 1);
2266        assert!(matches!(
2267            workers.get(&key),
2268            Some(AutoWorkerEntry::Running(_))
2269        ));
2270    }
2271
2272    #[test]
2273    fn reconcile_auto_workers_keeps_pending_worker_on_retry_failure() {
2274        let key = worker_key("wlan0", 4, "fe80::1");
2275        let mut workers = HashMap::from([(key.clone(), AutoWorkerEntry::Pending)]);
2276        let mut starts = 0;
2277
2278        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
2279            starts += 1;
2280            Err(io::Error::new(io::ErrorKind::AddrInUse, "busy"))
2281        });
2282
2283        assert_eq!(starts, 1);
2284        assert!(matches!(workers.get(&key), Some(AutoWorkerEntry::Pending)));
2285    }
2286
2287    #[test]
2288    fn reconcile_auto_workers_removes_pending_worker() {
2289        let key = worker_key("wlan0", 4, "fe80::1");
2290        let mut workers = HashMap::from([(key.clone(), AutoWorkerEntry::Pending)]);
2291
2292        reconcile_auto_workers(&mut workers, Vec::new(), |_| unreachable!());
2293
2294        assert!(!workers.contains_key(&key));
2295    }
2296
2297    #[test]
2298    fn filter_skips_android_system_interfaces() {
2299        let allowed = Vec::new();
2300        let ignored = Vec::new();
2301
2302        for name in ["dummy0", "lo", "tun0", "rmnet0", "rmnet7"] {
2303            assert!(
2304                !should_adopt_interface_name(name, &allowed, &ignored, ANDROID_IGNORE_IFS),
2305                "{name} should be skipped by Android AutoInterface defaults"
2306            );
2307        }
2308    }
2309
2310    #[test]
2311    fn filter_does_not_skip_rmnet8_by_android_defaults() {
2312        assert!(should_adopt_interface_name(
2313            "rmnet8",
2314            &[],
2315            &[],
2316            ANDROID_IGNORE_IFS
2317        ));
2318    }
2319
2320    #[test]
2321    fn filter_allowed_overrides_system_ignored_interface() {
2322        assert!(should_adopt_interface_name(
2323            "rmnet0",
2324            &["rmnet0".to_string()],
2325            &[],
2326            ANDROID_IGNORE_IFS
2327        ));
2328
2329        assert!(should_adopt_interface_name(
2330            "lo0",
2331            &["lo0".to_string()],
2332            &[],
2333            &[]
2334        ));
2335    }
2336
2337    #[test]
2338    fn filter_ignored_wins_over_allowed_interface() {
2339        assert!(!should_adopt_interface_name(
2340            "rmnet0",
2341            &["rmnet0".to_string()],
2342            &["rmnet0".to_string()],
2343            ANDROID_IGNORE_IFS
2344        ));
2345    }
2346
2347    #[test]
2348    fn filter_allowed_list_excludes_unlisted_interfaces() {
2349        assert!(!should_adopt_interface_name(
2350            "wlan0",
2351            &["eth0".to_string()],
2352            &[],
2353            ANDROID_IGNORE_IFS
2354        ));
2355
2356        assert!(should_adopt_interface_name(
2357            "wlan0",
2358            &["wlan0".to_string()],
2359            &[],
2360            ANDROID_IGNORE_IFS
2361        ));
2362    }
2363
2364    // ── Config defaults ──────────────────────────────────────────────
2365
2366    #[test]
2367    fn config_defaults() {
2368        let config = AutoConfig::default();
2369        assert_eq!(config.group_id, DEFAULT_GROUP_ID);
2370        assert_eq!(config.discovery_scope, SCOPE_LINK);
2371        assert_eq!(config.discovery_port, DEFAULT_DISCOVERY_PORT);
2372        assert_eq!(config.data_port, DEFAULT_DATA_PORT);
2373        assert_eq!(
2374            config.multicast_address_type,
2375            MULTICAST_TEMPORARY_ADDRESS_TYPE
2376        );
2377        assert_eq!(config.configured_bitrate, BITRATE_GUESS);
2378        assert!(config.allowed_interfaces.is_empty());
2379        assert!(config.ignored_interfaces.is_empty());
2380        assert_eq!(config.max_peers, DEFAULT_MAX_PEERS);
2381    }
2382
2383    fn parse_auto_config(params: &[(&str, &str)]) -> AutoConfig {
2384        let params = params
2385            .iter()
2386            .map(|(k, v)| (k.to_string(), v.to_string()))
2387            .collect::<HashMap<_, _>>();
2388        let config = AutoFactory
2389            .parse_config("auto", InterfaceId(1), &params)
2390            .unwrap();
2391        *config.into_any().downcast::<AutoConfig>().unwrap()
2392    }
2393
2394    #[test]
2395    fn parse_config_reads_max_peers() {
2396        assert_eq!(parse_auto_config(&[]).max_peers, DEFAULT_MAX_PEERS);
2397        assert_eq!(parse_auto_config(&[("max_peers", "16")]).max_peers, 16);
2398        assert_eq!(
2399            parse_auto_config(&[("max_peers", "0")]).max_peers,
2400            DEFAULT_MAX_PEERS
2401        );
2402        assert_eq!(
2403            parse_auto_config(&[("max_peers", "many")]).max_peers,
2404            DEFAULT_MAX_PEERS
2405        );
2406    }
2407
2408    // ── Peer admission ───────────────────────────────────────────────
2409
2410    fn insert_peer(state: &mut SharedState, addr: &str, ifname: &str, last_heard: f64) {
2411        let key = peer_key(addr, 2);
2412        let id = InterfaceId(state.next_id.fetch_add(1, Ordering::Relaxed));
2413        state.peers.insert(
2414            key.clone(),
2415            AutoPeer {
2416                interface_id: id,
2417                link_local_addr: key.link_local_addr.clone(),
2418                ifname: ifname.to_string(),
2419                if_index: key.if_index,
2420                last_heard,
2421            },
2422        );
2423    }
2424
2425    #[test]
2426    fn peer_cap_refuses_peer_beyond_limit_and_admits_after_timeout() {
2427        let mut state = SharedState::new(Arc::new(AtomicU64::new(100)));
2428        state.max_peers = 3;
2429        let now = Instant::now();
2430
2431        for (i, addr) in ["fe80::1", "fe80::2", "fe80::3"].iter().enumerate() {
2432            assert_eq!(
2433                state.admit_new_peer("eth0", now),
2434                PeerAdmissionDecision::Admit
2435            );
2436            insert_peer(&mut state, addr, "eth0", 1000.0 + i as f64);
2437        }
2438        assert_eq!(
2439            state.admit_new_peer("eth0", now),
2440            PeerAdmissionDecision::AtCapacity
2441        );
2442        // Another adopted interface has its own cap.
2443        assert_eq!(
2444            state.admit_new_peer("wlan0", now),
2445            PeerAdmissionDecision::Admit
2446        );
2447
2448        // fe80::1 was last heard at 1000.0 and times out first.
2449        let removed = state.remove_timed_out_peers(1000.0 + PEERING_TIMEOUT + 0.5, PEERING_TIMEOUT);
2450        assert_eq!(removed.len(), 1);
2451        assert_eq!(removed[0].0, peer_key("fe80::1", 2));
2452        assert_eq!(state.active_peers_on("eth0"), 2);
2453
2454        assert_eq!(
2455            state.admit_new_peer("eth0", now),
2456            PeerAdmissionDecision::Admit
2457        );
2458    }
2459
2460    #[test]
2461    fn peer_admission_rate_limit_refuses_within_window_and_admits_after() {
2462        let mut admission = PeerAdmission::default();
2463        let start = Instant::now();
2464
2465        for _ in 0..MAX_NEW_PEERS_PER_WINDOW {
2466            assert_eq!(
2467                admission.decide(0, usize::MAX, start),
2468                PeerAdmissionDecision::Admit
2469            );
2470        }
2471        assert_eq!(
2472            admission.decide(0, usize::MAX, start),
2473            PeerAdmissionDecision::RateLimited
2474        );
2475        let almost = start + PEER_ADMISSION_WINDOW - Duration::from_millis(1);
2476        assert_eq!(
2477            admission.decide(0, usize::MAX, almost),
2478            PeerAdmissionDecision::RateLimited
2479        );
2480
2481        let next_window = start + PEER_ADMISSION_WINDOW;
2482        assert_eq!(
2483            admission.decide(0, usize::MAX, next_window),
2484            PeerAdmissionDecision::Admit
2485        );
2486    }
2487
2488    #[test]
2489    fn peer_admission_cap_takes_precedence_and_does_not_use_window() {
2490        let mut admission = PeerAdmission::default();
2491        let now = Instant::now();
2492        for _ in 0..MAX_NEW_PEERS_PER_WINDOW * 2 {
2493            assert_eq!(
2494                admission.decide(4, 4, now),
2495                PeerAdmissionDecision::AtCapacity
2496            );
2497        }
2498        assert_eq!(admission.decide(3, 4, now), PeerAdmissionDecision::Admit);
2499    }
2500
2501    #[test]
2502    fn peer_refusal_warnings_are_spaced_out() {
2503        let mut admission = PeerAdmission::default();
2504        let start = Instant::now();
2505        assert_eq!(admission.note_refusal(start), Some(1));
2506        assert_eq!(admission.note_refusal(start), None);
2507        assert_eq!(admission.note_refusal(start + Duration::from_secs(1)), None);
2508        assert_eq!(
2509            admission.note_refusal(start + PEER_REFUSAL_LOG_INTERVAL),
2510            Some(3)
2511        );
2512    }
2513
2514    #[test]
2515    fn peers_advertise_no_more_than_the_receive_buffer_takes() {
2516        let shared = Arc::new(Mutex::new(SharedState::new(Arc::new(AtomicU64::new(900)))));
2517        let runtime = Arc::new(Mutex::new(AutoRuntime::from_config(&AutoConfig::default())));
2518        let event = add_peer(AddPeerContext {
2519            shared: &shared,
2520            peer_key: &PeerKey {
2521                link_local_addr: "fe80::1".into(),
2522                if_index: 1,
2523            },
2524            data_port: DEFAULT_DATA_PORT,
2525            name: "auto-test",
2526            ifname: "test0",
2527            configured_bitrate: BITRATE_GUESS,
2528            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
2529            runtime: &runtime,
2530            dynamic_template: None,
2531            underlay_mark: None,
2532            inline: false,
2533        });
2534        let Some(Event::InterfaceUp(_, Some(_), Some(info))) = event else {
2535            panic!("expected the new peer's InterfaceUp");
2536        };
2537        // Packets up to the MTU must fit the data receivers' buffer, or
2538        // link traffic above it is truncated and lost.
2539        assert_eq!(info.mtu as usize, HW_MTU);
2540    }
2541
2542    #[test]
2543    fn remove_peers_for_worker_resets_peer_admission() {
2544        let next_id = Arc::new(AtomicU64::new(100));
2545        let shared = Arc::new(Mutex::new(SharedState::new(next_id)));
2546        let now = Instant::now();
2547        {
2548            let mut state = lock_or_recover(&shared, "auto shared state");
2549            for _ in 0..MAX_NEW_PEERS_PER_WINDOW {
2550                state.admit_new_peer("eth0", now);
2551            }
2552            assert_eq!(
2553                state.admit_new_peer("eth0", now),
2554                PeerAdmissionDecision::RateLimited
2555            );
2556        }
2557        let (tx, _rx) = crate::event::channel();
2558        let key = AutoWorkerKey {
2559            ifname: "eth0".to_string(),
2560            if_index: 2,
2561            link_local_addr: "fe80::10".to_string(),
2562        };
2563        remove_peers_for_worker(&shared, &tx, &key);
2564        assert_eq!(
2565            lock_or_recover(&shared, "auto shared state").admit_new_peer("eth0", now),
2566            PeerAdmissionDecision::Admit
2567        );
2568    }
2569
2570    // ── Constants ────────────────────────────────────────────────────
2571
2572    #[test]
2573    fn constants_match_python() {
2574        assert_eq!(DEFAULT_DISCOVERY_PORT, 29716);
2575        assert_eq!(DEFAULT_DATA_PORT, 42671);
2576        assert_eq!(HW_MTU, 1196);
2577        assert_eq!(MULTI_IF_DEQUE_LEN, 48);
2578        assert!((MULTI_IF_DEQUE_TTL - 0.75).abs() < f64::EPSILON);
2579        assert!((PEERING_TIMEOUT - 22.0).abs() < f64::EPSILON);
2580        assert!((ANNOUNCE_INTERVAL - 1.6).abs() < f64::EPSILON);
2581        assert!((PEER_JOB_INTERVAL - 4.0).abs() < f64::EPSILON);
2582        assert!((MCAST_ECHO_TIMEOUT - 6.5).abs() < f64::EPSILON);
2583        assert_eq!(BITRATE_GUESS, 10_000_000);
2584    }
2585
2586    #[test]
2587    fn unicast_discovery_port() {
2588        // Python: unicast_discovery_port = discovery_port + 1
2589        let unicast_port = DEFAULT_DISCOVERY_PORT + 1;
2590        assert_eq!(unicast_port, 29717);
2591    }
2592
2593    #[test]
2594    fn reverse_peering_interval() {
2595        let interval = ANNOUNCE_INTERVAL * REVERSE_PEERING_MULTIPLIER;
2596        assert!((interval - 5.2).abs() < 0.01);
2597    }
2598}