1#[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
45pub 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
57pub 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#[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#[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#[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
223pub 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
237pub 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 fn flush(&mut self) -> io::Result<()> {
252 Ok(())
253 }
254
255 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
330pub 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 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 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
586pub struct InterfaceEntry {
588 pub id: InterfaceId,
589 pub info: InterfaceInfo,
590 pub writer: Box<dyn Writer>,
591 pub async_writer_metrics: Option<AsyncWriterMetrics>,
592 pub enabled: bool,
594 pub online: bool,
595 pub dynamic: bool,
598 pub ifac: Option<IfacState>,
600 pub stats: InterfaceStats,
602 pub interface_type: String,
604 pub send_retry_at: Option<Instant>,
606 pub send_retry_backoff: Duration,
608}
609
610#[allow(clippy::large_enum_variant)]
613pub enum StartResult {
614 Simple {
616 id: InterfaceId,
617 info: InterfaceInfo,
618 writer: Box<dyn Writer>,
619 interface_type_name: String,
620 },
621 Listener { control: Option<ListenerControl> },
623 Multi(Vec<SubInterface>),
625}
626
627pub struct SubInterface {
629 pub id: InterfaceId,
630 pub info: InterfaceInfo,
631 pub writer: Box<dyn Writer>,
632 pub interface_type_name: String,
633}
634
635pub 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 pub underlay_mark: Option<u32>,
648}
649
650pub trait InterfaceConfigData: Send + Any {
652 fn as_any(&self) -> &dyn Any;
653 fn into_any(self: Box<Self>) -> Box<dyn Any>;
654}
655
656pub 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
704pub trait InterfaceFactory: Send + Sync {
706 fn type_name(&self) -> &str;
708
709 fn default_ifac_size(&self) -> usize {
711 16
712 }
713
714 fn parse_config(
716 &self,
717 name: &str,
718 id: InterfaceId,
719 params: &HashMap<String, String>,
720 ) -> Result<Box<dyn InterfaceConfigData>, String>;
721
722 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 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 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 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}