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