Skip to main content

rns_net/interface/
tcp.rs

1//! TCP client interface with HDLC framing.
2//!
3//! Matches Python `TCPClientInterface` from `TCPInterface.py`.
4
5use std::io::{self, Write};
6use std::net::{TcpStream, ToSocketAddrs};
7use std::os::unix::io::AsRawFd;
8use std::sync::{Arc, Mutex};
9use std::thread;
10use std::time::Duration;
11
12use rns_core::transport::types::InterfaceId;
13
14use crate::event::{Event, EventSender, LoopContext, LoopSource, SourceStatus};
15use crate::hdlc;
16use crate::interface::{lock_or_recover, wait_while_active, ListenerControl, Writer};
17
18/// Upstream `TCPInterface.HW_MTU`.
19const HW_MTU: usize = 262_144;
20
21/// Configuration for a TCP client interface.
22#[derive(Debug, Clone)]
23pub struct TcpClientConfig {
24    pub name: String,
25    pub target_host: String,
26    pub target_port: u16,
27    pub interface_id: InterfaceId,
28    pub reconnect_wait: Duration,
29    pub max_reconnect_tries: Option<u32>,
30    pub connect_timeout: Duration,
31    /// Linux network interface to bind the socket to (e.g. "usb0").
32    pub device: Option<String>,
33    /// Interface bitrate; it also selects the hardware MTU tier, as upstream.
34    pub bitrate: Option<u64>,
35    /// Hardware MTU, overriding the bitrate tier.
36    pub fixed_mtu: Option<u32>,
37    /// Write buffered frames once the oldest has waited this long instead of
38    /// at the end of the driver's pass (`flush_after_us`; see
39    /// [`super::DEFAULT_FLUSH_AFTER`]).
40    pub flush_after: Option<Duration>,
41    pub runtime: Arc<Mutex<TcpClientRuntime>>,
42}
43
44#[derive(Debug, Clone)]
45pub struct TcpClientRuntime {
46    pub target_host: String,
47    pub target_port: u16,
48    pub reconnect_wait: Duration,
49    pub max_reconnect_tries: Option<u32>,
50    pub connect_timeout: Duration,
51}
52
53impl TcpClientRuntime {
54    pub fn from_config(config: &TcpClientConfig) -> Self {
55        Self {
56            target_host: config.target_host.clone(),
57            target_port: config.target_port,
58            reconnect_wait: config.reconnect_wait,
59            max_reconnect_tries: config.max_reconnect_tries,
60            connect_timeout: config.connect_timeout,
61        }
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct TcpClientRuntimeConfigHandle {
67    pub interface_name: String,
68    pub runtime: Arc<Mutex<TcpClientRuntime>>,
69    pub startup: TcpClientRuntime,
70}
71
72impl Default for TcpClientConfig {
73    fn default() -> Self {
74        let mut config = TcpClientConfig {
75            name: String::new(),
76            target_host: "127.0.0.1".into(),
77            target_port: 4242,
78            interface_id: InterfaceId(0),
79            reconnect_wait: Duration::from_secs(5),
80            max_reconnect_tries: None,
81            connect_timeout: Duration::from_secs(5),
82            device: None,
83            bitrate: None,
84            fixed_mtu: None,
85            flush_after: super::DEFAULT_FLUSH_AFTER,
86            runtime: Arc::new(Mutex::new(TcpClientRuntime {
87                target_host: "127.0.0.1".into(),
88                target_port: 4242,
89                reconnect_wait: Duration::from_secs(5),
90                max_reconnect_tries: None,
91                connect_timeout: Duration::from_secs(5),
92            })),
93        };
94        let startup = TcpClientRuntime::from_config(&config);
95        config.runtime = Arc::new(Mutex::new(startup));
96        config
97    }
98}
99
100/// Writer that sends HDLC-framed data over a TCP stream.
101struct TcpWriter {
102    stream: TcpStream,
103    /// Reused encoding buffer for coalesced writes.
104    scratch: Vec<u8>,
105    flush_after: Option<Duration>,
106}
107
108impl Writer for TcpWriter {
109    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
110        self.stream.write_all(&hdlc::frame(data))
111    }
112
113    fn send_frames(&mut self, frames: &[crate::interface::FrameBytes]) -> io::Result<()> {
114        hdlc::write_frames(&mut self.stream, frames, &mut self.scratch)
115    }
116
117    fn loop_socket(&self) -> Option<std::os::fd::RawFd> {
118        Some(self.stream.as_raw_fd())
119    }
120
121    fn loop_flush_after(&self) -> Option<Duration> {
122        self.flush_after
123    }
124}
125
126/// Set TCP_NODELAY, keepalive and (on Linux) keepalive tuning and user
127/// timeout options. Shared by outbound client sockets and sockets accepted
128/// by the TCP server.
129pub(crate) fn set_socket_options(stream: &TcpStream) -> io::Result<()> {
130    let fd = stream.as_raw_fd();
131    unsafe {
132        // TCP_NODELAY = 1
133        let val: libc::c_int = 1;
134        if libc::setsockopt(
135            fd,
136            libc::IPPROTO_TCP,
137            libc::TCP_NODELAY,
138            &val as *const _ as *const libc::c_void,
139            std::mem::size_of::<libc::c_int>() as libc::socklen_t,
140        ) != 0
141        {
142            return Err(io::Error::last_os_error());
143        }
144
145        // SO_KEEPALIVE = 1
146        if libc::setsockopt(
147            fd,
148            libc::SOL_SOCKET,
149            libc::SO_KEEPALIVE,
150            &val as *const _ as *const libc::c_void,
151            std::mem::size_of::<libc::c_int>() as libc::socklen_t,
152        ) != 0
153        {
154            return Err(io::Error::last_os_error());
155        }
156
157        // Linux-specific keepalive tuning and user timeout
158        #[cfg(target_os = "linux")]
159        {
160            // TCP_KEEPIDLE = 5
161            let idle: libc::c_int = 5;
162            if libc::setsockopt(
163                fd,
164                libc::IPPROTO_TCP,
165                libc::TCP_KEEPIDLE,
166                &idle as *const _ as *const libc::c_void,
167                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
168            ) != 0
169            {
170                return Err(io::Error::last_os_error());
171            }
172
173            // TCP_KEEPINTVL = 2
174            let intvl: libc::c_int = 2;
175            if libc::setsockopt(
176                fd,
177                libc::IPPROTO_TCP,
178                libc::TCP_KEEPINTVL,
179                &intvl as *const _ as *const libc::c_void,
180                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
181            ) != 0
182            {
183                return Err(io::Error::last_os_error());
184            }
185
186            // TCP_KEEPCNT = 12
187            let cnt: libc::c_int = 12;
188            if libc::setsockopt(
189                fd,
190                libc::IPPROTO_TCP,
191                libc::TCP_KEEPCNT,
192                &cnt as *const _ as *const libc::c_void,
193                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
194            ) != 0
195            {
196                return Err(io::Error::last_os_error());
197            }
198
199            // TCP_USER_TIMEOUT = 24000 ms
200            let timeout: libc::c_int = 24_000;
201            if libc::setsockopt(
202                fd,
203                libc::IPPROTO_TCP,
204                libc::TCP_USER_TIMEOUT,
205                &timeout as *const _ as *const libc::c_void,
206                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
207            ) != 0
208            {
209                return Err(io::Error::last_os_error());
210            }
211        }
212    }
213    Ok(())
214}
215
216/// Try to connect to the target host:port with timeout.
217fn try_connect(config: &TcpClientConfig, underlay_mark: Option<u32>) -> io::Result<TcpStream> {
218    let runtime = lock_or_recover(&config.runtime, "tcp client runtime").clone();
219    let addr_str = format!("{}:{}", config.target_host, config.target_port);
220    let addr = addr_str
221        .to_socket_addrs()?
222        .next()
223        .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "no addresses resolved"))?;
224
225    #[cfg(target_os = "linux")]
226    let stream = if config.device.is_some() || underlay_mark.is_some() {
227        super::connect_tcp_with_options(
228            &addr,
229            config.device.as_deref(),
230            underlay_mark,
231            runtime.connect_timeout,
232        )?
233    } else {
234        TcpStream::connect_timeout(&addr, runtime.connect_timeout)?
235    };
236    #[cfg(not(target_os = "linux"))]
237    let stream = TcpStream::connect_timeout(&addr, runtime.connect_timeout)?;
238    set_socket_options(&stream)?;
239    Ok(stream)
240}
241
242/// Connect and start the reader thread. Returns the writer for the driver.
243pub fn start(config: TcpClientConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
244    start_with_ifac(config, tx, 0, None)
245}
246
247fn start_with_ifac(
248    config: TcpClientConfig,
249    tx: EventSender,
250    ifac_size: usize,
251    underlay_mark: Option<u32>,
252) -> io::Result<Box<dyn Writer>> {
253    start_with_ifac_control(config, tx, ifac_size, underlay_mark).map(|(writer, _)| writer)
254}
255
256fn start_with_ifac_control(
257    config: TcpClientConfig,
258    tx: EventSender,
259    ifac_size: usize,
260    underlay_mark: Option<u32>,
261) -> io::Result<(Box<dyn Writer>, ListenerControl)> {
262    let stream = try_connect(&config, underlay_mark)?;
263    let writer_stream = stream.try_clone()?;
264
265    let id = config.interface_id;
266    let flush_after = config.flush_after;
267    // Initial connect: writer is None because it's returned directly to the caller
268    let _ = tx.send(Event::InterfaceUp(id, None, None));
269
270    let control = ListenerControl::new();
271    let link = Arc::new(ClientLink {
272        config,
273        tx,
274        ifac_size,
275        underlay_mark,
276        control: control.clone(),
277    });
278    if let Err(error) = link.serve(stream) {
279        let _ = link.tx.send(Event::InterfaceDown(id));
280        return Err(error);
281    }
282
283    Ok((
284        Box::new(TcpWriter {
285            stream: writer_stream,
286            scratch: Vec::new(),
287            flush_after,
288        }),
289        control,
290    ))
291}
292
293pub(crate) use super::stream_read::{read_stream_frames, StreamRead};
294
295/// What a client connection needs across reconnects.
296struct ClientLink {
297    config: TcpClientConfig,
298    tx: EventSender,
299    ifac_size: usize,
300    underlay_mark: Option<u32>,
301    control: ListenerControl,
302}
303
304impl ClientLink {
305    /// Read `stream` on the driver thread from now on.
306    fn serve(self: &Arc<Self>, stream: TcpStream) -> io::Result<()> {
307        let source = ReaderSource {
308            stream,
309            decoder: hdlc::Decoder::reticulum(HW_MTU, self.ifac_size),
310            link: Arc::clone(self),
311        };
312        self.tx.register_source(
313            crate::interface::loop_writer::read_key(self.config.interface_id),
314            Box::new(source),
315        )
316    }
317
318    /// Report the connection down and reconnect on a thread of its own,
319    /// which exits once the new connection is served on the driver (or
320    /// reconnecting gives up or is stopped).
321    fn connection_lost(self: &Arc<Self>) {
322        let id = self.config.interface_id;
323        let _ = self.tx.push_from_loop([Event::InterfaceDown(id)]);
324        let link = Arc::clone(self);
325        let spawned = thread::Builder::new()
326            .name(format!("tcp-reconn-{}", id.0))
327            .spawn(move || link.reconnect_loop());
328        if let Err(error) = spawned {
329            log::error!(
330                "[{}] cannot start reconnecting, giving up: {}",
331                self.config.name,
332                error
333            );
334        }
335    }
336
337    fn reconnect_loop(self: Arc<Self>) {
338        loop {
339            let Some(stream) = reconnect(&self.config, &self.tx, self.underlay_mark, &self.control)
340            else {
341                if !self.control.should_stop() {
342                    log::error!("[{}] reconnection failed, giving up", self.config.name);
343                }
344                return;
345            };
346            match self.serve(stream) {
347                Ok(()) => return,
348                Err(error) => {
349                    log::warn!(
350                        "[{}] cannot read the new connection: {}",
351                        self.config.name,
352                        error
353                    );
354                    let _ = self.tx.send(Event::InterfaceDown(self.config.interface_id));
355                }
356            }
357        }
358    }
359}
360
361/// The connected socket, read on the driver thread.
362struct ReaderSource {
363    stream: TcpStream,
364    decoder: hdlc::Decoder,
365    link: Arc<ClientLink>,
366}
367
368impl LoopSource for ReaderSource {
369    fn fd(&self) -> std::os::fd::RawFd {
370        self.stream.as_raw_fd()
371    }
372
373    fn stopped(&self) -> bool {
374        self.link.control.should_stop()
375    }
376
377    fn on_ready(&mut self, cx: &mut LoopContext<'_>) -> SourceStatus {
378        let link = &self.link;
379        if link.control.should_stop() {
380            return SourceStatus::Remove;
381        }
382        let name = &link.config.name;
383        match read_stream_frames(
384            self.stream.as_raw_fd(),
385            &mut self.decoder,
386            cx,
387            &link.tx,
388            link.config.interface_id,
389            name,
390            |_, _| {},
391        ) {
392            StreamRead::Rearm => SourceStatus::Rearm,
393            StreamRead::Suspend => SourceStatus::Suspend,
394            StreamRead::Closed => {
395                log::warn!("[{}] connection closed", name);
396                link.connection_lost();
397                SourceStatus::Remove
398            }
399            StreamRead::Failed(error) => {
400                log::warn!("[{}] read error: {}", name, error);
401                link.connection_lost();
402                SourceStatus::Remove
403            }
404            StreamRead::DriverGone => SourceStatus::Remove,
405        }
406    }
407}
408
409/// Attempt to reconnect with retry logic. Returns the new reader stream on success.
410/// Sends the new writer to the driver via InterfaceUp event.
411fn reconnect(
412    config: &TcpClientConfig,
413    tx: &EventSender,
414    underlay_mark: Option<u32>,
415    control: &ListenerControl,
416) -> Option<TcpStream> {
417    let mut attempts = 0u32;
418    loop {
419        let runtime = lock_or_recover(&config.runtime, "tcp client runtime").clone();
420        if !wait_while_active(control, runtime.reconnect_wait) {
421            return None;
422        }
423        attempts += 1;
424
425        if let Some(max) = runtime.max_reconnect_tries {
426            if attempts > max {
427                let _ = tx.send(Event::InterfaceDown(config.interface_id));
428                return None;
429            }
430        }
431
432        log::info!("[{}] reconnect attempt {} ...", config.name, attempts);
433
434        match try_connect(config, underlay_mark) {
435            Ok(new_stream) => {
436                if control.should_stop() {
437                    return None;
438                }
439                // Clone the stream: one for the reader, one for the writer
440                let writer_stream = match new_stream.try_clone() {
441                    Ok(s) => s,
442                    Err(e) => {
443                        log::warn!("[{}] failed to clone stream: {}", config.name, e);
444                        continue;
445                    }
446                };
447                log::info!("[{}] reconnected", config.name);
448                // Send new writer to the driver so it can replace the stale one
449                let new_writer: Box<dyn Writer> = Box::new(TcpWriter {
450                    stream: writer_stream,
451                    scratch: Vec::new(),
452                    flush_after: config.flush_after,
453                });
454                let _ = tx.send(Event::InterfaceUp(
455                    config.interface_id,
456                    Some(new_writer),
457                    None,
458                ));
459                return Some(new_stream);
460            }
461            Err(e) => {
462                log::warn!("[{}] reconnect failed: {}", config.name, e);
463            }
464        }
465    }
466}
467
468// --- Factory implementation ---
469
470use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
471use rns_core::transport::types::InterfaceInfo;
472use std::collections::HashMap;
473
474/// Factory for `TCPClientInterface`.
475pub struct TcpClientFactory;
476
477impl InterfaceFactory for TcpClientFactory {
478    fn type_name(&self) -> &str {
479        "TCPClientInterface"
480    }
481
482    fn parse_config(
483        &self,
484        name: &str,
485        id: InterfaceId,
486        params: &HashMap<String, String>,
487    ) -> Result<Box<dyn InterfaceConfigData>, String> {
488        let target_host = params
489            .get("target_host")
490            .cloned()
491            .unwrap_or_else(|| "127.0.0.1".into());
492        let target_port = params
493            .get("target_port")
494            .and_then(|v| v.parse().ok())
495            .unwrap_or(4242);
496        let (bitrate, fixed_mtu) = super::parse_tcp_rate_params(params)?;
497        let flush_after = super::parse_flush_after(params)?;
498
499        Ok(Box::new(TcpClientConfig {
500            name: name.to_string(),
501            target_host,
502            target_port,
503            interface_id: id,
504            device: params.get("device").cloned(),
505            bitrate,
506            fixed_mtu,
507            flush_after,
508            ..TcpClientConfig::default()
509        }))
510    }
511
512    fn start(
513        &self,
514        config: Box<dyn InterfaceConfigData>,
515        ctx: StartContext,
516    ) -> io::Result<StartResult> {
517        let tcp_config = *config
518            .into_any()
519            .downcast::<TcpClientConfig>()
520            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
521
522        let id = tcp_config.interface_id;
523        let name = tcp_config.name.clone();
524        let info = InterfaceInfo {
525            stamp_costs: Default::default(),
526            id,
527            name,
528            mode: ctx.mode,
529            gravity: ctx.gravity,
530            recursive_prs: ctx.recursive_prs,
531            announces_from_internal: ctx.announces_from_internal,
532            announces_to_internal: ctx.announces_to_internal,
533            out_capable: true,
534            in_capable: true,
535            bitrate: tcp_config.bitrate,
536            airtime_profile: None,
537            announce_rate_target: None,
538            announce_rate_grace: 0,
539            announce_rate_penalty: 0.0,
540            announce_cap: rns_core::constants::ANNOUNCE_CAP,
541            is_local_client: false,
542            wants_tunnel: false,
543            tunnel_id: None,
544            mtu: super::tcp_hardware_mtu(tcp_config.bitrate, tcp_config.fixed_mtu),
545            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
546            ia_freq: 0.0,
547            ip_freq: 0.0,
548            op_freq: 0.0,
549            op_samples: 0,
550            started: crate::time::now(),
551        };
552
553        let ifac_size = ctx.ifac.as_ref().map(|ifac| ifac.size).unwrap_or(0);
554        let (writer, control) =
555            start_with_ifac_control(tcp_config, ctx.tx, ifac_size, ctx.underlay_mark)?;
556
557        Ok(StartResult::Simple {
558            control: Some(control),
559            id,
560            info,
561            writer,
562            interface_type_name: "TCPClientInterface".to_string(),
563        })
564    }
565}
566
567pub(crate) fn tcp_client_runtime_handle_from_config(
568    config: &TcpClientConfig,
569) -> TcpClientRuntimeConfigHandle {
570    TcpClientRuntimeConfigHandle {
571        interface_name: config.name.clone(),
572        runtime: Arc::clone(&config.runtime),
573        startup: TcpClientRuntime::from_config(config),
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use std::io::Read;
581
582    #[test]
583    fn detach_stops_idle_tcp_reader_without_reconnect() {
584        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
585        let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
586        let (server, _) = listener.accept().unwrap();
587        let (tx, rx) = crate::event::channel();
588        let control = ListenerControl::new();
589        let link = Arc::new(ClientLink {
590            config: TcpClientConfig {
591                name: "detach-test".into(),
592                interface_id: InterfaceId(75),
593                ..Default::default()
594            },
595            tx,
596            ifac_size: 0,
597            underlay_mark: None,
598            control: control.clone(),
599        });
600        link.serve(server).unwrap();
601        control.request_stop();
602        rx.sweep_stopped_sources();
603
604        // The reader closed its socket and did not report or reconnect.
605        client
606            .set_read_timeout(Some(Duration::from_secs(2)))
607            .unwrap();
608        let mut buf = [0u8; 1];
609        assert_eq!(std::io::Read::read(&mut client, &mut buf).unwrap(), 0);
610        assert!(rx.try_recv().is_err());
611        drop(link);
612    }
613
614    #[test]
615    fn a_reconnected_client_is_read_on_the_receiving_thread() {
616        let port = find_free_port();
617        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
618        let (tx, rx) = crate::event::channel();
619        let config = TcpClientConfig {
620            interface_id: InterfaceId(76),
621            ..make_config(port)
622        };
623        let _writer = start(config, tx).unwrap();
624        let (first, _) = listener.accept().unwrap();
625        assert!(matches!(
626            rx.recv_timeout(Duration::from_secs(1)).unwrap(),
627            Event::InterfaceUp(InterfaceId(76), None, _)
628        ));
629        drop(first);
630        assert!(matches!(
631            rx.recv_timeout(Duration::from_secs(2)).unwrap(),
632            Event::InterfaceDown(InterfaceId(76))
633        ));
634        let (mut second, _) = listener.accept().unwrap();
635        assert!(matches!(
636            rx.recv_timeout(Duration::from_secs(2)).unwrap(),
637            Event::InterfaceUp(InterfaceId(76), Some(_), _)
638        ));
639        let payload: Vec<u8> = (0..32).collect();
640        second.write_all(&hdlc::frame(&payload)).unwrap();
641        match rx.recv_timeout(Duration::from_secs(2)).unwrap() {
642            Event::Frame { data, .. } => assert_eq!(data, payload.into()),
643            other => panic!("expected Frame, got {:?}", other),
644        }
645        // The reconnect thread exits once the new socket is served here.
646        let reconnecting = || {
647            std::fs::read_dir("/proc/self/task")
648                .unwrap()
649                .filter_map(|task| std::fs::read_to_string(task.ok()?.path().join("comm")).ok())
650                .any(|name| name.trim_end() == "tcp-reconn-76")
651        };
652        let deadline = std::time::Instant::now() + Duration::from_secs(2);
653        while reconnecting() && std::time::Instant::now() < deadline {
654            thread::sleep(Duration::from_millis(10));
655        }
656        assert!(!reconnecting());
657    }
658
659    use std::net::TcpListener;
660    use std::time::Duration;
661
662    fn find_free_port() -> u16 {
663        crate::test_support::port()
664    }
665
666    fn make_config(port: u16) -> TcpClientConfig {
667        TcpClientConfig {
668            name: format!("test-tcp-{}", port),
669            target_host: "127.0.0.1".into(),
670            target_port: port,
671            interface_id: InterfaceId(1),
672            reconnect_wait: Duration::from_millis(100),
673            max_reconnect_tries: Some(2),
674            connect_timeout: Duration::from_secs(2),
675            runtime: Arc::new(Mutex::new(TcpClientRuntime {
676                target_host: "127.0.0.1".into(),
677                target_port: port,
678                reconnect_wait: Duration::from_millis(100),
679                max_reconnect_tries: Some(2),
680                connect_timeout: Duration::from_secs(2),
681            })),
682            flush_after: None,
683            device: None,
684            bitrate: None,
685            fixed_mtu: None,
686        }
687    }
688
689    #[test]
690    fn connect_to_listener() {
691        let port = find_free_port();
692        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
693        let (tx, rx) = crate::event::channel();
694
695        let config = make_config(port);
696        let _writer = start(config, tx).unwrap();
697
698        // Accept the connection
699        let _server_stream = listener.accept().unwrap();
700
701        // Should receive InterfaceUp event
702        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
703        assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
704    }
705
706    #[test]
707    fn receive_frame() {
708        let port = find_free_port();
709        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
710        let (tx, rx) = crate::event::channel();
711
712        let config = make_config(port);
713        let _writer = start(config, tx).unwrap();
714
715        let (mut server_stream, _) = listener.accept().unwrap();
716
717        // Drain the InterfaceUp event
718        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
719
720        // Send an HDLC frame from server (>= 19 bytes payload)
721        let payload: Vec<u8> = (0..32).collect();
722        let framed = hdlc::frame(&payload);
723        server_stream.write_all(&framed).unwrap();
724
725        // Should receive Frame event
726        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
727        match event {
728            Event::Frame {
729                interface_id,
730                data,
731                rssi: _,
732                snr: _,
733            } => {
734                assert_eq!(interface_id, InterfaceId(1));
735                assert_eq!(data, payload.into());
736            }
737            other => panic!("expected Frame, got {:?}", other),
738        }
739    }
740
741    #[test]
742    fn reader_enforces_upstream_frame_bounds_and_ifac_allowance() {
743        let port = find_free_port();
744        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
745        let (tx, rx) = crate::event::channel();
746        let ifac_size = 8;
747
748        let config = make_config(port);
749        let _writer = start_with_ifac(config, tx, ifac_size, None).unwrap();
750        let (mut server_stream, _) = listener.accept().unwrap();
751        assert!(matches!(
752            rx.recv_timeout(Duration::from_secs(1)).unwrap(),
753            Event::InterfaceUp(_, _, _)
754        ));
755
756        // The exact Reticulum header minimum is invalid; the next valid frame
757        // must still be recovered from the same stream.
758        server_stream
759            .write_all(&hdlc::frame(&[0x11; rns_core::constants::HEADER_MINSIZE]))
760            .unwrap();
761        let minimum_valid = vec![0x22; rns_core::constants::HEADER_MINSIZE + 1];
762        server_stream
763            .write_all(&hdlc::frame(&minimum_valid))
764            .unwrap();
765        assert!(matches!(
766            rx.recv_timeout(Duration::from_secs(2)).unwrap(),
767            Event::Frame { data, .. } if data == minimum_valid.into()
768        ));
769
770        // IFAC bytes are part of the on-wire frame allowance.
771        let maximum_valid = vec![0x33; HW_MTU + ifac_size];
772        server_stream
773            .write_all(&hdlc::frame(&maximum_valid))
774            .unwrap();
775        assert!(matches!(
776            rx.recv_timeout(Duration::from_secs(5)).unwrap(),
777            Event::Frame { data, .. } if data == maximum_valid.into()
778        ));
779
780        // An oversized frame is discarded and cannot poison the next frame.
781        server_stream
782            .write_all(&hdlc::frame(&vec![0x44; HW_MTU + ifac_size + 1]))
783            .unwrap();
784        let recovery = vec![0x55; rns_core::constants::HEADER_MINSIZE + 1];
785        server_stream.write_all(&hdlc::frame(&recovery)).unwrap();
786        assert!(matches!(
787            rx.recv_timeout(Duration::from_secs(5)).unwrap(),
788            Event::Frame { data, .. } if data == recovery.into()
789        ));
790    }
791
792    #[test]
793    fn send_frame() {
794        let port = find_free_port();
795        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
796        let (tx, _rx) = crate::event::channel();
797
798        let config = make_config(port);
799        let mut writer = start(config, tx).unwrap();
800
801        let (mut server_stream, _) = listener.accept().unwrap();
802        server_stream
803            .set_read_timeout(Some(Duration::from_secs(2)))
804            .unwrap();
805
806        // Send a frame via writer
807        let payload: Vec<u8> = (0..24).collect();
808        writer.send_frame(&payload).unwrap();
809
810        // Read from server side
811        let mut buf = [0u8; 256];
812        let n = server_stream.read(&mut buf).unwrap();
813        let expected = hdlc::frame(&payload);
814        assert_eq!(&buf[..n], &expected[..]);
815    }
816
817    #[test]
818    fn multiple_frames() {
819        let port = find_free_port();
820        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
821        let (tx, rx) = crate::event::channel();
822
823        let config = make_config(port);
824        let _writer = start(config, tx).unwrap();
825
826        let (mut server_stream, _) = listener.accept().unwrap();
827
828        // Drain InterfaceUp
829        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
830
831        // Send multiple frames
832        let payloads: Vec<Vec<u8>> = (0..3)
833            .map(|i| (0..24).map(|j| j + i * 50).collect())
834            .collect();
835        for p in &payloads {
836            server_stream.write_all(&hdlc::frame(p)).unwrap();
837        }
838
839        // Should receive all frames
840        for expected in &payloads {
841            let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
842            match event {
843                Event::Frame { data, .. } => assert_eq!(&*data, &expected[..]),
844                other => panic!("expected Frame, got {:?}", other),
845            }
846        }
847    }
848
849    #[test]
850    fn split_across_reads() {
851        let port = find_free_port();
852        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
853        let (tx, rx) = crate::event::channel();
854
855        let config = make_config(port);
856        let _writer = start(config, tx).unwrap();
857
858        let (mut server_stream, _) = listener.accept().unwrap();
859
860        // Drain InterfaceUp
861        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
862
863        // Send frame in two parts
864        let payload: Vec<u8> = (0..32).collect();
865        let framed = hdlc::frame(&payload);
866        let mid = framed.len() / 2;
867
868        server_stream.write_all(&framed[..mid]).unwrap();
869        server_stream.flush().unwrap();
870        thread::sleep(Duration::from_millis(50));
871        server_stream.write_all(&framed[mid..]).unwrap();
872        server_stream.flush().unwrap();
873
874        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
875        match event {
876            Event::Frame { data, .. } => assert_eq!(data, payload.into()),
877            other => panic!("expected Frame, got {:?}", other),
878        }
879    }
880
881    #[test]
882    fn reconnect_on_close() {
883        let port = find_free_port();
884        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
885        listener.set_nonblocking(false).unwrap();
886        let (tx, rx) = crate::event::channel();
887
888        let config = make_config(port);
889        let _writer = start(config, tx).unwrap();
890
891        // Accept first connection and immediately close it
892        let (server_stream, _) = listener.accept().unwrap();
893
894        // Drain InterfaceUp
895        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
896
897        drop(server_stream);
898
899        // Should get InterfaceDown
900        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
901        assert!(matches!(event, Event::InterfaceDown(InterfaceId(1))));
902
903        // Accept the reconnection
904        let _server_stream2 = listener.accept().unwrap();
905
906        // Should get InterfaceUp again
907        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
908        assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
909    }
910
911    #[test]
912    fn socket_options() {
913        let port = find_free_port();
914        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
915
916        let stream = try_connect(&make_config(port), None).unwrap();
917        let _server = listener.accept().unwrap();
918
919        // Verify TCP_NODELAY is set
920        let fd = stream.as_raw_fd();
921        let mut val: libc::c_int = 0;
922        let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
923        unsafe {
924            libc::getsockopt(
925                fd,
926                libc::IPPROTO_TCP,
927                libc::TCP_NODELAY,
928                &mut val as *mut _ as *mut libc::c_void,
929                &mut len,
930            );
931        }
932        assert_eq!(val, 1, "TCP_NODELAY should be 1");
933    }
934
935    #[test]
936    fn connect_timeout() {
937        // Use a non-routable address to trigger timeout
938        let config = TcpClientConfig {
939            name: "timeout-test".into(),
940            target_host: "192.0.2.1".into(), // TEST-NET, non-routable
941            target_port: 12345,
942            interface_id: InterfaceId(99),
943            reconnect_wait: Duration::from_millis(100),
944            max_reconnect_tries: Some(0),
945            connect_timeout: Duration::from_millis(500),
946            device: None,
947            bitrate: None,
948            fixed_mtu: None,
949            flush_after: None,
950            runtime: Arc::new(Mutex::new(TcpClientRuntime {
951                target_host: "192.0.2.1".into(),
952                target_port: 12345,
953                reconnect_wait: Duration::from_millis(100),
954                max_reconnect_tries: Some(0),
955                connect_timeout: Duration::from_millis(500),
956            })),
957        };
958
959        let start_time = std::time::Instant::now();
960        let result = try_connect(&config, None);
961        let elapsed = start_time.elapsed();
962
963        assert!(result.is_err());
964        // Should timeout roughly around 500ms, definitely under 5s
965        assert!(elapsed < Duration::from_secs(5));
966    }
967}