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