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::sync::atomic::{AtomicBool, AtomicU64, Ordering};
18use std::sync::{Arc, Mutex};
19use std::thread;
20use std::time::Duration;
21
22use rns_core::transport::types::InterfaceId;
23
24use crate::event::{Event, EventSender};
25use crate::interface::{lock_or_recover, Writer};
26
27// ── Constants (matching Python AutoInterface) ──────────────────────────────
28
29/// Default UDP port for multicast discovery.
30pub const DEFAULT_DISCOVERY_PORT: u16 = 29716;
31
32/// Default UDP port for unicast data exchange.
33pub const DEFAULT_DATA_PORT: u16 = 42671;
34
35/// Default group identifier.
36pub const DEFAULT_GROUP_ID: &[u8] = b"reticulum";
37
38/// Default IFAC size for AutoInterface (bytes).
39pub const DEFAULT_IFAC_SIZE: usize = 16;
40
41/// Hardware MTU for AutoInterface packets.
42pub const HW_MTU: usize = 1196;
43
44/// Multicast scope: link-local.
45pub const SCOPE_LINK: &str = "2";
46/// Multicast scope: admin-local.
47pub const SCOPE_ADMIN: &str = "4";
48/// Multicast scope: site-local.
49pub const SCOPE_SITE: &str = "5";
50/// Multicast scope: organization-local.
51pub const SCOPE_ORGANISATION: &str = "8";
52/// Multicast scope: global.
53pub const SCOPE_GLOBAL: &str = "e";
54
55/// Permanent multicast address type.
56pub const MULTICAST_PERMANENT_ADDRESS_TYPE: &str = "0";
57/// Temporary multicast address type.
58pub const MULTICAST_TEMPORARY_ADDRESS_TYPE: &str = "1";
59
60/// How long before a peer is considered timed out (seconds).
61pub const PEERING_TIMEOUT: f64 = 22.0;
62
63/// How often to send multicast discovery announcements (seconds).
64pub const ANNOUNCE_INTERVAL: f64 = 1.6;
65
66/// How often to run peer maintenance jobs (seconds).
67pub const PEER_JOB_INTERVAL: f64 = 4.0;
68
69/// Multicast echo timeout (seconds). Used for carrier detection.
70pub const MCAST_ECHO_TIMEOUT: f64 = 6.5;
71
72/// Default bitrate guess for AutoInterface (10 Mbps).
73pub const BITRATE_GUESS: u64 = 10_000_000;
74
75/// Deduplication deque size.
76pub const MULTI_IF_DEQUE_LEN: usize = 48;
77
78/// Deduplication deque entry TTL (seconds).
79pub const MULTI_IF_DEQUE_TTL: f64 = 0.75;
80
81/// Reverse peering interval multiplier (announce_interval * 3.25).
82pub const REVERSE_PEERING_MULTIPLIER: f64 = 3.25;
83
84/// How often AutoInterface rescans local link-local interfaces.
85pub const AUTO_RESCAN_INTERVAL: f64 = 1.25;
86
87/// Interfaces always ignored.
88pub const ALL_IGNORE_IFS: &[&str] = &["lo0"];
89
90#[cfg_attr(not(any(target_os = "android", test)), allow(dead_code))]
91const ANDROID_IGNORE_IFS: &[&str] = &[
92    "dummy0", "lo", "tun0", "rmnet0", "rmnet1", "rmnet2", "rmnet3", "rmnet4", "rmnet5", "rmnet6",
93    "rmnet7",
94];
95
96// ── Configuration ──────────────────────────────────────────────────────────
97
98/// Configuration for an AutoInterface.
99#[derive(Debug, Clone)]
100pub struct AutoConfig {
101    pub name: String,
102    pub group_id: Vec<u8>,
103    pub discovery_scope: String,
104    pub discovery_port: u16,
105    pub data_port: u16,
106    pub multicast_address_type: String,
107    pub allowed_interfaces: Vec<String>,
108    pub ignored_interfaces: Vec<String>,
109    pub configured_bitrate: u64,
110    /// Base interface ID. Per-peer IDs will be assigned dynamically.
111    pub interface_id: InterfaceId,
112    pub ingress_control: rns_core::transport::types::IngressControlConfig,
113    pub runtime: Arc<Mutex<AutoRuntime>>,
114}
115
116#[derive(Debug, Clone)]
117pub struct AutoRuntime {
118    pub announce_interval_secs: f64,
119    pub peer_timeout_secs: f64,
120    pub peer_job_interval_secs: f64,
121}
122
123impl AutoRuntime {
124    pub fn from_config(_config: &AutoConfig) -> Self {
125        Self {
126            announce_interval_secs: ANNOUNCE_INTERVAL,
127            peer_timeout_secs: PEERING_TIMEOUT,
128            peer_job_interval_secs: PEER_JOB_INTERVAL,
129        }
130    }
131}
132
133#[derive(Debug, Clone)]
134pub struct AutoRuntimeConfigHandle {
135    pub interface_name: String,
136    pub runtime: Arc<Mutex<AutoRuntime>>,
137    pub startup: AutoRuntime,
138}
139
140impl Default for AutoConfig {
141    fn default() -> Self {
142        let mut config = AutoConfig {
143            name: String::new(),
144            group_id: DEFAULT_GROUP_ID.to_vec(),
145            discovery_scope: SCOPE_LINK.to_string(),
146            discovery_port: DEFAULT_DISCOVERY_PORT,
147            data_port: DEFAULT_DATA_PORT,
148            multicast_address_type: MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string(),
149            allowed_interfaces: Vec::new(),
150            ignored_interfaces: Vec::new(),
151            configured_bitrate: BITRATE_GUESS,
152            interface_id: InterfaceId(0),
153            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
154            runtime: Arc::new(Mutex::new(AutoRuntime {
155                announce_interval_secs: ANNOUNCE_INTERVAL,
156                peer_timeout_secs: PEERING_TIMEOUT,
157                peer_job_interval_secs: PEER_JOB_INTERVAL,
158            })),
159        };
160        let startup = AutoRuntime::from_config(&config);
161        config.runtime = Arc::new(Mutex::new(startup));
162        config
163    }
164}
165
166// ── Multicast address derivation ───────────────────────────────────────────
167
168/// Derive the IPv6 multicast discovery address from group_id, scope, and address type.
169///
170/// Algorithm (matching Python):
171///   1. group_hash = SHA-256(group_id)
172///   2. Build suffix from hash bytes 2..14 as 6 little-endian 16-bit words
173///   3. First word is hardcoded "0"
174///   4. Prefix = "ff" + address_type + scope
175pub fn derive_multicast_address(group_id: &[u8], address_type: &str, scope: &str) -> String {
176    let group_hash = rns_crypto::sha256::sha256(group_id);
177    let g = &group_hash;
178
179    // Build 6 LE 16-bit words from bytes 2..14
180    let w1 = (g[2] as u16) << 8 | g[3] as u16;
181    let w2 = (g[4] as u16) << 8 | g[5] as u16;
182    let w3 = (g[6] as u16) << 8 | g[7] as u16;
183    let w4 = (g[8] as u16) << 8 | g[9] as u16;
184    let w5 = (g[10] as u16) << 8 | g[11] as u16;
185    let w6 = (g[12] as u16) << 8 | g[13] as u16;
186
187    format!(
188        "ff{}{}:0:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
189        address_type, scope, w1, w2, w3, w4, w5, w6
190    )
191}
192
193/// Parse a multicast address string into an Ipv6Addr.
194pub fn parse_multicast_addr(addr: &str) -> Option<Ipv6Addr> {
195    addr.parse::<Ipv6Addr>().ok()
196}
197
198// ── Discovery token ────────────────────────────────────────────────────────
199
200/// Compute the discovery token: SHA-256(group_id + link_local_address_string).
201pub fn compute_discovery_token(group_id: &[u8], link_local_addr: &str) -> [u8; 32] {
202    let mut input = group_id.to_vec();
203    input.extend_from_slice(link_local_addr.as_bytes());
204    rns_crypto::sha256::sha256(&input)
205}
206
207// ── Network interface enumeration ──────────────────────────────────────────
208
209/// Information about a local network interface with an IPv6 link-local address.
210#[derive(Debug, Clone)]
211pub struct LocalInterface {
212    pub name: String,
213    pub link_local_addr: String,
214    pub index: u32,
215}
216
217#[cfg(target_os = "android")]
218fn platform_ignored_interfaces() -> &'static [&'static str] {
219    ANDROID_IGNORE_IFS
220}
221
222#[cfg(not(target_os = "android"))]
223fn platform_ignored_interfaces() -> &'static [&'static str] {
224    &[]
225}
226
227fn should_adopt_interface_name(
228    name: &str,
229    allowed: &[String],
230    ignored: &[String],
231    platform_ignored: &[&str],
232) -> bool {
233    let is_allowed = allowed.iter().any(|a| a == name);
234    let is_system_ignored = ALL_IGNORE_IFS.iter().any(|&ig| ig == name)
235        || platform_ignored.iter().any(|&ig| ig == 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}
591
592/// Start an AutoInterface. Discovers local IPv6 link-local interfaces,
593/// sets up multicast discovery, and creates UDP data servers.
594///
595/// Returns a vec of (InterfaceId, Writer) for each initial peer (typically empty
596/// since peers are discovered dynamically via InterfaceUp events).
597pub fn start(
598    config: AutoConfig,
599    tx: EventSender,
600    next_dynamic_id: Arc<AtomicU64>,
601) -> io::Result<()> {
602    let group_id = config.group_id.clone();
603    let mcast_addr_str = derive_multicast_address(
604        &group_id,
605        &config.multicast_address_type,
606        &config.discovery_scope,
607    );
608
609    let mcast_ip = match parse_multicast_addr(&mcast_addr_str) {
610        Some(ip) => ip,
611        None => {
612            return Err(io::Error::new(
613                io::ErrorKind::InvalidData,
614                format!("Invalid multicast address: {}", mcast_addr_str),
615            ));
616        }
617    };
618
619    {
620        let startup = AutoRuntime::from_config(&config);
621        *lock_or_recover(&config.runtime, "auto runtime") = startup;
622    }
623    let runtime = Arc::clone(&config.runtime);
624    let shared = Arc::new(Mutex::new(SharedState::new(next_dynamic_id)));
625    let running = Arc::new(AtomicBool::new(true));
626
627    let worker_context = AutoWorkerContext {
628        group_id,
629        mcast_ip,
630        discovery_port: config.discovery_port,
631        unicast_discovery_port: config.discovery_port + 1,
632        data_port: config.data_port,
633        name: config.name.clone(),
634        configured_bitrate: config.configured_bitrate,
635        ingress_control: config.ingress_control,
636        runtime: runtime.clone(),
637        shared: shared.clone(),
638        tx: tx.clone(),
639    };
640
641    {
642        let allowed = config.allowed_interfaces.clone();
643        let ignored = config.ignored_interfaces.clone();
644        let running = running.clone();
645        let name = config.name.clone();
646        thread::Builder::new()
647            .name(format!("auto-supervisor-{}", name))
648            .spawn(move || {
649                auto_supervisor_loop(allowed, ignored, worker_context, running, &name);
650            })?;
651    }
652
653    {
654        let shared = shared.clone();
655        let tx = tx.clone();
656        let running = running.clone();
657        let name = config.name.clone();
658        let runtime = runtime.clone();
659
660        thread::Builder::new()
661            .name(format!("auto-peer-jobs-{}", name))
662            .spawn(move || {
663                peer_jobs_loop(shared, tx, runtime, &running, &name);
664            })?;
665    }
666
667    let announce_interval = lock_or_recover(&runtime, "auto runtime").announce_interval_secs;
668    let peering_wait = Duration::from_secs_f64(announce_interval * 1.2);
669    thread::sleep(peering_wait);
670
671    {
672        let mut state = lock_or_recover(&shared, "auto shared state");
673        state.online = true;
674    }
675
676    log::info!("[{}] AutoInterface online", config.name);
677
678    Ok(())
679}
680
681fn auto_supervisor_loop(
682    allowed_interfaces: Vec<String>,
683    ignored_interfaces: Vec<String>,
684    context: AutoWorkerContext,
685    running: Arc<AtomicBool>,
686    name: &str,
687) {
688    let mut workers: HashMap<AutoWorkerKey, AutoWorkerEntry> = HashMap::new();
689
690    while running.load(Ordering::Relaxed) {
691        let interfaces = enumerate_interfaces(&allowed_interfaces, &ignored_interfaces);
692        let desired = interfaces
693            .iter()
694            .map(AutoWorkerKey::from_local_interface)
695            .collect::<Vec<_>>();
696
697        {
698            let mut state = lock_or_recover(&context.shared, "auto shared state");
699            state.link_local_addresses = desired
700                .iter()
701                .map(|key| PeerKey {
702                    link_local_addr: key.link_local_addr.clone(),
703                    if_index: key.if_index,
704                })
705                .collect();
706        }
707
708        let removed_workers = reconcile_worker_keys(workers.keys().cloned(), desired.clone())
709            .into_iter()
710            .filter_map(|action| match action {
711                WorkerReconcileAction::Remove(key) => Some(key),
712                WorkerReconcileAction::Add(_) => None,
713            })
714            .collect::<Vec<_>>();
715        for key in &removed_workers {
716            remove_peers_for_worker(&context.shared, &context.tx, key);
717        }
718
719        if desired.is_empty() && workers.is_empty() {
720            log::warn!("[{}] No suitable IPv6 link-local interfaces found", name);
721        }
722
723        reconcile_auto_workers(&mut workers, desired, |key| {
724            start_auto_worker(key, &context)
725        });
726
727        thread::sleep(Duration::from_secs_f64(AUTO_RESCAN_INTERVAL));
728    }
729
730    for (key, worker) in workers {
731        remove_peers_for_worker(&context.shared, &context.tx, &key);
732        worker.stop();
733    }
734}
735
736fn start_auto_worker(
737    key: &AutoWorkerKey,
738    context: &AutoWorkerContext,
739) -> io::Result<RunningAutoWorker> {
740    let mcast_socket =
741        create_multicast_recv_socket(&context.mcast_ip, context.discovery_port, key.if_index)?;
742    let unicast_socket = create_unicast_recv_socket(
743        &key.link_local_addr,
744        context.unicast_discovery_port,
745        key.if_index,
746    )?;
747    let data_socket =
748        create_data_recv_socket(&key.link_local_addr, context.data_port, key.if_index)?;
749
750    let worker_running = Arc::new(AtomicBool::new(true));
751
752    {
753        let group_id = context.group_id.clone();
754        let link_local = key.link_local_addr.clone();
755        let running = worker_running.clone();
756        let name = context.name.clone();
757        let runtime = context.runtime.clone();
758        let mcast_ip = context.mcast_ip;
759        let discovery_port = context.discovery_port;
760        let if_index = key.if_index;
761        thread::Builder::new()
762            .name(format!("auto-disc-tx-{}", key.ifname))
763            .spawn(move || {
764                discovery_sender_loop(
765                    &group_id,
766                    &link_local,
767                    &mcast_ip,
768                    discovery_port,
769                    if_index,
770                    runtime,
771                    &running,
772                    &name,
773                );
774            })?;
775    }
776
777    {
778        let group_id = context.group_id.clone();
779        let shared = context.shared.clone();
780        let tx = context.tx.clone();
781        let running = worker_running.clone();
782        let name = context.name.clone();
783        let ifname = key.ifname.clone();
784        let if_index = key.if_index;
785        let runtime = context.runtime.clone();
786        let data_port = context.data_port;
787        let configured_bitrate = context.configured_bitrate;
788        let ingress_control = context.ingress_control;
789        thread::Builder::new()
790            .name(format!("auto-disc-rx-{}", key.ifname))
791            .spawn(move || {
792                discovery_receiver_loop(
793                    mcast_socket,
794                    &group_id,
795                    shared,
796                    tx,
797                    &running,
798                    &name,
799                    &ifname,
800                    if_index,
801                    data_port,
802                    configured_bitrate,
803                    ingress_control,
804                    runtime,
805                );
806            })?;
807    }
808
809    {
810        let group_id = context.group_id.clone();
811        let shared = context.shared.clone();
812        let tx = context.tx.clone();
813        let running = worker_running.clone();
814        let name = context.name.clone();
815        let ifname = key.ifname.clone();
816        let if_index = key.if_index;
817        let runtime = context.runtime.clone();
818        let data_port = context.data_port;
819        let configured_bitrate = context.configured_bitrate;
820        let ingress_control = context.ingress_control;
821        thread::Builder::new()
822            .name(format!("auto-udisc-rx-{}", key.ifname))
823            .spawn(move || {
824                discovery_receiver_loop(
825                    unicast_socket,
826                    &group_id,
827                    shared,
828                    tx,
829                    &running,
830                    &name,
831                    &ifname,
832                    if_index,
833                    data_port,
834                    configured_bitrate,
835                    ingress_control,
836                    runtime,
837                );
838            })?;
839    }
840
841    {
842        let shared = context.shared.clone();
843        let tx = context.tx.clone();
844        let running = worker_running.clone();
845        let name = context.name.clone();
846        let if_index = key.if_index;
847        thread::Builder::new()
848            .name(format!("auto-data-rx-{}", key.ifname))
849            .spawn(move || {
850                data_receiver_loop(data_socket, shared, tx, &running, &name, if_index);
851            })?;
852    }
853
854    log::info!(
855        "[{}] AutoInterface worker online on {} {}%{}",
856        context.name,
857        key.ifname,
858        key.link_local_addr,
859        key.if_index
860    );
861
862    Ok(RunningAutoWorker {
863        running: worker_running,
864    })
865}
866
867// ── Socket creation helpers ────────────────────────────────────────────────
868
869fn create_multicast_recv_socket(
870    mcast_ip: &Ipv6Addr,
871    port: u16,
872    if_index: u32,
873) -> io::Result<UdpSocket> {
874    let socket = socket2::Socket::new(
875        socket2::Domain::IPV6,
876        socket2::Type::DGRAM,
877        Some(socket2::Protocol::UDP),
878    )?;
879
880    socket.set_reuse_address(true)?;
881    #[cfg(not(target_os = "windows"))]
882    socket.set_reuse_port(true)?;
883
884    // Bind to [::]:port on the specific interface
885    let bind_addr = SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, port, 0, if_index);
886    socket.bind(&bind_addr.into())?;
887
888    // Join multicast group on the specific interface
889    socket.join_multicast_v6(mcast_ip, if_index)?;
890
891    socket.set_nonblocking(false)?;
892    let std_socket: UdpSocket = socket.into();
893    std_socket.set_read_timeout(Some(Duration::from_secs(2)))?;
894    Ok(std_socket)
895}
896
897fn create_unicast_recv_socket(link_local: &str, port: u16, if_index: u32) -> io::Result<UdpSocket> {
898    let ip: Ipv6Addr = link_local
899        .parse()
900        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad IPv6: {}", e)))?;
901
902    let socket = socket2::Socket::new(
903        socket2::Domain::IPV6,
904        socket2::Type::DGRAM,
905        Some(socket2::Protocol::UDP),
906    )?;
907
908    socket.set_reuse_address(true)?;
909    #[cfg(not(target_os = "windows"))]
910    socket.set_reuse_port(true)?;
911
912    let bind_addr = SocketAddrV6::new(ip, port, 0, if_index);
913    socket.bind(&bind_addr.into())?;
914
915    socket.set_nonblocking(false)?;
916    let std_socket: UdpSocket = socket.into();
917    std_socket.set_read_timeout(Some(Duration::from_secs(2)))?;
918    Ok(std_socket)
919}
920
921fn create_data_recv_socket(link_local: &str, port: u16, if_index: u32) -> io::Result<UdpSocket> {
922    let ip: Ipv6Addr = link_local
923        .parse()
924        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad IPv6: {}", e)))?;
925
926    let socket = socket2::Socket::new(
927        socket2::Domain::IPV6,
928        socket2::Type::DGRAM,
929        Some(socket2::Protocol::UDP),
930    )?;
931
932    socket.set_reuse_address(true)?;
933    #[cfg(not(target_os = "windows"))]
934    socket.set_reuse_port(true)?;
935
936    let bind_addr = SocketAddrV6::new(ip, port, 0, if_index);
937    socket.bind(&bind_addr.into())?;
938
939    socket.set_nonblocking(false)?;
940    let std_socket: UdpSocket = socket.into();
941    std_socket.set_read_timeout(Some(Duration::from_secs(2)))?;
942    Ok(std_socket)
943}
944
945// ── Thread loops ───────────────────────────────────────────────────────────
946
947/// Discovery sender: periodically sends discovery token via multicast.
948fn discovery_sender_loop(
949    group_id: &[u8],
950    link_local_addr: &str,
951    mcast_ip: &Ipv6Addr,
952    discovery_port: u16,
953    if_index: u32,
954    runtime: Arc<Mutex<AutoRuntime>>,
955    running: &AtomicBool,
956    name: &str,
957) {
958    let token = compute_discovery_token(group_id, link_local_addr);
959
960    while running.load(Ordering::Relaxed) {
961        // Create a fresh socket for each send (matches Python)
962        if let Ok(socket) = UdpSocket::bind("[::]:0") {
963            // Set multicast interface
964            let if_bytes = if_index.to_ne_bytes();
965            unsafe {
966                libc::setsockopt(
967                    socket_fd(&socket),
968                    libc::IPPROTO_IPV6,
969                    libc::IPV6_MULTICAST_IF,
970                    if_bytes.as_ptr() as *const libc::c_void,
971                    4,
972                );
973            }
974
975            let target = SocketAddrV6::new(*mcast_ip, discovery_port, 0, 0);
976            if let Err(e) = socket.send_to(&token, target) {
977                log::debug!("[{}] multicast send error: {}", name, e);
978            }
979        }
980
981        let sleep_dur = Duration::from_secs_f64(
982            lock_or_recover(&runtime, "auto runtime")
983                .announce_interval_secs
984                .max(0.1),
985        );
986        thread::sleep(sleep_dur);
987    }
988}
989
990/// Discovery receiver: listens for discovery tokens and adds peers.
991fn discovery_receiver_loop(
992    socket: UdpSocket,
993    group_id: &[u8],
994    shared: Arc<Mutex<SharedState>>,
995    tx: EventSender,
996    running: &AtomicBool,
997    name: &str,
998    ifname: &str,
999    if_index: u32,
1000    data_port: u16,
1001    configured_bitrate: u64,
1002    ingress_control: rns_core::transport::types::IngressControlConfig,
1003    runtime: Arc<Mutex<AutoRuntime>>,
1004) {
1005    let mut buf = [0u8; 1024];
1006
1007    while running.load(Ordering::Relaxed) {
1008        match socket.recv_from(&mut buf) {
1009            Ok((n, src)) => {
1010                if n < 32 {
1011                    continue;
1012                }
1013
1014                // Extract source IPv6 address
1015                let src_addr = match src {
1016                    std::net::SocketAddr::V6(v6) => v6,
1017                    _ => continue,
1018                };
1019                let peer_key = peer_key_from_src(&src_addr, if_index);
1020                let src_ip = peer_key.link_local_addr.clone();
1021
1022                let peering_hash = &buf[..32];
1023                let expected = compute_discovery_token(group_id, &src_ip);
1024
1025                if peering_hash != expected {
1026                    log::debug!("[{}] invalid peering hash from {}", name, src_ip);
1027                    continue;
1028                }
1029
1030                // Check if online
1031                let state = lock_or_recover(&shared, "auto shared state");
1032                if !state.online {
1033                    // Not fully initialized yet, but still accept for initial peering
1034                    // (Python processes after final_init_done)
1035                }
1036
1037                // Check if it's our own echo
1038                if state.link_local_addresses.contains(&peer_key) {
1039                    // Multicast echo from ourselves — just record it
1040                    drop(state);
1041                    continue;
1042                }
1043
1044                // Check if already known
1045                if state.peers.contains_key(&peer_key) {
1046                    let now = crate::time::now();
1047                    drop(state);
1048                    let mut state = lock_or_recover(&shared, "auto shared state");
1049                    state.refresh_peer(&peer_key, now);
1050                    continue;
1051                }
1052                drop(state);
1053
1054                // New peer! Create a data writer to send to them.
1055                add_peer(
1056                    &shared,
1057                    &tx,
1058                    &peer_key,
1059                    data_port,
1060                    name,
1061                    ifname,
1062                    configured_bitrate,
1063                    ingress_control,
1064                    &runtime,
1065                );
1066            }
1067            Err(ref e)
1068                if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut =>
1069            {
1070                // Timeout, loop again
1071                continue;
1072            }
1073            Err(e) => {
1074                log::warn!("[{}] discovery recv error: {}", name, e);
1075                if !running.load(Ordering::Relaxed) {
1076                    return;
1077                }
1078                thread::sleep(Duration::from_millis(100));
1079            }
1080        }
1081    }
1082}
1083
1084/// Add a new peer, creating a writer and emitting InterfaceUp.
1085fn add_peer(
1086    shared: &Arc<Mutex<SharedState>>,
1087    tx: &EventSender,
1088    peer_key: &PeerKey,
1089    data_port: u16,
1090    name: &str,
1091    ifname: &str,
1092    configured_bitrate: u64,
1093    ingress_control: rns_core::transport::types::IngressControlConfig,
1094    _runtime: &Arc<Mutex<AutoRuntime>>,
1095) {
1096    // Create UDP writer to send data to this peer
1097    let send_socket = match UdpSocket::bind("[::]:0") {
1098        Ok(s) => s,
1099        Err(e) => {
1100            log::warn!(
1101                "[{}] failed to create writer for peer {}: {}",
1102                name,
1103                peer_key.link_local_addr,
1104                e
1105            );
1106            return;
1107        }
1108    };
1109
1110    let target = match peer_target_addr(peer_key, data_port) {
1111        Ok(target) => target,
1112        Err(e) => {
1113            log::warn!(
1114                "[{}] failed to create target for peer {}%{}: {}",
1115                name,
1116                peer_key.link_local_addr,
1117                peer_key.if_index,
1118                e
1119            );
1120            return;
1121        }
1122    };
1123
1124    let mut state = lock_or_recover(shared, "auto shared state");
1125
1126    // Double-check not already added (race)
1127    if state.peers.contains_key(peer_key) {
1128        state.refresh_peer(peer_key, crate::time::now());
1129        return;
1130    }
1131
1132    let peer_id = InterfaceId(state.next_id.fetch_add(1, Ordering::Relaxed));
1133
1134    // Create a boxed writer for the driver
1135    let driver_writer: Box<dyn Writer> = Box::new(UdpWriter {
1136        socket: send_socket,
1137        target,
1138    });
1139
1140    let peer_info = rns_core::transport::types::InterfaceInfo {
1141        id: peer_id,
1142        name: format!(
1143            "{}:{}%{}",
1144            name, peer_key.link_local_addr, peer_key.if_index
1145        ),
1146        mode: rns_core::constants::MODE_FULL,
1147        recursive_prs: false,
1148        announces_from_internal: true,
1149        out_capable: true,
1150        in_capable: true,
1151        bitrate: Some(configured_bitrate),
1152        airtime_profile: None,
1153        announce_rate_target: None,
1154        announce_rate_grace: 0,
1155        announce_rate_penalty: 0.0,
1156        announce_cap: rns_core::constants::ANNOUNCE_CAP,
1157        is_local_client: false,
1158        wants_tunnel: false,
1159        tunnel_id: None,
1160        mtu: 1400,
1161        ia_freq: 0.0,
1162        ip_freq: 0.0,
1163        op_freq: 0.0,
1164        op_samples: 0,
1165        started: 0.0,
1166        ingress_control,
1167    };
1168
1169    let now = crate::time::now();
1170    state.peers.insert(
1171        peer_key.clone(),
1172        AutoPeer {
1173            interface_id: peer_id,
1174            link_local_addr: peer_key.link_local_addr.clone(),
1175            ifname: ifname.to_string(),
1176            if_index: peer_key.if_index,
1177            last_heard: now,
1178        },
1179    );
1180
1181    log::info!(
1182        "[{}] Peer discovered: {} (id={})",
1183        name,
1184        peer_key.link_local_addr,
1185        peer_id.0
1186    );
1187
1188    // Notify driver of new dynamic interface
1189    let _ = tx.send(Event::InterfaceUp(
1190        peer_id,
1191        Some(driver_writer),
1192        Some(peer_info),
1193    ));
1194}
1195
1196/// Data receiver: receives unicast UDP data from peers and dispatches as frames.
1197fn data_receiver_loop(
1198    socket: UdpSocket,
1199    shared: Arc<Mutex<SharedState>>,
1200    tx: EventSender,
1201    running: &AtomicBool,
1202    name: &str,
1203    if_index: u32,
1204) {
1205    let mut buf = [0u8; HW_MTU + 64]; // a bit extra
1206
1207    while running.load(Ordering::Relaxed) {
1208        match socket.recv_from(&mut buf) {
1209            Ok((n, src)) => {
1210                if n == 0 {
1211                    continue;
1212                }
1213
1214                let src_addr = match src {
1215                    std::net::SocketAddr::V6(v6) => v6,
1216                    _ => continue,
1217                };
1218                let peer_key = peer_key_from_src(&src_addr, if_index);
1219                let data = &buf[..n];
1220
1221                let now = crate::time::now();
1222                let data_hash = rns_crypto::sha256::sha256(data);
1223
1224                let mut state = lock_or_recover(&shared, "auto shared state");
1225
1226                if !state.online {
1227                    continue;
1228                }
1229
1230                // Deduplication
1231                if state.is_duplicate(&data_hash, now) {
1232                    continue;
1233                }
1234                state.add_dedup(data_hash, now);
1235
1236                // Refresh peer
1237                state.refresh_peer(&peer_key, now);
1238
1239                // Find the interface ID for this peer
1240                let iface_id = match state.peers.get(&peer_key) {
1241                    Some(peer) => peer.interface_id,
1242                    None => {
1243                        // Unknown peer, skip
1244                        continue;
1245                    }
1246                };
1247
1248                drop(state);
1249
1250                if tx
1251                    .send(Event::Frame {
1252                        interface_id: iface_id,
1253                        data: data.to_vec(),
1254                        rssi: None,
1255                        snr: None,
1256                    })
1257                    .is_err()
1258                {
1259                    return;
1260                }
1261            }
1262            Err(ref e)
1263                if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut =>
1264            {
1265                continue;
1266            }
1267            Err(e) => {
1268                log::warn!("[{}] data recv error: {}", name, e);
1269                if !running.load(Ordering::Relaxed) {
1270                    return;
1271                }
1272                thread::sleep(Duration::from_millis(100));
1273            }
1274        }
1275    }
1276}
1277
1278/// Peer jobs: periodically cull timed-out peers.
1279fn peer_jobs_loop(
1280    shared: Arc<Mutex<SharedState>>,
1281    tx: EventSender,
1282    runtime: Arc<Mutex<AutoRuntime>>,
1283    running: &AtomicBool,
1284    name: &str,
1285) {
1286    while running.load(Ordering::Relaxed) {
1287        let interval = Duration::from_secs_f64(
1288            lock_or_recover(&runtime, "auto runtime")
1289                .peer_job_interval_secs
1290                .max(0.1),
1291        );
1292        thread::sleep(interval);
1293
1294        let now = crate::time::now();
1295        let mut timed_out = Vec::new();
1296        let peer_timeout_secs = lock_or_recover(&runtime, "auto runtime").peer_timeout_secs;
1297
1298        {
1299            let state = lock_or_recover(&shared, "auto shared state");
1300            for (peer_key, peer) in &state.peers {
1301                if now > peer.last_heard + peer_timeout_secs {
1302                    timed_out.push((peer_key.clone(), peer.interface_id));
1303                }
1304            }
1305        }
1306
1307        for (peer_key, iface_id) in &timed_out {
1308            log::info!(
1309                "[{}] Peer timed out: {}%{}",
1310                name,
1311                peer_key.link_local_addr,
1312                peer_key.if_index
1313            );
1314            let mut state = lock_or_recover(&shared, "auto shared state");
1315            state.peers.remove(peer_key);
1316            let _ = tx.send(Event::InterfaceDown(*iface_id));
1317        }
1318    }
1319}
1320
1321// ── Helper ─────────────────────────────────────────────────────────────────
1322
1323/// Get the raw file descriptor from a UdpSocket (for setsockopt).
1324#[cfg(unix)]
1325fn socket_fd(socket: &UdpSocket) -> i32 {
1326    use std::os::unix::io::AsRawFd;
1327    socket.as_raw_fd()
1328}
1329
1330#[cfg(not(unix))]
1331fn socket_fd(_socket: &UdpSocket) -> i32 {
1332    0
1333}
1334
1335// ── Factory implementation ─────────────────────────────────────────────────
1336
1337use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
1338
1339/// Factory for `AutoInterface`.
1340pub struct AutoFactory;
1341
1342impl InterfaceFactory for AutoFactory {
1343    fn type_name(&self) -> &str {
1344        "AutoInterface"
1345    }
1346
1347    fn parse_config(
1348        &self,
1349        name: &str,
1350        id: InterfaceId,
1351        params: &HashMap<String, String>,
1352    ) -> Result<Box<dyn InterfaceConfigData>, String> {
1353        let group_id = params
1354            .get("group_id")
1355            .map(|v| v.as_bytes().to_vec())
1356            .unwrap_or_else(|| DEFAULT_GROUP_ID.to_vec());
1357
1358        let discovery_scope = params
1359            .get("discovery_scope")
1360            .map(|v| match v.to_lowercase().as_str() {
1361                "link" => SCOPE_LINK.to_string(),
1362                "admin" => SCOPE_ADMIN.to_string(),
1363                "site" => SCOPE_SITE.to_string(),
1364                "organisation" | "organization" => SCOPE_ORGANISATION.to_string(),
1365                "global" => SCOPE_GLOBAL.to_string(),
1366                _ => v.clone(),
1367            })
1368            .unwrap_or_else(|| SCOPE_LINK.to_string());
1369
1370        let discovery_port = params
1371            .get("discovery_port")
1372            .and_then(|v| v.parse().ok())
1373            .unwrap_or(DEFAULT_DISCOVERY_PORT);
1374
1375        let data_port = params
1376            .get("data_port")
1377            .and_then(|v| v.parse().ok())
1378            .unwrap_or(DEFAULT_DATA_PORT);
1379
1380        let multicast_address_type = params
1381            .get("multicast_address_type")
1382            .map(|v| match v.to_lowercase().as_str() {
1383                "permanent" => MULTICAST_PERMANENT_ADDRESS_TYPE.to_string(),
1384                "temporary" => MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string(),
1385                _ => v.clone(),
1386            })
1387            .unwrap_or_else(|| MULTICAST_TEMPORARY_ADDRESS_TYPE.to_string());
1388
1389        let configured_bitrate = params
1390            .get("configured_bitrate")
1391            .or_else(|| params.get("bitrate"))
1392            .and_then(|v| v.parse().ok())
1393            .unwrap_or(BITRATE_GUESS);
1394
1395        let allowed_interfaces = params
1396            .get("devices")
1397            .or_else(|| params.get("allowed_interfaces"))
1398            .map(|v| {
1399                v.split(',')
1400                    .map(|s| s.trim().to_string())
1401                    .filter(|s| !s.is_empty())
1402                    .collect()
1403            })
1404            .unwrap_or_default();
1405
1406        let ignored_interfaces = params
1407            .get("ignored_devices")
1408            .or_else(|| params.get("ignored_interfaces"))
1409            .map(|v| {
1410                v.split(',')
1411                    .map(|s| s.trim().to_string())
1412                    .filter(|s| !s.is_empty())
1413                    .collect()
1414            })
1415            .unwrap_or_default();
1416
1417        Ok(Box::new(AutoConfig {
1418            name: name.to_string(),
1419            group_id,
1420            discovery_scope,
1421            discovery_port,
1422            data_port,
1423            multicast_address_type,
1424            allowed_interfaces,
1425            ignored_interfaces,
1426            configured_bitrate,
1427            interface_id: id,
1428            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
1429            runtime: Arc::new(Mutex::new(AutoRuntime {
1430                announce_interval_secs: ANNOUNCE_INTERVAL,
1431                peer_timeout_secs: PEERING_TIMEOUT,
1432                peer_job_interval_secs: PEER_JOB_INTERVAL,
1433            })),
1434        }))
1435    }
1436
1437    fn start(
1438        &self,
1439        config: Box<dyn InterfaceConfigData>,
1440        ctx: StartContext,
1441    ) -> std::io::Result<StartResult> {
1442        let mut auto_config = *config.into_any().downcast::<AutoConfig>().map_err(|_| {
1443            std::io::Error::new(std::io::ErrorKind::InvalidData, "wrong config type")
1444        })?;
1445
1446        auto_config.ingress_control = ctx.ingress_control;
1447        start(auto_config, ctx.tx, ctx.next_dynamic_id)?;
1448        Ok(StartResult::Listener { control: None })
1449    }
1450}
1451
1452pub(crate) fn auto_runtime_handle_from_config(config: &AutoConfig) -> AutoRuntimeConfigHandle {
1453    AutoRuntimeConfigHandle {
1454        interface_name: config.name.clone(),
1455        runtime: Arc::clone(&config.runtime),
1456        startup: AutoRuntime::from_config(config),
1457    }
1458}
1459
1460// ── Tests ──────────────────────────────────────────────────────────────────
1461
1462#[cfg(test)]
1463mod tests {
1464    use super::*;
1465
1466    // ── Multicast address derivation ──────────────────────────────────
1467
1468    #[test]
1469    fn multicast_address_default_group() {
1470        // Python vector: ff12:0:d70b:fb1c:16e4:5e39:485e:31e1
1471        let addr = derive_multicast_address(
1472            DEFAULT_GROUP_ID,
1473            MULTICAST_TEMPORARY_ADDRESS_TYPE,
1474            SCOPE_LINK,
1475        );
1476        assert_eq!(addr, "ff12:0:d70b:fb1c:16e4:5e39:485e:31e1");
1477    }
1478
1479    #[test]
1480    fn multicast_address_custom_group() {
1481        let addr =
1482            derive_multicast_address(b"testgroup", MULTICAST_TEMPORARY_ADDRESS_TYPE, SCOPE_LINK);
1483        // Just verify format
1484        assert!(addr.starts_with("ff12:0:"));
1485        // Must be different from default
1486        assert_ne!(addr, "ff12:0:d70b:fb1c:16e4:5e39:485e:31e1");
1487    }
1488
1489    #[test]
1490    fn multicast_address_scope_admin() {
1491        let addr = derive_multicast_address(
1492            DEFAULT_GROUP_ID,
1493            MULTICAST_TEMPORARY_ADDRESS_TYPE,
1494            SCOPE_ADMIN,
1495        );
1496        assert!(addr.starts_with("ff14:0:"));
1497    }
1498
1499    #[test]
1500    fn multicast_address_permanent_type() {
1501        let addr = derive_multicast_address(
1502            DEFAULT_GROUP_ID,
1503            MULTICAST_PERMANENT_ADDRESS_TYPE,
1504            SCOPE_LINK,
1505        );
1506        assert!(addr.starts_with("ff02:0:"));
1507    }
1508
1509    #[test]
1510    fn multicast_address_parseable() {
1511        let addr = derive_multicast_address(
1512            DEFAULT_GROUP_ID,
1513            MULTICAST_TEMPORARY_ADDRESS_TYPE,
1514            SCOPE_LINK,
1515        );
1516        let ip = parse_multicast_addr(&addr);
1517        assert!(ip.is_some());
1518        assert!(ip.unwrap().is_multicast());
1519    }
1520
1521    // ── Discovery token ──────────────────────────────────────────────
1522
1523    #[test]
1524    fn discovery_token_interop() {
1525        // Python vector: fe80::1 → 97b25576749ea936b0d8a8536ffaf442d157cf47d460dcf13c48b7bd18b6c163
1526        let token = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::1");
1527        let expected = "97b25576749ea936b0d8a8536ffaf442d157cf47d460dcf13c48b7bd18b6c163";
1528        let got = token
1529            .iter()
1530            .map(|b| format!("{:02x}", b))
1531            .collect::<String>();
1532        assert_eq!(got, expected);
1533    }
1534
1535    #[test]
1536    fn discovery_token_interop_2() {
1537        // Python vector: fe80::dead:beef:1234:5678
1538        let token = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::dead:beef:1234:5678");
1539        let expected = "46b6ec7595504b6a35f06bd4bfff71567fb82fcf2706cd361bab20409c42d072";
1540        let got = token
1541            .iter()
1542            .map(|b| format!("{:02x}", b))
1543            .collect::<String>();
1544        assert_eq!(got, expected);
1545    }
1546
1547    #[test]
1548    fn discovery_token_different_groups() {
1549        let t1 = compute_discovery_token(b"reticulum", "fe80::1");
1550        let t2 = compute_discovery_token(b"othergroup", "fe80::1");
1551        assert_ne!(t1, t2);
1552    }
1553
1554    #[test]
1555    fn discovery_token_different_addrs() {
1556        let t1 = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::1");
1557        let t2 = compute_discovery_token(DEFAULT_GROUP_ID, "fe80::2");
1558        assert_ne!(t1, t2);
1559    }
1560
1561    // ── Deduplication ────────────────────────────────────────────────
1562
1563    #[test]
1564    fn dedup_basic() {
1565        let next_id = Arc::new(AtomicU64::new(1));
1566        let mut state = SharedState::new(next_id);
1567
1568        let hash = [0xAA; 32];
1569        let now = 1000.0;
1570
1571        assert!(!state.is_duplicate(&hash, now));
1572        state.add_dedup(hash, now);
1573        assert!(state.is_duplicate(&hash, now));
1574    }
1575
1576    #[test]
1577    fn dedup_expired() {
1578        let next_id = Arc::new(AtomicU64::new(1));
1579        let mut state = SharedState::new(next_id);
1580
1581        let hash = [0xBB; 32];
1582        state.add_dedup(hash, 1000.0);
1583
1584        // Within TTL
1585        assert!(state.is_duplicate(&hash, 1000.5));
1586        // Expired
1587        assert!(!state.is_duplicate(&hash, 1001.0));
1588    }
1589
1590    #[test]
1591    fn dedup_max_length() {
1592        let next_id = Arc::new(AtomicU64::new(1));
1593        let mut state = SharedState::new(next_id);
1594
1595        // Fill beyond max
1596        for i in 0..MULTI_IF_DEQUE_LEN + 10 {
1597            let mut hash = [0u8; 32];
1598            hash[0] = (i & 0xFF) as u8;
1599            hash[1] = ((i >> 8) & 0xFF) as u8;
1600            state.add_dedup(hash, 1000.0);
1601        }
1602
1603        assert_eq!(state.dedup_deque.len(), MULTI_IF_DEQUE_LEN);
1604    }
1605
1606    // ── Peer tracking ────────────────────────────────────────────────
1607
1608    #[test]
1609    fn peer_refresh() {
1610        let next_id = Arc::new(AtomicU64::new(100));
1611        let mut state = SharedState::new(next_id);
1612        let key = peer_key("fe80::1", 2);
1613
1614        state.peers.insert(
1615            key.clone(),
1616            AutoPeer {
1617                interface_id: InterfaceId(100),
1618                link_local_addr: key.link_local_addr.clone(),
1619                ifname: "eth0".to_string(),
1620                if_index: key.if_index,
1621                last_heard: 1000.0,
1622            },
1623        );
1624
1625        state.refresh_peer(&key, 2000.0);
1626        assert_eq!(state.peers[&key].last_heard, 2000.0);
1627    }
1628
1629    #[test]
1630    fn peer_not_found_refresh() {
1631        let next_id = Arc::new(AtomicU64::new(100));
1632        let mut state = SharedState::new(next_id);
1633        // Should not panic
1634        state.refresh_peer(&peer_key("fe80::999", 4), 1000.0);
1635    }
1636
1637    fn peer_key(addr: &str, if_index: u32) -> PeerKey {
1638        PeerKey {
1639            link_local_addr: addr.to_string(),
1640            if_index,
1641        }
1642    }
1643
1644    #[test]
1645    fn peer_key_distinguishes_same_link_local_on_different_interfaces() {
1646        let next_id = Arc::new(AtomicU64::new(100));
1647        let mut state = SharedState::new(next_id);
1648        let eth = peer_key("fe80::1", 2);
1649        let wlan = peer_key("fe80::1", 4);
1650
1651        state.peers.insert(
1652            eth.clone(),
1653            AutoPeer {
1654                interface_id: InterfaceId(100),
1655                link_local_addr: eth.link_local_addr.clone(),
1656                ifname: "eth0".to_string(),
1657                if_index: eth.if_index,
1658                last_heard: 1000.0,
1659            },
1660        );
1661        state.peers.insert(
1662            wlan.clone(),
1663            AutoPeer {
1664                interface_id: InterfaceId(101),
1665                link_local_addr: wlan.link_local_addr.clone(),
1666                ifname: "wlan0".to_string(),
1667                if_index: wlan.if_index,
1668                last_heard: 1000.0,
1669            },
1670        );
1671
1672        assert_eq!(state.peers.len(), 2);
1673        assert_eq!(state.peers[&eth].interface_id, InterfaceId(100));
1674        assert_eq!(state.peers[&wlan].interface_id, InterfaceId(101));
1675    }
1676
1677    #[test]
1678    fn peer_target_addr_uses_scope_id() {
1679        let target = peer_target_addr(&peer_key("fe80::1", 7), 42671).unwrap();
1680
1681        assert_eq!(target.port(), 42671);
1682        assert_eq!(target.scope_id(), 7);
1683    }
1684
1685    #[test]
1686    fn remove_peers_for_worker_emits_interface_down() {
1687        let next_id = Arc::new(AtomicU64::new(100));
1688        let shared = Arc::new(Mutex::new(SharedState::new(next_id)));
1689        let worker = worker_key("wlan0", 4, "fe80::abcd");
1690        let removed = peer_key("fe80::1", 4);
1691        let kept = peer_key("fe80::1", 5);
1692        let (tx, rx) = crate::event::channel_with_capacity(4);
1693
1694        {
1695            let mut state = lock_or_recover(&shared, "test auto shared state");
1696            state.peers.insert(
1697                removed.clone(),
1698                AutoPeer {
1699                    interface_id: InterfaceId(100),
1700                    link_local_addr: removed.link_local_addr.clone(),
1701                    ifname: "wlan0".to_string(),
1702                    if_index: removed.if_index,
1703                    last_heard: 1000.0,
1704                },
1705            );
1706            state.peers.insert(
1707                kept.clone(),
1708                AutoPeer {
1709                    interface_id: InterfaceId(101),
1710                    link_local_addr: kept.link_local_addr.clone(),
1711                    ifname: "wlan1".to_string(),
1712                    if_index: kept.if_index,
1713                    last_heard: 1000.0,
1714                },
1715            );
1716        }
1717
1718        remove_peers_for_worker(&shared, &tx, &worker);
1719
1720        let state = lock_or_recover(&shared, "test auto shared state");
1721        assert!(!state.peers.contains_key(&removed));
1722        assert!(state.peers.contains_key(&kept));
1723        drop(state);
1724
1725        assert!(matches!(
1726            rx.recv_timeout(std::time::Duration::from_secs(1)).unwrap(),
1727            Event::InterfaceDown(InterfaceId(100))
1728        ));
1729        assert!(rx.try_recv().is_err());
1730    }
1731
1732    // ── Network interface enumeration ────────────────────────────────
1733
1734    #[test]
1735    fn enumerate_returns_vec() {
1736        // This test just verifies the function runs without crashing.
1737        // Results depend on the system's network configuration.
1738        let interfaces = enumerate_interfaces(&[], &[]);
1739        // On CI/test machines, we may or may not have IPv6 link-local
1740        for iface in &interfaces {
1741            assert!(!iface.name.is_empty());
1742            assert!(iface.link_local_addr.starts_with("fe80"));
1743            assert!(iface.index > 0);
1744        }
1745    }
1746
1747    #[test]
1748    fn enumerate_with_ignored() {
1749        // Ignore everything
1750        let interfaces = enumerate_interfaces(
1751            &[],
1752            &[
1753                "lo".to_string(),
1754                "eth0".to_string(),
1755                "wlan0".to_string(),
1756                "enp0s3".to_string(),
1757                "docker0".to_string(),
1758            ],
1759        );
1760        // May still have some interfaces, but known ones should be filtered
1761        for iface in &interfaces {
1762            assert_ne!(iface.name, "lo");
1763            assert_ne!(iface.name, "eth0");
1764            assert_ne!(iface.name, "wlan0");
1765        }
1766    }
1767
1768    #[test]
1769    fn enumerate_with_allowed_nonexistent() {
1770        // Only allow an interface that doesn't exist
1771        let interfaces = enumerate_interfaces(&["nonexistent_if_12345".to_string()], &[]);
1772        assert!(interfaces.is_empty());
1773    }
1774
1775    fn worker_key(name: &str, index: u32, addr: &str) -> AutoWorkerKey {
1776        AutoWorkerKey {
1777            ifname: name.to_string(),
1778            if_index: index,
1779            link_local_addr: addr.to_string(),
1780        }
1781    }
1782
1783    #[test]
1784    fn reconcile_worker_keys_adds_new_interface() {
1785        let desired = vec![worker_key("wlan0", 4, "fe80::1")];
1786
1787        assert_eq!(
1788            reconcile_worker_keys(Vec::new(), desired.clone()),
1789            vec![WorkerReconcileAction::Add(desired[0].clone())]
1790        );
1791    }
1792
1793    #[test]
1794    fn reconcile_worker_keys_keeps_unchanged_interface() {
1795        let key = worker_key("wlan0", 4, "fe80::1");
1796
1797        assert!(reconcile_worker_keys(vec![key.clone()], vec![key]).is_empty());
1798    }
1799
1800    #[test]
1801    fn reconcile_worker_keys_removes_missing_interface() {
1802        let active = vec![worker_key("wlan0", 4, "fe80::1")];
1803
1804        assert_eq!(
1805            reconcile_worker_keys(active.clone(), Vec::new()),
1806            vec![WorkerReconcileAction::Remove(active[0].clone())]
1807        );
1808    }
1809
1810    #[test]
1811    fn reconcile_worker_keys_replaces_changed_interface_identity() {
1812        let old = worker_key("wlan0", 4, "fe80::1");
1813        let new = worker_key("wlan0", 9, "fe80::2");
1814        let actions = reconcile_worker_keys(vec![old.clone()], vec![new.clone()]);
1815
1816        assert!(actions.contains(&WorkerReconcileAction::Remove(old)));
1817        assert!(actions.contains(&WorkerReconcileAction::Add(new)));
1818        assert_eq!(actions.len(), 2);
1819    }
1820
1821    #[test]
1822    fn reconcile_auto_workers_starts_new_worker() {
1823        let key = worker_key("wlan0", 4, "fe80::1");
1824        let mut workers = HashMap::new();
1825        let mut starts = Vec::new();
1826
1827        reconcile_auto_workers(&mut workers, vec![key.clone()], |key| {
1828            starts.push(key.clone());
1829            Ok(RunningAutoWorker {
1830                running: Arc::new(AtomicBool::new(true)),
1831            })
1832        });
1833
1834        assert_eq!(starts, vec![key.clone()]);
1835        assert!(matches!(
1836            workers.get(&key),
1837            Some(AutoWorkerEntry::Running(_))
1838        ));
1839    }
1840
1841    #[test]
1842    fn reconcile_auto_workers_stops_removed_worker() {
1843        let key = worker_key("wlan0", 4, "fe80::1");
1844        let running = Arc::new(AtomicBool::new(true));
1845        let mut workers = HashMap::from([(
1846            key.clone(),
1847            AutoWorkerEntry::Running(RunningAutoWorker {
1848                running: running.clone(),
1849            }),
1850        )]);
1851
1852        reconcile_auto_workers(&mut workers, Vec::new(), |_| unreachable!());
1853
1854        assert!(!running.load(Ordering::Relaxed));
1855        assert!(!workers.contains_key(&key));
1856    }
1857
1858    #[test]
1859    fn reconcile_auto_workers_leaves_unchanged_worker_running() {
1860        let key = worker_key("wlan0", 4, "fe80::1");
1861        let running = Arc::new(AtomicBool::new(true));
1862        let mut workers = HashMap::from([(
1863            key.clone(),
1864            AutoWorkerEntry::Running(RunningAutoWorker {
1865                running: running.clone(),
1866            }),
1867        )]);
1868        let mut starts = 0;
1869
1870        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
1871            starts += 1;
1872            unreachable!()
1873        });
1874
1875        assert_eq!(starts, 0);
1876        assert!(running.load(Ordering::Relaxed));
1877        assert!(workers.contains_key(&key));
1878    }
1879
1880    #[test]
1881    fn reconcile_auto_workers_records_pending_start_failure() {
1882        let key = worker_key("wlan0", 4, "fe80::1");
1883        let mut workers = HashMap::new();
1884
1885        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
1886            Err(io::Error::new(io::ErrorKind::AddrInUse, "busy"))
1887        });
1888
1889        assert!(matches!(workers.get(&key), Some(AutoWorkerEntry::Pending)));
1890    }
1891
1892    #[test]
1893    fn reconcile_auto_workers_retries_pending_worker() {
1894        let key = worker_key("wlan0", 4, "fe80::1");
1895        let running = Arc::new(AtomicBool::new(true));
1896        let mut workers = HashMap::from([(key.clone(), AutoWorkerEntry::Pending)]);
1897        let mut starts = 0;
1898
1899        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
1900            starts += 1;
1901            Ok(RunningAutoWorker {
1902                running: running.clone(),
1903            })
1904        });
1905
1906        assert_eq!(starts, 1);
1907        assert!(matches!(
1908            workers.get(&key),
1909            Some(AutoWorkerEntry::Running(_))
1910        ));
1911    }
1912
1913    #[test]
1914    fn reconcile_auto_workers_keeps_pending_worker_on_retry_failure() {
1915        let key = worker_key("wlan0", 4, "fe80::1");
1916        let mut workers = HashMap::from([(key.clone(), AutoWorkerEntry::Pending)]);
1917        let mut starts = 0;
1918
1919        reconcile_auto_workers(&mut workers, vec![key.clone()], |_| {
1920            starts += 1;
1921            Err(io::Error::new(io::ErrorKind::AddrInUse, "busy"))
1922        });
1923
1924        assert_eq!(starts, 1);
1925        assert!(matches!(workers.get(&key), Some(AutoWorkerEntry::Pending)));
1926    }
1927
1928    #[test]
1929    fn reconcile_auto_workers_removes_pending_worker() {
1930        let key = worker_key("wlan0", 4, "fe80::1");
1931        let mut workers = HashMap::from([(key.clone(), AutoWorkerEntry::Pending)]);
1932
1933        reconcile_auto_workers(&mut workers, Vec::new(), |_| unreachable!());
1934
1935        assert!(!workers.contains_key(&key));
1936    }
1937
1938    #[test]
1939    fn filter_skips_android_system_interfaces() {
1940        let allowed = Vec::new();
1941        let ignored = Vec::new();
1942
1943        for name in ["dummy0", "lo", "tun0", "rmnet0", "rmnet7"] {
1944            assert!(
1945                !should_adopt_interface_name(name, &allowed, &ignored, ANDROID_IGNORE_IFS),
1946                "{name} should be skipped by Android AutoInterface defaults"
1947            );
1948        }
1949    }
1950
1951    #[test]
1952    fn filter_does_not_skip_rmnet8_by_android_defaults() {
1953        assert!(should_adopt_interface_name(
1954            "rmnet8",
1955            &[],
1956            &[],
1957            ANDROID_IGNORE_IFS
1958        ));
1959    }
1960
1961    #[test]
1962    fn filter_allowed_overrides_system_ignored_interface() {
1963        assert!(should_adopt_interface_name(
1964            "rmnet0",
1965            &["rmnet0".to_string()],
1966            &[],
1967            ANDROID_IGNORE_IFS
1968        ));
1969
1970        assert!(should_adopt_interface_name(
1971            "lo0",
1972            &["lo0".to_string()],
1973            &[],
1974            &[]
1975        ));
1976    }
1977
1978    #[test]
1979    fn filter_ignored_wins_over_allowed_interface() {
1980        assert!(!should_adopt_interface_name(
1981            "rmnet0",
1982            &["rmnet0".to_string()],
1983            &["rmnet0".to_string()],
1984            ANDROID_IGNORE_IFS
1985        ));
1986    }
1987
1988    #[test]
1989    fn filter_allowed_list_excludes_unlisted_interfaces() {
1990        assert!(!should_adopt_interface_name(
1991            "wlan0",
1992            &["eth0".to_string()],
1993            &[],
1994            ANDROID_IGNORE_IFS
1995        ));
1996
1997        assert!(should_adopt_interface_name(
1998            "wlan0",
1999            &["wlan0".to_string()],
2000            &[],
2001            ANDROID_IGNORE_IFS
2002        ));
2003    }
2004
2005    // ── Config defaults ──────────────────────────────────────────────
2006
2007    #[test]
2008    fn config_defaults() {
2009        let config = AutoConfig::default();
2010        assert_eq!(config.group_id, DEFAULT_GROUP_ID);
2011        assert_eq!(config.discovery_scope, SCOPE_LINK);
2012        assert_eq!(config.discovery_port, DEFAULT_DISCOVERY_PORT);
2013        assert_eq!(config.data_port, DEFAULT_DATA_PORT);
2014        assert_eq!(
2015            config.multicast_address_type,
2016            MULTICAST_TEMPORARY_ADDRESS_TYPE
2017        );
2018        assert_eq!(config.configured_bitrate, BITRATE_GUESS);
2019        assert!(config.allowed_interfaces.is_empty());
2020        assert!(config.ignored_interfaces.is_empty());
2021    }
2022
2023    // ── Constants ────────────────────────────────────────────────────
2024
2025    #[test]
2026    fn constants_match_python() {
2027        assert_eq!(DEFAULT_DISCOVERY_PORT, 29716);
2028        assert_eq!(DEFAULT_DATA_PORT, 42671);
2029        assert_eq!(HW_MTU, 1196);
2030        assert_eq!(MULTI_IF_DEQUE_LEN, 48);
2031        assert!((MULTI_IF_DEQUE_TTL - 0.75).abs() < f64::EPSILON);
2032        assert!((PEERING_TIMEOUT - 22.0).abs() < f64::EPSILON);
2033        assert!((ANNOUNCE_INTERVAL - 1.6).abs() < f64::EPSILON);
2034        assert!((PEER_JOB_INTERVAL - 4.0).abs() < f64::EPSILON);
2035        assert!((MCAST_ECHO_TIMEOUT - 6.5).abs() < f64::EPSILON);
2036        assert_eq!(BITRATE_GUESS, 10_000_000);
2037    }
2038
2039    #[test]
2040    fn unicast_discovery_port() {
2041        // Python: unicast_discovery_port = discovery_port + 1
2042        let unicast_port = DEFAULT_DISCOVERY_PORT + 1;
2043        assert_eq!(unicast_port, 29717);
2044    }
2045
2046    #[test]
2047    fn reverse_peering_interval() {
2048        let interval = ANNOUNCE_INTERVAL * REVERSE_PEERING_MULTIPLIER;
2049        assert!((interval - 5.2).abs() < 0.01);
2050    }
2051}