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    /// Write one frame without silently dropping it. WouldBlock means the
244    /// frame was accepted and `flush` must finish it before success is reported.
245    /// Writers with intentional drop policies must override this method.
246    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
247        self.send_frame(data)
248    }
249
250    /// Internal async-writer admission. Leaves completion untouched on failure.
251    #[doc(hidden)]
252    fn enqueue_confirmed(
253        &mut self,
254        _data: &[u8],
255        _completion: &mut Option<crate::link_send::Completion>,
256    ) -> io::Result<()> {
257        Err(io::Error::new(
258            io::ErrorKind::Unsupported,
259            "confirmed sends require an async interface writer",
260        ))
261    }
262
263    fn send_frames(&mut self, frames: &[Vec<u8>]) -> io::Result<()> {
264        for frame in frames {
265            self.send_frame(frame)?;
266        }
267        Ok(())
268    }
269
270    /// Resume bytes accepted by a previous call that returned `WouldBlock`.
271    fn flush(&mut self) -> io::Result<()> {
272        Ok(())
273    }
274
275    /// Share an exact byte admission budget with the async producer when this
276    /// writer uses dataplane egress control.
277    fn egress_control(&self) -> Option<EgressControl> {
278        None
279    }
280
281    fn shutdown(&mut self) {}
282}
283
284pub const DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY: usize = 256;
285
286#[derive(Clone)]
287#[doc(hidden)]
288pub struct EgressControl {
289    byte_limit: usize,
290    reserved_bytes: Arc<AtomicUsize>,
291    stalled: Arc<AtomicBool>,
292    dropped_frames: Arc<AtomicU64>,
293    dropped_bytes: Arc<AtomicU64>,
294}
295
296impl EgressControl {
297    pub(crate) fn new(byte_limit: usize) -> Self {
298        Self {
299            byte_limit,
300            reserved_bytes: Arc::new(AtomicUsize::new(0)),
301            stalled: Arc::new(AtomicBool::new(false)),
302            dropped_frames: Arc::new(AtomicU64::new(0)),
303            dropped_bytes: Arc::new(AtomicU64::new(0)),
304        }
305    }
306
307    fn try_reserve(&self, bytes: usize) -> bool {
308        if self.stalled.load(Ordering::Relaxed) {
309            return false;
310        }
311        let mut current = self.reserved_bytes.load(Ordering::Relaxed);
312        loop {
313            let Some(next) = current.checked_add(bytes) else {
314                return false;
315            };
316            if next > self.byte_limit {
317                return false;
318            }
319            match self.reserved_bytes.compare_exchange_weak(
320                current,
321                next,
322                Ordering::Relaxed,
323                Ordering::Relaxed,
324            ) {
325                Ok(_) => return true,
326                Err(observed) => current = observed,
327            }
328        }
329    }
330
331    fn release(&self, bytes: usize) {
332        self.reserved_bytes.fetch_sub(bytes, Ordering::Relaxed);
333    }
334
335    pub(crate) fn stalled(&self) -> bool {
336        self.stalled.load(Ordering::Relaxed)
337    }
338
339    pub(crate) fn set_stalled(&self, stalled: bool) {
340        self.stalled.store(stalled, Ordering::Relaxed);
341    }
342
343    pub(crate) fn record_drop(&self, bytes: usize) {
344        self.dropped_frames.fetch_add(1, Ordering::Relaxed);
345        self.dropped_bytes
346            .fetch_add(bytes as u64, Ordering::Relaxed);
347    }
348}
349
350/// Return whether an interface type normally operates on a shared medium.
351///
352/// This is a type-level hint: runtime configuration can still describe links
353/// with different physical properties. It mirrors Reticulum's interface-class
354/// defaults and is suitable for discovery/capability metadata.
355pub fn shared_medium_hint(interface_type: &str) -> bool {
356    matches!(
357        interface_type,
358        "AX25KISSInterface"
359            | "KISSInterface"
360            | "PipeInterface"
361            | "RNodeInterface"
362            | "RNodeMultiInterface"
363            | "SerialInterface"
364            | "UDPInterface"
365    )
366}
367
368pub(crate) fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, label: &str) -> MutexGuard<'a, T> {
369    match mutex.lock() {
370        Ok(guard) => guard,
371        Err(poisoned) => {
372            log::warn!("recovering poisoned mutex: {}", label);
373            poisoned.into_inner()
374        }
375    }
376}
377
378#[derive(Clone, Default)]
379pub struct ListenerControl {
380    stop: Arc<AtomicBool>,
381    running: Option<Arc<AtomicBool>>,
382}
383
384impl ListenerControl {
385    pub fn new() -> Self {
386        Self {
387            stop: Arc::new(AtomicBool::new(false)),
388            running: None,
389        }
390    }
391
392    pub(crate) fn with_running_flag(running: Arc<AtomicBool>) -> Self {
393        Self {
394            stop: Arc::new(AtomicBool::new(false)),
395            running: Some(running),
396        }
397    }
398
399    pub fn request_stop(&self) {
400        self.stop.store(true, Ordering::Relaxed);
401        if let Some(running) = &self.running {
402            running.store(false, Ordering::Relaxed);
403        }
404    }
405
406    pub fn should_stop(&self) -> bool {
407        self.stop.load(Ordering::Relaxed)
408    }
409
410    pub(crate) fn same_instance(&self, other: &Self) -> bool {
411        Arc::ptr_eq(&self.stop, &other.stop)
412    }
413}
414
415/// Wait for serial input without keeping a detached reader blocked forever.
416pub(crate) fn poll_readable(fd: std::os::fd::RawFd, timeout: Duration) -> io::Result<bool> {
417    let mut pfd = libc::pollfd {
418        fd,
419        events: libc::POLLIN,
420        revents: 0,
421    };
422    let timeout_ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
423    let result = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
424    if result < 0 {
425        let error = io::Error::last_os_error();
426        return if error.kind() == io::ErrorKind::Interrupted {
427            Ok(false)
428        } else {
429            Err(error)
430        };
431    }
432    if pfd.revents & libc::POLLNVAL != 0 {
433        return Err(io::Error::new(
434            io::ErrorKind::BrokenPipe,
435            "serial port closed",
436        ));
437    }
438    Ok(result > 0)
439}
440
441pub(crate) fn wait_while_active(control: &ListenerControl, duration: Duration) -> bool {
442    let deadline = Instant::now() + duration;
443    while !control.should_stop() {
444        let remaining = deadline.saturating_duration_since(Instant::now());
445        if remaining.is_zero() {
446            return true;
447        }
448        thread::sleep(remaining.min(Duration::from_millis(100)));
449    }
450    false
451}
452
453pub(crate) struct UnavailableWriter;
454
455impl Writer for UnavailableWriter {
456    fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
457        Err(io::Error::new(
458            io::ErrorKind::NotConnected,
459            "interface is waiting for its port",
460        ))
461    }
462}
463
464#[derive(Clone, Default)]
465pub struct AsyncWriterMetrics {
466    queued_frames: Arc<AtomicUsize>,
467    worker_alive: Arc<AtomicBool>,
468    egress_control: Option<EgressControl>,
469}
470
471impl AsyncWriterMetrics {
472    pub fn queued_frames(&self) -> usize {
473        self.queued_frames.load(Ordering::Relaxed)
474    }
475
476    pub fn worker_alive(&self) -> bool {
477        self.worker_alive.load(Ordering::Relaxed)
478    }
479
480    /// Exact encoded bytes waiting in the async queue or concrete writer.
481    pub fn tx_buffered(&self) -> usize {
482        self.egress_control
483            .as_ref()
484            .map(|control| control.reserved_bytes.load(Ordering::Relaxed))
485            .unwrap_or(0)
486    }
487
488    pub fn tx_drops(&self) -> u64 {
489        self.egress_control
490            .as_ref()
491            .map(|control| control.dropped_frames.load(Ordering::Relaxed))
492            .unwrap_or(0)
493    }
494
495    pub fn tx_dropped_bytes(&self) -> u64 {
496        self.egress_control
497            .as_ref()
498            .map(|control| control.dropped_bytes.load(Ordering::Relaxed))
499            .unwrap_or(0)
500    }
501
502    pub fn tx_stalled(&self) -> bool {
503        self.egress_control
504            .as_ref()
505            .is_some_and(EgressControl::stalled)
506    }
507}
508
509struct AsyncWriter {
510    tx: SyncSender<QueuedFrame>,
511    metrics: AsyncWriterMetrics,
512}
513
514struct QueuedFrame {
515    data: Vec<u8>,
516    reserved_bytes: usize,
517    completion: Option<crate::link_send::Completion>,
518}
519
520impl Writer for AsyncWriter {
521    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
522        self.enqueue(data, &mut None)
523    }
524
525    fn enqueue_confirmed(
526        &mut self,
527        data: &[u8],
528        completion: &mut Option<crate::link_send::Completion>,
529    ) -> io::Result<()> {
530        self.enqueue(data, completion)
531    }
532}
533
534impl AsyncWriter {
535    fn enqueue(
536        &mut self,
537        data: &[u8],
538        completion: &mut Option<crate::link_send::Completion>,
539    ) -> io::Result<()> {
540        if !self.metrics.worker_alive() {
541            return Err(io::Error::new(
542                io::ErrorKind::BrokenPipe,
543                "interface writer worker is offline",
544            ));
545        }
546
547        let reserved_bytes = if let Some(control) = &self.metrics.egress_control {
548            let framed_bytes = crate::hdlc::framed_len(data);
549            if !control.try_reserve(framed_bytes) {
550                if completion.is_some() {
551                    if framed_bytes > control.byte_limit {
552                        return Err(io::Error::new(
553                            io::ErrorKind::InvalidInput,
554                            "frame exceeds interface byte budget",
555                        ));
556                    }
557                    return Err(io::Error::new(
558                        io::ErrorKind::WouldBlock,
559                        "interface egress budget is full",
560                    ));
561                }
562                control.record_drop(framed_bytes);
563                return Ok(());
564            }
565            framed_bytes
566        } else {
567            0
568        };
569
570        // Publish accounting before the frame becomes visible to the worker.
571        self.metrics.queued_frames.fetch_add(1, Ordering::Relaxed);
572        match self.tx.try_send(QueuedFrame {
573            data: data.to_vec(),
574            reserved_bytes,
575            completion: completion.take(),
576        }) {
577            Ok(()) => Ok(()),
578            Err(TrySendError::Full(frame)) => {
579                *completion = frame.completion;
580                self.metrics.queued_frames.fetch_sub(1, Ordering::Relaxed);
581                if let Some(control) = &self.metrics.egress_control {
582                    control.release(frame.reserved_bytes);
583                }
584                Err(io::Error::new(
585                    io::ErrorKind::WouldBlock,
586                    "interface writer queue is full",
587                ))
588            }
589            Err(TrySendError::Disconnected(frame)) => {
590                *completion = frame.completion;
591                self.metrics.queued_frames.fetch_sub(1, Ordering::Relaxed);
592                if let Some(control) = &self.metrics.egress_control {
593                    control.release(frame.reserved_bytes);
594                }
595                self.metrics.worker_alive.store(false, Ordering::Relaxed);
596                Err(io::Error::new(
597                    io::ErrorKind::BrokenPipe,
598                    "interface writer worker disconnected",
599                ))
600            }
601        }
602    }
603}
604
605pub fn wrap_async_writer(
606    writer: Box<dyn Writer>,
607    interface_id: InterfaceId,
608    interface_name: &str,
609    event_tx: EventSender,
610    queue_capacity: usize,
611) -> (Box<dyn Writer>, AsyncWriterMetrics) {
612    let egress_control = writer.egress_control();
613    let (tx, rx) = sync_channel::<QueuedFrame>(queue_capacity.max(1));
614    let metrics = AsyncWriterMetrics {
615        queued_frames: Arc::new(AtomicUsize::new(0)),
616        worker_alive: Arc::new(AtomicBool::new(true)),
617        egress_control,
618    };
619    let metrics_thread = metrics.clone();
620    let name = interface_name.to_string();
621
622    let spawn_result = thread::Builder::new()
623        .name(format!("iface-writer-{}", interface_id.0))
624        .spawn(move || async_writer_loop(writer, rx, interface_id, name, event_tx, metrics_thread));
625
626    if let Err(err) = spawn_result {
627        metrics.worker_alive.store(false, Ordering::Relaxed);
628        log::error!(
629            "[{}:{}] failed to spawn async writer thread: {}",
630            interface_name,
631            interface_id.0,
632            err
633        );
634        return (Box::new(DirectWriterFallback), metrics);
635    }
636
637    (
638        Box::new(AsyncWriter {
639            tx,
640            metrics: metrics.clone(),
641        }),
642        metrics,
643    )
644}
645
646struct DirectWriterFallback;
647
648impl Writer for DirectWriterFallback {
649    fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
650        Err(io::Error::other("interface writer worker unavailable"))
651    }
652}
653
654fn async_writer_loop(
655    mut writer: Box<dyn Writer>,
656    rx: std::sync::mpsc::Receiver<QueuedFrame>,
657    interface_id: InterfaceId,
658    interface_name: String,
659    event_tx: EventSender,
660    metrics: AsyncWriterMetrics,
661) {
662    while let Ok(first) = rx.recv() {
663        let mut queued = vec![first];
664        queued.extend(rx.try_iter());
665        let reserved_bytes = queued.iter().map(|frame| frame.reserved_bytes).sum();
666        metrics
667            .queued_frames
668            .fetch_sub(queued.len(), Ordering::Relaxed);
669        if event_tx.link_send_pool().in_flight() > 0 {
670            let _ = event_tx.try_send(crate::event::Event::LinkWriterReady);
671        }
672
673        let mut result = Ok(());
674        if queued.iter().any(|frame| frame.completion.is_some()) {
675            for mut frame in queued {
676                let confirmed = frame.completion.is_some();
677                if frame.completion.as_ref().is_some_and(|c| c.is_finished()) {
678                    result = Err(io::Error::new(
679                        io::ErrorKind::Interrupted,
680                        "transmission cancelled by shutdown",
681                    ));
682                }
683                if result.is_ok() {
684                    result = if frame.completion.is_some() {
685                        writer.send_frame_confirmed(&frame.data)
686                    } else {
687                        writer.send_frame(&frame.data)
688                    };
689                    while result
690                        .as_ref()
691                        .is_err_and(|e| e.kind() == io::ErrorKind::WouldBlock)
692                    {
693                        if frame.completion.as_ref().is_some_and(|c| c.is_finished()) {
694                            result = Err(io::Error::new(
695                                io::ErrorKind::Interrupted,
696                                "transmission cancelled by shutdown",
697                            ));
698                            break;
699                        }
700                        thread::sleep(Duration::from_millis(1));
701                        result = writer.flush();
702                    }
703                }
704                if let Some(completion) = frame.completion.take() {
705                    completion.finish(
706                        result.as_ref().map(|_| ()).map_err(|e| {
707                            crate::link_send::LinkSendError::WriteFailed(e.to_string())
708                        }),
709                    );
710                }
711                // Packet-level policy/validation errors do not imply a broken
712                // connection (e.g. a Local client that temporarily holds TX).
713                if confirmed
714                    && result.as_ref().is_err_and(|error| {
715                        matches!(
716                            error.kind(),
717                            io::ErrorKind::PermissionDenied
718                                | io::ErrorKind::InvalidInput
719                                | io::ErrorKind::Unsupported
720                        )
721                    })
722                {
723                    result = Ok(());
724                }
725            }
726        } else {
727            let frames: Vec<_> = queued.into_iter().map(|frame| frame.data).collect();
728            result = writer.send_frames(&frames);
729        }
730        while result
731            .as_ref()
732            .is_err_and(|error| error.kind() == io::ErrorKind::WouldBlock)
733        {
734            thread::sleep(Duration::from_millis(1));
735            result = writer.flush();
736        }
737
738        if let Some(control) = &metrics.egress_control {
739            control.release(reserved_bytes);
740        }
741        if event_tx.link_send_pool().in_flight() > 0 {
742            let _ = event_tx.try_send(crate::event::Event::LinkWriterReady);
743        }
744
745        if let Err(err) = result {
746            metrics.worker_alive.store(false, Ordering::Relaxed);
747            log::warn!(
748                "[{}:{}] async writer exiting after send failure: {}",
749                interface_name,
750                interface_id.0,
751                err
752            );
753            writer.shutdown();
754            let _ = event_tx.send(crate::event::Event::InterfaceDown(interface_id));
755            return;
756        }
757    }
758
759    metrics.worker_alive.store(false, Ordering::Relaxed);
760}
761
762pub use crate::common::interface_stats::{InterfaceStats, TrafficRates, ANNOUNCE_SAMPLE_MAX};
763
764use crate::common::management::InterfaceStatusView;
765
766/// Everything the driver tracks per interface.
767pub struct InterfaceEntry {
768    pub id: InterfaceId,
769    pub info: InterfaceInfo,
770    pub writer: Box<dyn Writer>,
771    pub async_writer_metrics: Option<AsyncWriterMetrics>,
772    /// Administrative enable/disable state.
773    pub enabled: bool,
774    pub online: bool,
775    /// True for dynamically spawned interfaces (e.g. TCP server clients).
776    /// These are fully removed on InterfaceDown rather than just marked offline.
777    pub dynamic: bool,
778    /// IFAC state for this interface, if access codes are enabled.
779    pub ifac: Option<IfacState>,
780    /// Traffic statistics.
781    pub stats: InterfaceStats,
782    /// Human-readable interface type string (e.g. "TCPClientInterface").
783    pub interface_type: String,
784    /// Next time a send should be retried after a transient WouldBlock.
785    pub send_retry_at: Option<Instant>,
786    /// Current retry backoff for transient send failures.
787    pub send_retry_backoff: Duration,
788}
789
790/// Result of starting an interface via a factory.
791// Boxing `Simple` would change the published factory result representation.
792#[allow(clippy::large_enum_variant)]
793pub enum StartResult {
794    /// One writer, registered immediately (TcpClient, Udp, Serial, etc.)
795    Simple {
796        id: InterfaceId,
797        info: InterfaceInfo,
798        writer: Box<dyn Writer>,
799        interface_type_name: String,
800        control: Option<ListenerControl>,
801    },
802    /// Spawns a listener; dynamic interfaces arrive via Event::InterfaceUp (TcpServer, Auto, I2P, etc.)
803    Listener { control: Option<ListenerControl> },
804    /// Multiple subinterfaces from one config (RNode).
805    Multi {
806        subinterfaces: Vec<SubInterface>,
807        control: Option<ListenerControl>,
808    },
809}
810
811/// A single subinterface returned from a multi-interface factory.
812pub struct SubInterface {
813    pub id: InterfaceId,
814    pub info: InterfaceInfo,
815    pub writer: Box<dyn Writer>,
816    pub interface_type_name: String,
817}
818
819/// Context passed to [`InterfaceFactory::start()`].
820pub struct StartContext {
821    pub tx: EventSender,
822    pub next_dynamic_id: Arc<AtomicU64>,
823    pub mode: u8,
824    pub gravity: i64,
825    pub recursive_prs: bool,
826    pub announces_from_internal: bool,
827    pub announces_to_internal: Option<bool>,
828    pub ingress_control: rns_core::transport::types::IngressControlConfig,
829    pub ifac: Option<IfacState>,
830    /// Linux `SO_MARK` value applied to IP underlay sockets before connect.
831    pub underlay_mark: Option<u32>,
832}
833
834/// Opaque interface config data. Each factory downcasts to its concrete type.
835pub trait InterfaceConfigData: Send + Any {
836    fn as_any(&self) -> &dyn Any;
837    fn into_any(self: Box<Self>) -> Box<dyn Any>;
838}
839
840/// ConfigObj section passed to factories that need nested `[[[sections]]]`.
841pub struct ConfigSection<'a> {
842    pub params: &'a HashMap<String, String>,
843    pub children: &'a [crate::config::ParsedSubinterface],
844}
845
846#[derive(Debug, Clone)]
847pub struct DynamicInterfaceTemplate {
848    pub parent_id: InterfaceId,
849    pub interface_type: String,
850    pub ifac: Option<IfacState>,
851    pub mode: u8,
852    pub gravity: i64,
853    pub recursive_prs: bool,
854    pub announces_from_internal: bool,
855    pub announces_to_internal: Option<bool>,
856}
857
858impl DynamicInterfaceTemplate {
859    pub fn registration(
860        &self,
861        mut info: InterfaceInfo,
862    ) -> crate::event::DynamicInterfaceRegistration {
863        info.mode = self.mode;
864        info.gravity = self.gravity;
865        info.recursive_prs = self.recursive_prs;
866        info.announces_from_internal = self.announces_from_internal;
867        info.announces_to_internal = self.announces_to_internal;
868        crate::event::DynamicInterfaceRegistration {
869            info,
870            interface_type: self.interface_type.clone(),
871            parent_id: self.parent_id,
872            telemetry: crate::event::InterfaceTelemetry::default(),
873            ifac: self.ifac.clone(),
874        }
875    }
876}
877
878impl<T: Send + 'static> InterfaceConfigData for T {
879    fn as_any(&self) -> &dyn Any {
880        self
881    }
882
883    fn into_any(self: Box<Self>) -> Box<dyn Any> {
884        self
885    }
886}
887
888/// Factory that can parse config and start an interface type.
889pub trait InterfaceFactory: Send + Sync {
890    /// Config-file type name, e.g. "TCPClientInterface".
891    fn type_name(&self) -> &str;
892
893    /// Default IFAC size (bytes). 8 for serial/kiss/rnode, 16 for others.
894    fn default_ifac_size(&self) -> usize {
895        16
896    }
897
898    /// Parse from key-value params (config file or external).
899    fn parse_config(
900        &self,
901        name: &str,
902        id: InterfaceId,
903        params: &HashMap<String, String>,
904    ) -> Result<Box<dyn InterfaceConfigData>, String>;
905
906    /// Parse a complete ConfigObj interface section. Flat factories inherit
907    /// the compatibility default; multi-interface factories can inspect children.
908    fn parse_config_section(
909        &self,
910        name: &str,
911        id: InterfaceId,
912        section: ConfigSection<'_>,
913    ) -> Result<Box<dyn InterfaceConfigData>, String> {
914        self.parse_config(name, id, section.params)
915    }
916
917    /// Start the interface from parsed config.
918    fn start(
919        &self,
920        config: Box<dyn InterfaceConfigData>,
921        ctx: StartContext,
922    ) -> io::Result<StartResult>;
923}
924
925impl InterfaceStatusView for InterfaceEntry {
926    fn id(&self) -> InterfaceId {
927        self.id
928    }
929    fn info(&self) -> &InterfaceInfo {
930        &self.info
931    }
932    fn online(&self) -> bool {
933        self.online
934    }
935    fn stats(&self) -> &InterfaceStats {
936        &self.stats
937    }
938    fn tx_drops(&self) -> u64 {
939        self.async_writer_metrics
940            .as_ref()
941            .map(AsyncWriterMetrics::tx_drops)
942            .unwrap_or(0)
943    }
944    fn tx_dropped_bytes(&self) -> u64 {
945        self.async_writer_metrics
946            .as_ref()
947            .map(AsyncWriterMetrics::tx_dropped_bytes)
948            .unwrap_or(0)
949    }
950    fn tx_stalled(&self) -> bool {
951        self.async_writer_metrics
952            .as_ref()
953            .is_some_and(AsyncWriterMetrics::tx_stalled)
954    }
955    fn tx_buffered(&self) -> usize {
956        self.async_writer_metrics
957            .as_ref()
958            .map(AsyncWriterMetrics::tx_buffered)
959            .unwrap_or(0)
960    }
961}
962
963#[cfg(test)]
964mod tests {
965    use super::*;
966
967    #[test]
968    fn shared_medium_hint_matches_interface_class_defaults() {
969        for interface_type in [
970            "AX25KISSInterface",
971            "KISSInterface",
972            "PipeInterface",
973            "RNodeInterface",
974            "RNodeMultiInterface",
975            "SerialInterface",
976            "UDPInterface",
977        ] {
978            assert!(shared_medium_hint(interface_type), "{interface_type}");
979        }
980        for interface_type in [
981            "AutoInterface",
982            "BackboneInterface",
983            "I2PInterface",
984            "LocalInterface",
985            "TCPClientInterface",
986            "TCPServerInterface",
987        ] {
988            assert!(!shared_medium_hint(interface_type), "{interface_type}");
989        }
990    }
991    use crate::event::Event;
992    use rns_core::constants;
993    use std::sync::mpsc;
994
995    struct MockWriter {
996        sent: Vec<Vec<u8>>,
997    }
998
999    impl MockWriter {
1000        fn new() -> Self {
1001            MockWriter { sent: Vec::new() }
1002        }
1003    }
1004
1005    impl Writer for MockWriter {
1006        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
1007            self.sent.push(data.to_vec());
1008            Ok(())
1009        }
1010    }
1011
1012    #[test]
1013    fn interface_entry_construction() {
1014        let entry = InterfaceEntry {
1015            id: InterfaceId(1),
1016            info: InterfaceInfo {
1017                id: InterfaceId(1),
1018                name: String::new(),
1019                mode: constants::MODE_FULL,
1020                gravity: 0,
1021                recursive_prs: false,
1022                announces_from_internal: true,
1023                announces_to_internal: None,
1024                out_capable: true,
1025                in_capable: true,
1026                bitrate: None,
1027                airtime_profile: None,
1028                announce_rate_target: None,
1029                announce_rate_grace: 0,
1030                announce_rate_penalty: 0.0,
1031                announce_cap: constants::ANNOUNCE_CAP,
1032                is_local_client: false,
1033                wants_tunnel: false,
1034                tunnel_id: None,
1035                mtu: constants::MTU as u32,
1036                ia_freq: 0.0,
1037                ip_freq: 0.0,
1038                op_freq: 0.0,
1039                op_samples: 0,
1040                started: 0.0,
1041                ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
1042            },
1043            writer: Box::new(MockWriter::new()),
1044            async_writer_metrics: None,
1045            enabled: true,
1046            online: false,
1047            dynamic: false,
1048            ifac: None,
1049            stats: InterfaceStats::default(),
1050            interface_type: String::new(),
1051            send_retry_at: None,
1052            send_retry_backoff: Duration::ZERO,
1053        };
1054        assert_eq!(entry.id, InterfaceId(1));
1055        assert!(!entry.online);
1056        assert!(!entry.dynamic);
1057    }
1058
1059    #[test]
1060    fn dynamic_registration_inherits_announces_to_internal() {
1061        let template = DynamicInterfaceTemplate {
1062            parent_id: InterfaceId(7),
1063            interface_type: "dynamic-test".into(),
1064            ifac: None,
1065            mode: constants::MODE_GATEWAY,
1066            gravity: -3,
1067            recursive_prs: true,
1068            announces_from_internal: false,
1069            announces_to_internal: Some(true),
1070        };
1071        let info = InterfaceInfo {
1072            id: InterfaceId(8),
1073            name: "child".into(),
1074            mode: constants::MODE_FULL,
1075            gravity: 0,
1076            recursive_prs: false,
1077            announces_from_internal: true,
1078            announces_to_internal: None,
1079            out_capable: true,
1080            in_capable: true,
1081            bitrate: None,
1082            airtime_profile: None,
1083            announce_rate_target: None,
1084            announce_rate_grace: 0,
1085            announce_rate_penalty: 0.0,
1086            announce_cap: constants::ANNOUNCE_CAP,
1087            is_local_client: false,
1088            wants_tunnel: false,
1089            tunnel_id: None,
1090            mtu: constants::MTU as u32,
1091            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
1092            ia_freq: 0.0,
1093            ip_freq: 0.0,
1094            op_freq: 0.0,
1095            op_samples: 0,
1096            started: 0.0,
1097        };
1098
1099        let registration = template.registration(info);
1100
1101        assert_eq!(registration.info.mode, constants::MODE_GATEWAY);
1102        assert_eq!(registration.info.gravity, -3);
1103        assert!(registration.info.recursive_prs);
1104        assert!(!registration.info.announces_from_internal);
1105        assert_eq!(registration.info.announces_to_internal, Some(true));
1106    }
1107
1108    #[test]
1109    fn mock_writer_captures_bytes() {
1110        let mut writer = MockWriter::new();
1111        writer.send_frame(b"hello").unwrap();
1112        writer.send_frame(b"world").unwrap();
1113        assert_eq!(writer.sent.len(), 2);
1114        assert_eq!(writer.sent[0], b"hello");
1115        assert_eq!(writer.sent[1], b"world");
1116    }
1117
1118    #[test]
1119    fn writer_send_frame_produces_output() {
1120        let mut writer = MockWriter::new();
1121        let data = vec![0x01, 0x02, 0x03];
1122        writer.send_frame(&data).unwrap();
1123        assert_eq!(writer.sent[0], data);
1124    }
1125
1126    struct BlockingWriter {
1127        entered_tx: mpsc::Sender<()>,
1128        release_rx: mpsc::Receiver<()>,
1129    }
1130
1131    #[test]
1132    fn confirmed_writer_waits_for_flush_and_reports_write_failures() {
1133        use crate::link_send::{Completion, LinkSendError, SendPool};
1134        use futures::FutureExt;
1135        struct FlushWriter {
1136            entered: mpsc::Sender<()>,
1137            release: mpsc::Receiver<()>,
1138            writes: Arc<AtomicUsize>,
1139        }
1140        impl Writer for FlushWriter {
1141            fn send_frame(&mut self, _: &[u8]) -> io::Result<()> {
1142                self.writes.fetch_add(1, Ordering::SeqCst);
1143                Err(io::ErrorKind::WouldBlock.into())
1144            }
1145            fn flush(&mut self) -> io::Result<()> {
1146                self.entered.send(()).unwrap();
1147                self.release.recv().unwrap();
1148                Ok(())
1149            }
1150        }
1151        let (event_tx, _rx) = crate::event::channel();
1152        let (entered, entered_rx) = mpsc::channel();
1153        let (release_tx, release) = mpsc::channel();
1154        let writes = Arc::new(AtomicUsize::new(0));
1155        let pool = SendPool::new(1);
1156        let (completion, mut receipt) = Completion::new(pool.try_acquire().unwrap());
1157        let (mut writer, _) = wrap_async_writer(
1158            Box::new(FlushWriter {
1159                entered,
1160                release,
1161                writes: writes.clone(),
1162            }),
1163            InterfaceId(999),
1164            "flush test",
1165            event_tx.clone(),
1166            1,
1167        );
1168        writer
1169            .enqueue_confirmed(b"once", &mut Some(completion))
1170            .unwrap();
1171        entered_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1172        assert!((&mut receipt).now_or_never().is_none());
1173        release_tx.send(()).unwrap();
1174        receipt.wait().unwrap();
1175        assert_eq!(
1176            writes.load(Ordering::SeqCst),
1177            1,
1178            "flush must not resubmit the frame"
1179        );
1180
1181        let (completion, receipt) = Completion::new(pool.try_acquire().unwrap());
1182        let (mut writer, _) = wrap_async_writer(
1183            Box::new(FailingWriter {
1184                shutdown_called: Arc::new(AtomicBool::new(false)),
1185            }),
1186            InterfaceId(998),
1187            "failure test",
1188            event_tx,
1189            1,
1190        );
1191        writer
1192            .enqueue_confirmed(b"fail", &mut Some(completion))
1193            .unwrap();
1194        assert_eq!(
1195            receipt.wait(),
1196            Err(LinkSendError::WriteFailed("boom".into()))
1197        );
1198    }
1199
1200    impl Writer for BlockingWriter {
1201        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
1202            let _ = self.entered_tx.send(());
1203            let _ = self.release_rx.recv();
1204            Ok(())
1205        }
1206    }
1207
1208    struct EgressControlledBlockingWriter {
1209        inner: BlockingWriter,
1210        control: EgressControl,
1211    }
1212
1213    impl Writer for EgressControlledBlockingWriter {
1214        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
1215            self.inner.send_frame(data)
1216        }
1217
1218        fn egress_control(&self) -> Option<EgressControl> {
1219            Some(self.control.clone())
1220        }
1221    }
1222
1223    struct FailingWriter {
1224        shutdown_called: Arc<AtomicBool>,
1225    }
1226
1227    struct RecordingWriter {
1228        sent_tx: mpsc::Sender<Vec<u8>>,
1229    }
1230
1231    impl Writer for RecordingWriter {
1232        fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
1233            self.sent_tx
1234                .send(data.to_vec())
1235                .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "test receiver closed"))
1236        }
1237    }
1238
1239    struct BatchRecordingWriter {
1240        batch_tx: mpsc::Sender<usize>,
1241        first_release: Option<mpsc::Receiver<()>>,
1242    }
1243
1244    impl Writer for BatchRecordingWriter {
1245        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
1246            unreachable!("the async worker must use batch delivery")
1247        }
1248
1249        fn send_frames(&mut self, frames: &[Vec<u8>]) -> io::Result<()> {
1250            self.batch_tx.send(frames.len()).unwrap();
1251            if let Some(release) = self.first_release.take() {
1252                release.recv().unwrap();
1253            }
1254            Ok(())
1255        }
1256    }
1257
1258    impl Writer for FailingWriter {
1259        fn send_frame(&mut self, _data: &[u8]) -> io::Result<()> {
1260            Err(io::Error::new(io::ErrorKind::BrokenPipe, "boom"))
1261        }
1262
1263        fn shutdown(&mut self) {
1264            self.shutdown_called.store(true, Ordering::Relaxed);
1265        }
1266    }
1267
1268    #[test]
1269    fn async_writer_returns_wouldblock_when_queue_is_full() {
1270        let (event_tx, _event_rx) = crate::event::channel();
1271        let (entered_tx, entered_rx) = mpsc::channel();
1272        let (release_tx, release_rx) = mpsc::channel();
1273        let (mut writer, metrics) = wrap_async_writer(
1274            Box::new(BlockingWriter {
1275                entered_tx,
1276                release_rx,
1277            }),
1278            InterfaceId(7),
1279            "test",
1280            event_tx,
1281            1,
1282        );
1283
1284        writer.send_frame(&[1]).unwrap();
1285        entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1286        writer.send_frame(&[2]).unwrap();
1287        assert_eq!(metrics.queued_frames(), 1);
1288        let err = writer.send_frame(&[3]).unwrap_err();
1289        assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
1290
1291        release_tx.send(()).unwrap();
1292        entered_rx
1293            .recv_timeout(Duration::from_secs(1))
1294            .expect("the queued frame must wake the writer without another send trigger");
1295        assert_eq!(metrics.queued_frames(), 0);
1296        release_tx.send(()).unwrap();
1297    }
1298
1299    #[test]
1300    fn async_writer_shares_exact_egress_byte_valve_and_stall_gate() {
1301        let (event_tx, _event_rx) = crate::event::channel();
1302        let (entered_tx, entered_rx) = mpsc::channel();
1303        let (release_tx, release_rx) = mpsc::channel();
1304        let control = EgressControl::new(4);
1305        let (mut writer, metrics) = wrap_async_writer(
1306            Box::new(EgressControlledBlockingWriter {
1307                inner: BlockingWriter {
1308                    entered_tx,
1309                    release_rx,
1310                },
1311                control: control.clone(),
1312            }),
1313            InterfaceId(8),
1314            "egress-test",
1315            event_tx,
1316            8,
1317        );
1318
1319        // Two ordinary bytes occupy exactly four HDLC bytes on wire.
1320        writer.send_frame(&[1, 2]).unwrap();
1321        entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1322        assert_eq!(control.reserved_bytes.load(Ordering::Relaxed), 4);
1323        assert_eq!(metrics.tx_buffered(), 4);
1324
1325        // The hard valve drops instead of retaining another four-byte frame.
1326        writer.send_frame(&[3, 4]).unwrap();
1327        assert_eq!(control.reserved_bytes.load(Ordering::Relaxed), 4);
1328        assert_eq!(control.dropped_frames.load(Ordering::Relaxed), 1);
1329        assert_eq!(control.dropped_bytes.load(Ordering::Relaxed), 4);
1330        assert_eq!(metrics.tx_drops(), 1);
1331        assert_eq!(metrics.tx_dropped_bytes(), 4);
1332
1333        release_tx.send(()).unwrap();
1334        let deadline = Instant::now() + Duration::from_secs(1);
1335        while control.reserved_bytes.load(Ordering::Relaxed) != 0 {
1336            assert!(
1337                Instant::now() < deadline,
1338                "egress reservation did not drain"
1339            );
1340            thread::yield_now();
1341        }
1342
1343        control.set_stalled(true);
1344        assert!(metrics.tx_stalled());
1345        writer.send_frame(&[5]).unwrap();
1346        assert_eq!(control.dropped_frames.load(Ordering::Relaxed), 2);
1347        assert_eq!(control.dropped_bytes.load(Ordering::Relaxed), 7);
1348        assert_eq!(metrics.queued_frames(), 0);
1349    }
1350
1351    #[test]
1352    fn async_writer_reports_interface_down_after_worker_failure() {
1353        let (event_tx, event_rx) = crate::event::channel();
1354        let shutdown_called = Arc::new(AtomicBool::new(false));
1355        let (mut writer, metrics) = wrap_async_writer(
1356            Box::new(FailingWriter {
1357                shutdown_called: Arc::clone(&shutdown_called),
1358            }),
1359            InterfaceId(9),
1360            "fail",
1361            event_tx,
1362            2,
1363        );
1364
1365        writer.send_frame(&[1]).unwrap();
1366        let event = event_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1367        assert!(matches!(event, Event::InterfaceDown(InterfaceId(9))));
1368        assert!(!metrics.worker_alive());
1369        assert!(shutdown_called.load(Ordering::Relaxed));
1370    }
1371
1372    #[test]
1373    fn async_writer_preserves_order_through_fifty_thousand_frame_burst() {
1374        const FRAME_COUNT: usize = 50_000;
1375        let (event_tx, _event_rx) = crate::event::channel();
1376        let (sent_tx, sent_rx) = mpsc::channel();
1377        let (mut writer, metrics) = wrap_async_writer(
1378            Box::new(RecordingWriter { sent_tx }),
1379            InterfaceId(11),
1380            "burst",
1381            event_tx,
1382            FRAME_COUNT,
1383        );
1384
1385        for index in 0..FRAME_COUNT {
1386            let payload = (index as u64).to_be_bytes();
1387            writer.send_frame(&payload).unwrap();
1388        }
1389
1390        for index in 0..FRAME_COUNT {
1391            let received = sent_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1392            assert_eq!(received, (index as u64).to_be_bytes());
1393        }
1394        assert!(metrics.worker_alive());
1395    }
1396
1397    #[test]
1398    fn async_writer_coalesces_frames_waiting_behind_an_active_batch() {
1399        let (event_tx, _event_rx) = crate::event::channel();
1400        let (batch_tx, batch_rx) = mpsc::channel();
1401        let (release_tx, release_rx) = mpsc::channel();
1402        let (mut writer, _) = wrap_async_writer(
1403            Box::new(BatchRecordingWriter {
1404                batch_tx,
1405                first_release: Some(release_rx),
1406            }),
1407            InterfaceId(12),
1408            "batch",
1409            event_tx,
1410            16,
1411        );
1412
1413        writer.send_frame(&[0]).unwrap();
1414        assert_eq!(batch_rx.recv_timeout(Duration::from_secs(1)).unwrap(), 1);
1415        for index in 1..=10 {
1416            writer.send_frame(&[index]).unwrap();
1417        }
1418        release_tx.send(()).unwrap();
1419
1420        assert_eq!(batch_rx.recv_timeout(Duration::from_secs(1)).unwrap(), 10);
1421    }
1422}