Skip to main content

rns_net/interface/
mod.rs

1//! Network interface abstractions.
2
3#[cfg(feature = "iface-auto")]
4pub mod auto;
5#[cfg(feature = "iface-kiss")]
6pub mod ax25_kiss;
7#[cfg(feature = "iface-backbone")]
8pub mod backbone;
9#[cfg(feature = "iface-i2p")]
10pub mod i2p;
11#[cfg(feature = "iface-kiss")]
12pub mod kiss_iface;
13#[cfg(feature = "iface-local")]
14pub mod local;
15#[cfg(feature = "iface-pipe")]
16pub mod pipe;
17pub mod registry;
18#[cfg(feature = "iface-rnode")]
19pub mod rnode;
20#[cfg(feature = "iface-serial")]
21pub mod serial_iface;
22#[cfg(feature = "iface-tcp")]
23pub mod tcp;
24#[cfg(feature = "iface-tcp")]
25pub mod tcp_server;
26pub(crate) mod transmit_buffer;
27#[cfg(feature = "iface-udp")]
28pub mod udp;
29#[cfg(all(feature = "iface-weave", target_os = "linux"))]
30pub mod weave;
31
32use std::any::Any;
33use std::collections::HashMap;
34use std::io;
35use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
36use std::sync::mpsc::{sync_channel, SyncSender, TrySendError};
37use std::sync::{Arc, Mutex, MutexGuard};
38use std::thread;
39use std::time::{Duration, Instant};
40
41use crate::event::EventSender;
42use crate::ifac::IfacState;
43use rns_core::transport::types::{InterfaceId, InterfaceInfo};
44
45/// Host callback used where every Reticulum underlay socket must be explicitly
46/// excluded from an application VPN, notably Android `VpnService.protect()`.
47pub trait SocketProtector: Send + Sync {
48    fn protect(&self, fd: std::os::unix::io::RawFd) -> io::Result<()>;
49}
50
51fn socket_protector_slot() -> &'static std::sync::RwLock<Option<Arc<dyn SocketProtector>>> {
52    static SLOT: std::sync::OnceLock<std::sync::RwLock<Option<Arc<dyn SocketProtector>>>> =
53        std::sync::OnceLock::new();
54    SLOT.get_or_init(|| std::sync::RwLock::new(None))
55}
56
57/// Exclusive process-scoped socket protection registration.
58///
59/// Android VPN services should run their private node in a dedicated process.
60/// A second registration fails instead of replacing a callback used by live
61/// sockets.
62pub struct SocketProtectorGuard {
63    protector: Arc<dyn SocketProtector>,
64}
65
66pub fn install_socket_protector(
67    protector: Arc<dyn SocketProtector>,
68) -> io::Result<SocketProtectorGuard> {
69    let mut slot = socket_protector_slot()
70        .write()
71        .unwrap_or_else(|poisoned| poisoned.into_inner());
72    if slot.is_some() {
73        return Err(io::Error::new(
74            io::ErrorKind::AlreadyExists,
75            "an underlay socket protector is already installed",
76        ));
77    }
78    *slot = Some(Arc::clone(&protector));
79    Ok(SocketProtectorGuard { protector })
80}
81
82#[cfg(test)]
83mod socket_protector_tests {
84    use super::*;
85    use std::sync::atomic::{AtomicUsize, Ordering};
86
87    static TEST_LOCK: Mutex<()> = Mutex::new(());
88    struct CountingProtector(Arc<AtomicUsize>);
89    impl SocketProtector for CountingProtector {
90        fn protect(&self, _fd: std::os::unix::io::RawFd) -> io::Result<()> {
91            self.0.fetch_add(1, Ordering::Relaxed);
92            Ok(())
93        }
94    }
95
96    #[test]
97    fn underlay_helper_invokes_installed_protector_without_mark() {
98        use std::os::fd::AsRawFd;
99        let _lock = TEST_LOCK.lock().unwrap();
100        let calls = Arc::new(AtomicUsize::new(0));
101        let _guard =
102            install_socket_protector(Arc::new(CountingProtector(Arc::clone(&calls)))).unwrap();
103        let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
104        apply_underlay_mark(socket.as_raw_fd(), None).unwrap();
105        assert!(calls.load(Ordering::Relaxed) >= 1);
106    }
107}
108
109impl Drop for SocketProtectorGuard {
110    fn drop(&mut self) {
111        let mut slot = socket_protector_slot()
112            .write()
113            .unwrap_or_else(|poisoned| poisoned.into_inner());
114        if slot
115            .as_ref()
116            .is_some_and(|installed| Arc::ptr_eq(installed, &self.protector))
117        {
118            *slot = None;
119        }
120    }
121}
122
123fn protect_underlay_socket(fd: std::os::unix::io::RawFd) -> io::Result<()> {
124    let protector = socket_protector_slot()
125        .read()
126        .unwrap_or_else(|poisoned| poisoned.into_inner())
127        .clone();
128    if let Some(protector) = protector {
129        protector.protect(fd)?;
130    }
131    Ok(())
132}
133
134/// Bind a socket to a specific network interface using `SO_BINDTODEVICE`.
135///
136/// Requires `CAP_NET_RAW` or root on Linux.
137#[cfg(target_os = "linux")]
138pub fn bind_to_device(fd: std::os::unix::io::RawFd, device: &str) -> io::Result<()> {
139    let dev_bytes = device.as_bytes();
140    if dev_bytes.len() >= libc::IFNAMSIZ {
141        return Err(io::Error::new(
142            io::ErrorKind::InvalidInput,
143            format!("device name too long: {}", device),
144        ));
145    }
146    let ret = unsafe {
147        libc::setsockopt(
148            fd,
149            libc::SOL_SOCKET,
150            libc::SO_BINDTODEVICE,
151            dev_bytes.as_ptr() as *const libc::c_void,
152            dev_bytes.len() as libc::socklen_t,
153        )
154    };
155    if ret != 0 {
156        return Err(io::Error::last_os_error());
157    }
158    Ok(())
159}
160
161/// Apply a Linux firewall mark to an IP underlay socket.
162///
163/// The mark must be set before `connect()` so policy routing can keep Reticulum
164/// underlay traffic outside application-level full tunnels. Configuring a mark
165/// is fail-closed: callers must not silently create an unmarked socket when the
166/// operation is not permitted.
167#[cfg(target_os = "linux")]
168pub fn apply_underlay_mark(fd: std::os::unix::io::RawFd, mark: Option<u32>) -> io::Result<()> {
169    protect_underlay_socket(fd)?;
170    let Some(mark) = mark else {
171        return Ok(());
172    };
173    let ret = unsafe {
174        libc::setsockopt(
175            fd,
176            libc::SOL_SOCKET,
177            libc::SO_MARK,
178            &mark as *const _ as *const libc::c_void,
179            std::mem::size_of::<u32>() as libc::socklen_t,
180        )
181    };
182    if ret != 0 {
183        return Err(io::Error::last_os_error());
184    }
185    Ok(())
186}
187
188#[cfg(not(target_os = "linux"))]
189pub fn apply_underlay_mark(fd: std::os::unix::io::RawFd, mark: Option<u32>) -> io::Result<()> {
190    protect_underlay_socket(fd)?;
191    if mark.is_some() {
192        return Err(io::Error::new(
193            io::ErrorKind::Unsupported,
194            "underlay socket marks are only supported on Linux",
195        ));
196    }
197    Ok(())
198}
199
200/// Connect a TCP socket after applying underlay routing options.
201#[cfg(target_os = "linux")]
202pub fn connect_tcp_with_options(
203    addr: &std::net::SocketAddr,
204    device: Option<&str>,
205    underlay_mark: Option<u32>,
206    timeout: Duration,
207) -> io::Result<std::net::TcpStream> {
208    use std::os::unix::io::AsRawFd;
209
210    let socket = socket2::Socket::new(
211        socket2::Domain::for_address(*addr),
212        socket2::Type::STREAM,
213        Some(socket2::Protocol::TCP),
214    )?;
215    apply_underlay_mark(socket.as_raw_fd(), underlay_mark)?;
216    if let Some(device) = device {
217        bind_to_device(socket.as_raw_fd(), device)?;
218    }
219    socket.connect_timeout(&(*addr).into(), timeout)?;
220    Ok(socket.into())
221}
222
223/// Validate at node startup that the configured mark can actually be applied.
224pub fn validate_underlay_mark(mark: Option<u32>) -> io::Result<()> {
225    let Some(_) = mark else {
226        return Ok(());
227    };
228    let socket = socket2::Socket::new(
229        socket2::Domain::IPV4,
230        socket2::Type::DGRAM,
231        Some(socket2::Protocol::UDP),
232    )?;
233    use std::os::unix::io::AsRawFd;
234    apply_underlay_mark(socket.as_raw_fd(), mark)
235}
236
237/// Writable end of an interface. Held by the driver.
238///
239/// Each implementation wraps a socket + framing.
240pub trait Writer: Send {
241    fn send_frame(&mut self, data: &[u8]) -> io::Result<()>;
242
243    fn send_frames(&mut self, frames: &[Vec<u8>]) -> io::Result<()> {
244        for frame in frames {
245            self.send_frame(frame)?;
246        }
247        Ok(())
248    }
249
250    /// Resume bytes accepted by a previous call that returned `WouldBlock`.
251    fn flush(&mut self) -> io::Result<()> {
252        Ok(())
253    }
254
255    /// Share an exact byte admission budget with the async producer when this
256    /// writer uses dataplane egress control.
257    fn egress_control(&self) -> Option<EgressControl> {
258        None
259    }
260
261    fn shutdown(&mut self) {}
262}
263
264pub const DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY: usize = 256;
265
266#[derive(Clone)]
267#[doc(hidden)]
268pub struct EgressControl {
269    byte_limit: usize,
270    reserved_bytes: Arc<AtomicUsize>,
271    stalled: Arc<AtomicBool>,
272    dropped_frames: Arc<AtomicU64>,
273    dropped_bytes: Arc<AtomicU64>,
274}
275
276impl EgressControl {
277    pub(crate) fn new(byte_limit: usize) -> Self {
278        Self {
279            byte_limit,
280            reserved_bytes: Arc::new(AtomicUsize::new(0)),
281            stalled: Arc::new(AtomicBool::new(false)),
282            dropped_frames: Arc::new(AtomicU64::new(0)),
283            dropped_bytes: Arc::new(AtomicU64::new(0)),
284        }
285    }
286
287    fn try_reserve(&self, bytes: usize) -> bool {
288        if self.stalled.load(Ordering::Relaxed) {
289            return false;
290        }
291        let mut current = self.reserved_bytes.load(Ordering::Relaxed);
292        loop {
293            let Some(next) = current.checked_add(bytes) else {
294                return false;
295            };
296            if next > self.byte_limit {
297                return false;
298            }
299            match self.reserved_bytes.compare_exchange_weak(
300                current,
301                next,
302                Ordering::Relaxed,
303                Ordering::Relaxed,
304            ) {
305                Ok(_) => return true,
306                Err(observed) => current = observed,
307            }
308        }
309    }
310
311    fn release(&self, bytes: usize) {
312        self.reserved_bytes.fetch_sub(bytes, Ordering::Relaxed);
313    }
314
315    pub(crate) fn stalled(&self) -> bool {
316        self.stalled.load(Ordering::Relaxed)
317    }
318
319    pub(crate) fn set_stalled(&self, stalled: bool) {
320        self.stalled.store(stalled, Ordering::Relaxed);
321    }
322
323    pub(crate) fn record_drop(&self, bytes: usize) {
324        self.dropped_frames.fetch_add(1, Ordering::Relaxed);
325        self.dropped_bytes
326            .fetch_add(bytes as u64, Ordering::Relaxed);
327    }
328}
329
330/// Return whether an interface type normally operates on a shared medium.
331///
332/// This is a type-level hint: runtime configuration can still describe links
333/// with different physical properties. It mirrors Reticulum's interface-class
334/// defaults and is suitable for discovery/capability metadata.
335pub fn shared_medium_hint(interface_type: &str) -> bool {
336    matches!(
337        interface_type,
338        "AX25KISSInterface"
339            | "KISSInterface"
340            | "PipeInterface"
341            | "RNodeInterface"
342            | "RNodeMultiInterface"
343            | "SerialInterface"
344            | "UDPInterface"
345    )
346}
347
348pub(crate) fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, label: &str) -> MutexGuard<'a, T> {
349    match mutex.lock() {
350        Ok(guard) => guard,
351        Err(poisoned) => {
352            log::warn!("recovering poisoned mutex: {}", label);
353            poisoned.into_inner()
354        }
355    }
356}
357
358#[derive(Clone, Default)]
359pub struct ListenerControl {
360    stop: Arc<AtomicBool>,
361}
362
363impl ListenerControl {
364    pub fn new() -> Self {
365        Self {
366            stop: Arc::new(AtomicBool::new(false)),
367        }
368    }
369
370    pub fn request_stop(&self) {
371        self.stop.store(true, Ordering::Relaxed);
372    }
373
374    pub fn should_stop(&self) -> bool {
375        self.stop.load(Ordering::Relaxed)
376    }
377}
378
379#[derive(Clone, Default)]
380pub struct AsyncWriterMetrics {
381    queued_frames: Arc<AtomicUsize>,
382    worker_alive: Arc<AtomicBool>,
383    egress_control: Option<EgressControl>,
384}
385
386impl AsyncWriterMetrics {
387    pub fn queued_frames(&self) -> usize {
388        self.queued_frames.load(Ordering::Relaxed)
389    }
390
391    pub fn worker_alive(&self) -> bool {
392        self.worker_alive.load(Ordering::Relaxed)
393    }
394
395    /// Exact encoded bytes waiting in the async queue or concrete writer.
396    pub fn tx_buffered(&self) -> usize {
397        self.egress_control
398            .as_ref()
399            .map(|control| control.reserved_bytes.load(Ordering::Relaxed))
400            .unwrap_or(0)
401    }
402
403    pub fn tx_drops(&self) -> u64 {
404        self.egress_control
405            .as_ref()
406            .map(|control| control.dropped_frames.load(Ordering::Relaxed))
407            .unwrap_or(0)
408    }
409
410    pub fn tx_dropped_bytes(&self) -> u64 {
411        self.egress_control
412            .as_ref()
413            .map(|control| control.dropped_bytes.load(Ordering::Relaxed))
414            .unwrap_or(0)
415    }
416
417    pub fn tx_stalled(&self) -> bool {
418        self.egress_control
419            .as_ref()
420            .is_some_and(EgressControl::stalled)
421    }
422}
423
424struct AsyncWriter {
425    tx: SyncSender<QueuedFrame>,
426    metrics: AsyncWriterMetrics,
427}
428
429struct QueuedFrame {
430    data: Vec<u8>,
431    reserved_bytes: usize,
432}
433
434impl Writer for AsyncWriter {
435    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
436        if !self.metrics.worker_alive() {
437            return Err(io::Error::new(
438                io::ErrorKind::BrokenPipe,
439                "interface writer worker is offline",
440            ));
441        }
442
443        let reserved_bytes = if let Some(control) = &self.metrics.egress_control {
444            let framed_bytes = crate::hdlc::framed_len(data);
445            if !control.try_reserve(framed_bytes) {
446                control.record_drop(framed_bytes);
447                return Ok(());
448            }
449            framed_bytes
450        } else {
451            0
452        };
453
454        // Publish accounting before the frame becomes visible to the worker.
455        self.metrics.queued_frames.fetch_add(1, Ordering::Relaxed);
456        match self.tx.try_send(QueuedFrame {
457            data: data.to_vec(),
458            reserved_bytes,
459        }) {
460            Ok(()) => Ok(()),
461            Err(TrySendError::Full(frame)) => {
462                self.metrics.queued_frames.fetch_sub(1, Ordering::Relaxed);
463                if let Some(control) = &self.metrics.egress_control {
464                    control.release(frame.reserved_bytes);
465                }
466                Err(io::Error::new(
467                    io::ErrorKind::WouldBlock,
468                    "interface writer queue is full",
469                ))
470            }
471            Err(TrySendError::Disconnected(frame)) => {
472                self.metrics.queued_frames.fetch_sub(1, Ordering::Relaxed);
473                if let Some(control) = &self.metrics.egress_control {
474                    control.release(frame.reserved_bytes);
475                }
476                self.metrics.worker_alive.store(false, Ordering::Relaxed);
477                Err(io::Error::new(
478                    io::ErrorKind::BrokenPipe,
479                    "interface writer worker disconnected",
480                ))
481            }
482        }
483    }
484}
485
486pub fn wrap_async_writer(
487    writer: Box<dyn Writer>,
488    interface_id: InterfaceId,
489    interface_name: &str,
490    event_tx: EventSender,
491    queue_capacity: usize,
492) -> (Box<dyn Writer>, AsyncWriterMetrics) {
493    let egress_control = writer.egress_control();
494    let (tx, rx) = sync_channel::<QueuedFrame>(queue_capacity.max(1));
495    let metrics = AsyncWriterMetrics {
496        queued_frames: Arc::new(AtomicUsize::new(0)),
497        worker_alive: Arc::new(AtomicBool::new(true)),
498        egress_control,
499    };
500    let metrics_thread = metrics.clone();
501    let name = interface_name.to_string();
502
503    let spawn_result = thread::Builder::new()
504        .name(format!("iface-writer-{}", interface_id.0))
505        .spawn(move || async_writer_loop(writer, rx, interface_id, name, event_tx, metrics_thread));
506
507    if let Err(err) = spawn_result {
508        metrics.worker_alive.store(false, Ordering::Relaxed);
509        log::error!(
510            "[{}:{}] failed to spawn async writer thread: {}",
511            interface_name,
512            interface_id.0,
513            err
514        );
515        return (Box::new(DirectWriterFallback), metrics);
516    }
517
518    (
519        Box::new(AsyncWriter {
520            tx,
521            metrics: metrics.clone(),
522        }),
523        metrics,
524    )
525}
526
527struct DirectWriterFallback;
528
529impl Writer for DirectWriterFallback {
530    fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
531        Err(io::Error::other("interface writer worker unavailable"))
532    }
533}
534
535fn async_writer_loop(
536    mut writer: Box<dyn Writer>,
537    rx: std::sync::mpsc::Receiver<QueuedFrame>,
538    interface_id: InterfaceId,
539    interface_name: String,
540    event_tx: EventSender,
541    metrics: AsyncWriterMetrics,
542) {
543    while let Ok(first) = rx.recv() {
544        let mut queued = vec![first];
545        queued.extend(rx.try_iter());
546        let reserved_bytes = queued.iter().map(|frame| frame.reserved_bytes).sum();
547        let frames: Vec<_> = queued.into_iter().map(|frame| frame.data).collect();
548        metrics
549            .queued_frames
550            .fetch_sub(frames.len(), Ordering::Relaxed);
551
552        let mut result = writer.send_frames(&frames);
553        while result
554            .as_ref()
555            .is_err_and(|error| error.kind() == io::ErrorKind::WouldBlock)
556        {
557            thread::sleep(Duration::from_millis(1));
558            result = writer.flush();
559        }
560
561        if let Some(control) = &metrics.egress_control {
562            control.release(reserved_bytes);
563        }
564
565        if let Err(err) = result {
566            metrics.worker_alive.store(false, Ordering::Relaxed);
567            log::warn!(
568                "[{}:{}] async writer exiting after send failure: {}",
569                interface_name,
570                interface_id.0,
571                err
572            );
573            writer.shutdown();
574            let _ = event_tx.send(crate::event::Event::InterfaceDown(interface_id));
575            return;
576        }
577    }
578
579    metrics.worker_alive.store(false, Ordering::Relaxed);
580}
581
582pub use crate::common::interface_stats::{InterfaceStats, TrafficRates, ANNOUNCE_SAMPLE_MAX};
583
584use crate::common::management::InterfaceStatusView;
585
586/// Everything the driver tracks per interface.
587pub struct InterfaceEntry {
588    pub id: InterfaceId,
589    pub info: InterfaceInfo,
590    pub writer: Box<dyn Writer>,
591    pub async_writer_metrics: Option<AsyncWriterMetrics>,
592    /// Administrative enable/disable state.
593    pub enabled: bool,
594    pub online: bool,
595    /// True for dynamically spawned interfaces (e.g. TCP server clients).
596    /// These are fully removed on InterfaceDown rather than just marked offline.
597    pub dynamic: bool,
598    /// IFAC state for this interface, if access codes are enabled.
599    pub ifac: Option<IfacState>,
600    /// Traffic statistics.
601    pub stats: InterfaceStats,
602    /// Human-readable interface type string (e.g. "TCPClientInterface").
603    pub interface_type: String,
604    /// Next time a send should be retried after a transient WouldBlock.
605    pub send_retry_at: Option<Instant>,
606    /// Current retry backoff for transient send failures.
607    pub send_retry_backoff: Duration,
608}
609
610/// Result of starting an interface via a factory.
611// Boxing `Simple` would change the published factory result representation.
612#[allow(clippy::large_enum_variant)]
613pub enum StartResult {
614    /// One writer, registered immediately (TcpClient, Udp, Serial, etc.)
615    Simple {
616        id: InterfaceId,
617        info: InterfaceInfo,
618        writer: Box<dyn Writer>,
619        interface_type_name: String,
620    },
621    /// Spawns a listener; dynamic interfaces arrive via Event::InterfaceUp (TcpServer, Auto, I2P, etc.)
622    Listener { control: Option<ListenerControl> },
623    /// Multiple subinterfaces from one config (RNode).
624    Multi(Vec<SubInterface>),
625}
626
627/// A single subinterface returned from a multi-interface factory.
628pub struct SubInterface {
629    pub id: InterfaceId,
630    pub info: InterfaceInfo,
631    pub writer: Box<dyn Writer>,
632    pub interface_type_name: String,
633}
634
635/// Context passed to [`InterfaceFactory::start()`].
636pub struct StartContext {
637    pub tx: EventSender,
638    pub next_dynamic_id: Arc<AtomicU64>,
639    pub mode: u8,
640    pub gravity: i64,
641    pub recursive_prs: bool,
642    pub announces_from_internal: bool,
643    pub announces_to_internal: Option<bool>,
644    pub ingress_control: rns_core::transport::types::IngressControlConfig,
645    pub ifac: Option<IfacState>,
646    /// Linux `SO_MARK` value applied to IP underlay sockets before connect.
647    pub underlay_mark: Option<u32>,
648}
649
650/// Opaque interface config data. Each factory downcasts to its concrete type.
651pub trait InterfaceConfigData: Send + Any {
652    fn as_any(&self) -> &dyn Any;
653    fn into_any(self: Box<Self>) -> Box<dyn Any>;
654}
655
656/// ConfigObj section passed to factories that need nested `[[[sections]]]`.
657pub struct ConfigSection<'a> {
658    pub params: &'a HashMap<String, String>,
659    pub children: &'a [crate::config::ParsedSubinterface],
660}
661
662#[derive(Debug, Clone)]
663pub struct DynamicInterfaceTemplate {
664    pub parent_id: InterfaceId,
665    pub interface_type: String,
666    pub ifac: Option<IfacState>,
667    pub mode: u8,
668    pub gravity: i64,
669    pub recursive_prs: bool,
670    pub announces_from_internal: bool,
671    pub announces_to_internal: Option<bool>,
672}
673
674impl DynamicInterfaceTemplate {
675    pub fn registration(
676        &self,
677        mut info: InterfaceInfo,
678    ) -> crate::event::DynamicInterfaceRegistration {
679        info.mode = self.mode;
680        info.gravity = self.gravity;
681        info.recursive_prs = self.recursive_prs;
682        info.announces_from_internal = self.announces_from_internal;
683        info.announces_to_internal = self.announces_to_internal;
684        crate::event::DynamicInterfaceRegistration {
685            info,
686            interface_type: self.interface_type.clone(),
687            parent_id: self.parent_id,
688            telemetry: crate::event::InterfaceTelemetry::default(),
689            ifac: self.ifac.clone(),
690        }
691    }
692}
693
694impl<T: Send + 'static> InterfaceConfigData for T {
695    fn as_any(&self) -> &dyn Any {
696        self
697    }
698
699    fn into_any(self: Box<Self>) -> Box<dyn Any> {
700        self
701    }
702}
703
704/// Factory that can parse config and start an interface type.
705pub trait InterfaceFactory: Send + Sync {
706    /// Config-file type name, e.g. "TCPClientInterface".
707    fn type_name(&self) -> &str;
708
709    /// Default IFAC size (bytes). 8 for serial/kiss/rnode, 16 for others.
710    fn default_ifac_size(&self) -> usize {
711        16
712    }
713
714    /// Parse from key-value params (config file or external).
715    fn parse_config(
716        &self,
717        name: &str,
718        id: InterfaceId,
719        params: &HashMap<String, String>,
720    ) -> Result<Box<dyn InterfaceConfigData>, String>;
721
722    /// Parse a complete ConfigObj interface section. Flat factories inherit
723    /// the compatibility default; multi-interface factories can inspect children.
724    fn parse_config_section(
725        &self,
726        name: &str,
727        id: InterfaceId,
728        section: ConfigSection<'_>,
729    ) -> Result<Box<dyn InterfaceConfigData>, String> {
730        self.parse_config(name, id, section.params)
731    }
732
733    /// Start the interface from parsed config.
734    fn start(
735        &self,
736        config: Box<dyn InterfaceConfigData>,
737        ctx: StartContext,
738    ) -> io::Result<StartResult>;
739}
740
741impl InterfaceStatusView for InterfaceEntry {
742    fn id(&self) -> InterfaceId {
743        self.id
744    }
745    fn info(&self) -> &InterfaceInfo {
746        &self.info
747    }
748    fn online(&self) -> bool {
749        self.online
750    }
751    fn stats(&self) -> &InterfaceStats {
752        &self.stats
753    }
754    fn tx_drops(&self) -> u64 {
755        self.async_writer_metrics
756            .as_ref()
757            .map(AsyncWriterMetrics::tx_drops)
758            .unwrap_or(0)
759    }
760    fn tx_dropped_bytes(&self) -> u64 {
761        self.async_writer_metrics
762            .as_ref()
763            .map(AsyncWriterMetrics::tx_dropped_bytes)
764            .unwrap_or(0)
765    }
766    fn tx_stalled(&self) -> bool {
767        self.async_writer_metrics
768            .as_ref()
769            .is_some_and(AsyncWriterMetrics::tx_stalled)
770    }
771    fn tx_buffered(&self) -> usize {
772        self.async_writer_metrics
773            .as_ref()
774            .map(AsyncWriterMetrics::tx_buffered)
775            .unwrap_or(0)
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782
783    #[test]
784    fn shared_medium_hint_matches_interface_class_defaults() {
785        for interface_type in [
786            "AX25KISSInterface",
787            "KISSInterface",
788            "PipeInterface",
789            "RNodeInterface",
790            "RNodeMultiInterface",
791            "SerialInterface",
792            "UDPInterface",
793        ] {
794            assert!(shared_medium_hint(interface_type), "{interface_type}");
795        }
796        for interface_type in [
797            "AutoInterface",
798            "BackboneInterface",
799            "I2PInterface",
800            "LocalInterface",
801            "TCPClientInterface",
802            "TCPServerInterface",
803        ] {
804            assert!(!shared_medium_hint(interface_type), "{interface_type}");
805        }
806    }
807    use crate::event::Event;
808    use rns_core::constants;
809    use std::sync::mpsc;
810
811    struct MockWriter {
812        sent: Vec<Vec<u8>>,
813    }
814
815    impl MockWriter {
816        fn new() -> Self {
817            MockWriter { sent: Vec::new() }
818        }
819    }
820
821    impl Writer for MockWriter {
822        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
823            self.sent.push(data.to_vec());
824            Ok(())
825        }
826    }
827
828    #[test]
829    fn interface_entry_construction() {
830        let entry = InterfaceEntry {
831            id: InterfaceId(1),
832            info: InterfaceInfo {
833                id: InterfaceId(1),
834                name: String::new(),
835                mode: constants::MODE_FULL,
836                gravity: 0,
837                recursive_prs: false,
838                announces_from_internal: true,
839                announces_to_internal: None,
840                out_capable: true,
841                in_capable: true,
842                bitrate: None,
843                airtime_profile: None,
844                announce_rate_target: None,
845                announce_rate_grace: 0,
846                announce_rate_penalty: 0.0,
847                announce_cap: constants::ANNOUNCE_CAP,
848                is_local_client: false,
849                wants_tunnel: false,
850                tunnel_id: None,
851                mtu: constants::MTU as u32,
852                ia_freq: 0.0,
853                ip_freq: 0.0,
854                op_freq: 0.0,
855                op_samples: 0,
856                started: 0.0,
857                ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
858            },
859            writer: Box::new(MockWriter::new()),
860            async_writer_metrics: None,
861            enabled: true,
862            online: false,
863            dynamic: false,
864            ifac: None,
865            stats: InterfaceStats::default(),
866            interface_type: String::new(),
867            send_retry_at: None,
868            send_retry_backoff: Duration::ZERO,
869        };
870        assert_eq!(entry.id, InterfaceId(1));
871        assert!(!entry.online);
872        assert!(!entry.dynamic);
873    }
874
875    #[test]
876    fn dynamic_registration_inherits_announces_to_internal() {
877        let template = DynamicInterfaceTemplate {
878            parent_id: InterfaceId(7),
879            interface_type: "dynamic-test".into(),
880            ifac: None,
881            mode: constants::MODE_GATEWAY,
882            gravity: -3,
883            recursive_prs: true,
884            announces_from_internal: false,
885            announces_to_internal: Some(true),
886        };
887        let info = InterfaceInfo {
888            id: InterfaceId(8),
889            name: "child".into(),
890            mode: constants::MODE_FULL,
891            gravity: 0,
892            recursive_prs: false,
893            announces_from_internal: true,
894            announces_to_internal: None,
895            out_capable: true,
896            in_capable: true,
897            bitrate: None,
898            airtime_profile: None,
899            announce_rate_target: None,
900            announce_rate_grace: 0,
901            announce_rate_penalty: 0.0,
902            announce_cap: constants::ANNOUNCE_CAP,
903            is_local_client: false,
904            wants_tunnel: false,
905            tunnel_id: None,
906            mtu: constants::MTU as u32,
907            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
908            ia_freq: 0.0,
909            ip_freq: 0.0,
910            op_freq: 0.0,
911            op_samples: 0,
912            started: 0.0,
913        };
914
915        let registration = template.registration(info);
916
917        assert_eq!(registration.info.mode, constants::MODE_GATEWAY);
918        assert_eq!(registration.info.gravity, -3);
919        assert!(registration.info.recursive_prs);
920        assert!(!registration.info.announces_from_internal);
921        assert_eq!(registration.info.announces_to_internal, Some(true));
922    }
923
924    #[test]
925    fn mock_writer_captures_bytes() {
926        let mut writer = MockWriter::new();
927        writer.send_frame(b"hello").unwrap();
928        writer.send_frame(b"world").unwrap();
929        assert_eq!(writer.sent.len(), 2);
930        assert_eq!(writer.sent[0], b"hello");
931        assert_eq!(writer.sent[1], b"world");
932    }
933
934    #[test]
935    fn writer_send_frame_produces_output() {
936        let mut writer = MockWriter::new();
937        let data = vec![0x01, 0x02, 0x03];
938        writer.send_frame(&data).unwrap();
939        assert_eq!(writer.sent[0], data);
940    }
941
942    struct BlockingWriter {
943        entered_tx: mpsc::Sender<()>,
944        release_rx: mpsc::Receiver<()>,
945    }
946
947    impl Writer for BlockingWriter {
948        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
949            let _ = self.entered_tx.send(());
950            let _ = self.release_rx.recv();
951            Ok(())
952        }
953    }
954
955    struct EgressControlledBlockingWriter {
956        inner: BlockingWriter,
957        control: EgressControl,
958    }
959
960    impl Writer for EgressControlledBlockingWriter {
961        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
962            self.inner.send_frame(data)
963        }
964
965        fn egress_control(&self) -> Option<EgressControl> {
966            Some(self.control.clone())
967        }
968    }
969
970    struct FailingWriter {
971        shutdown_called: Arc<AtomicBool>,
972    }
973
974    struct RecordingWriter {
975        sent_tx: mpsc::Sender<Vec<u8>>,
976    }
977
978    impl Writer for RecordingWriter {
979        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
980            self.sent_tx
981                .send(data.to_vec())
982                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "test receiver closed"))
983        }
984    }
985
986    struct BatchRecordingWriter {
987        batch_tx: mpsc::Sender<usize>,
988        first_release: Option<mpsc::Receiver<()>>,
989    }
990
991    impl Writer for BatchRecordingWriter {
992        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
993            unreachable!("the async worker must use batch delivery")
994        }
995
996        fn send_frames(&mut self, frames: &[Vec<u8>]) -> io::Result<()> {
997            self.batch_tx.send(frames.len()).unwrap();
998            if let Some(release) = self.first_release.take() {
999                release.recv().unwrap();
1000            }
1001            Ok(())
1002        }
1003    }
1004
1005    impl Writer for FailingWriter {
1006        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
1007            Err(io::Error::new(io::ErrorKind::BrokenPipe, "boom"))
1008        }
1009
1010        fn shutdown(&mut self) {
1011            self.shutdown_called.store(true, Ordering::Relaxed);
1012        }
1013    }
1014
1015    #[test]
1016    fn async_writer_returns_wouldblock_when_queue_is_full() {
1017        let (event_tx, _event_rx) = crate::event::channel();
1018        let (entered_tx, entered_rx) = mpsc::channel();
1019        let (release_tx, release_rx) = mpsc::channel();
1020        let (mut writer, metrics) = wrap_async_writer(
1021            Box::new(BlockingWriter {
1022                entered_tx,
1023                release_rx,
1024            }),
1025            InterfaceId(7),
1026            "test",
1027            event_tx,
1028            1,
1029        );
1030
1031        writer.send_frame(&[1]).unwrap();
1032        entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1033        writer.send_frame(&[2]).unwrap();
1034        assert_eq!(metrics.queued_frames(), 1);
1035        let err = writer.send_frame(&[3]).unwrap_err();
1036        assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
1037
1038        release_tx.send(()).unwrap();
1039        entered_rx
1040            .recv_timeout(Duration::from_secs(1))
1041            .expect("the queued frame must wake the writer without another send trigger");
1042        assert_eq!(metrics.queued_frames(), 0);
1043        release_tx.send(()).unwrap();
1044    }
1045
1046    #[test]
1047    fn async_writer_shares_exact_egress_byte_valve_and_stall_gate() {
1048        let (event_tx, _event_rx) = crate::event::channel();
1049        let (entered_tx, entered_rx) = mpsc::channel();
1050        let (release_tx, release_rx) = mpsc::channel();
1051        let control = EgressControl::new(4);
1052        let (mut writer, metrics) = wrap_async_writer(
1053            Box::new(EgressControlledBlockingWriter {
1054                inner: BlockingWriter {
1055                    entered_tx,
1056                    release_rx,
1057                },
1058                control: control.clone(),
1059            }),
1060            InterfaceId(8),
1061            "egress-test",
1062            event_tx,
1063            8,
1064        );
1065
1066        // Two ordinary bytes occupy exactly four HDLC bytes on wire.
1067        writer.send_frame(&[1, 2]).unwrap();
1068        entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1069        assert_eq!(control.reserved_bytes.load(Ordering::Relaxed), 4);
1070        assert_eq!(metrics.tx_buffered(), 4);
1071
1072        // The hard valve drops instead of retaining another four-byte frame.
1073        writer.send_frame(&[3, 4]).unwrap();
1074        assert_eq!(control.reserved_bytes.load(Ordering::Relaxed), 4);
1075        assert_eq!(control.dropped_frames.load(Ordering::Relaxed), 1);
1076        assert_eq!(control.dropped_bytes.load(Ordering::Relaxed), 4);
1077        assert_eq!(metrics.tx_drops(), 1);
1078        assert_eq!(metrics.tx_dropped_bytes(), 4);
1079
1080        release_tx.send(()).unwrap();
1081        let deadline = Instant::now() + Duration::from_secs(1);
1082        while control.reserved_bytes.load(Ordering::Relaxed) != 0 {
1083            assert!(
1084                Instant::now() < deadline,
1085                "egress reservation did not drain"
1086            );
1087            thread::yield_now();
1088        }
1089
1090        control.set_stalled(true);
1091        assert!(metrics.tx_stalled());
1092        writer.send_frame(&[5]).unwrap();
1093        assert_eq!(control.dropped_frames.load(Ordering::Relaxed), 2);
1094        assert_eq!(control.dropped_bytes.load(Ordering::Relaxed), 7);
1095        assert_eq!(metrics.queued_frames(), 0);
1096    }
1097
1098    #[test]
1099    fn async_writer_reports_interface_down_after_worker_failure() {
1100        let (event_tx, event_rx) = crate::event::channel();
1101        let shutdown_called = Arc::new(AtomicBool::new(false));
1102        let (mut writer, metrics) = wrap_async_writer(
1103            Box::new(FailingWriter {
1104                shutdown_called: Arc::clone(&shutdown_called),
1105            }),
1106            InterfaceId(9),
1107            "fail",
1108            event_tx,
1109            2,
1110        );
1111
1112        writer.send_frame(&[1]).unwrap();
1113        let event = event_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1114        assert!(matches!(event, Event::InterfaceDown(InterfaceId(9))));
1115        assert!(!metrics.worker_alive());
1116        assert!(shutdown_called.load(Ordering::Relaxed));
1117    }
1118
1119    #[test]
1120    fn async_writer_preserves_order_through_fifty_thousand_frame_burst() {
1121        const FRAME_COUNT: usize = 50_000;
1122        let (event_tx, _event_rx) = crate::event::channel();
1123        let (sent_tx, sent_rx) = mpsc::channel();
1124        let (mut writer, metrics) = wrap_async_writer(
1125            Box::new(RecordingWriter { sent_tx }),
1126            InterfaceId(11),
1127            "burst",
1128            event_tx,
1129            FRAME_COUNT,
1130        );
1131
1132        for index in 0..FRAME_COUNT {
1133            let payload = (index as u64).to_be_bytes();
1134            writer.send_frame(&payload).unwrap();
1135        }
1136
1137        for index in 0..FRAME_COUNT {
1138            let received = sent_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1139            assert_eq!(received, (index as u64).to_be_bytes());
1140        }
1141        assert!(metrics.worker_alive());
1142    }
1143
1144    #[test]
1145    fn async_writer_coalesces_frames_waiting_behind_an_active_batch() {
1146        let (event_tx, _event_rx) = crate::event::channel();
1147        let (batch_tx, batch_rx) = mpsc::channel();
1148        let (release_tx, release_rx) = mpsc::channel();
1149        let (mut writer, _) = wrap_async_writer(
1150            Box::new(BatchRecordingWriter {
1151                batch_tx,
1152                first_release: Some(release_rx),
1153            }),
1154            InterfaceId(12),
1155            "batch",
1156            event_tx,
1157            16,
1158        );
1159
1160        writer.send_frame(&[0]).unwrap();
1161        assert_eq!(batch_rx.recv_timeout(Duration::from_secs(1)).unwrap(), 1);
1162        for index in 1..=10 {
1163            writer.send_frame(&[index]).unwrap();
1164        }
1165        release_tx.send(()).unwrap();
1166
1167        assert_eq!(batch_rx.recv_timeout(Duration::from_secs(1)).unwrap(), 10);
1168    }
1169}