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