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