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, Read, 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};
15use crate::hdlc;
16use crate::interface::{lock_or_recover, 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}
93
94impl Writer for TcpWriter {
95    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
96        self.stream.write_all(&hdlc::frame(data))
97    }
98}
99
100/// Set TCP keepalive and timeout socket options (Linux).
101fn set_socket_options(stream: &TcpStream) -> io::Result<()> {
102    let fd = stream.as_raw_fd();
103    unsafe {
104        // TCP_NODELAY = 1
105        let val: libc::c_int = 1;
106        if libc::setsockopt(
107            fd,
108            libc::IPPROTO_TCP,
109            libc::TCP_NODELAY,
110            &val as *const _ as *const libc::c_void,
111            std::mem::size_of::<libc::c_int>() as libc::socklen_t,
112        ) != 0
113        {
114            return Err(io::Error::last_os_error());
115        }
116
117        // SO_KEEPALIVE = 1
118        if libc::setsockopt(
119            fd,
120            libc::SOL_SOCKET,
121            libc::SO_KEEPALIVE,
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        // Linux-specific keepalive tuning and user timeout
130        #[cfg(target_os = "linux")]
131        {
132            // TCP_KEEPIDLE = 5
133            let idle: libc::c_int = 5;
134            if libc::setsockopt(
135                fd,
136                libc::IPPROTO_TCP,
137                libc::TCP_KEEPIDLE,
138                &idle 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            // TCP_KEEPINTVL = 2
146            let intvl: libc::c_int = 2;
147            if libc::setsockopt(
148                fd,
149                libc::IPPROTO_TCP,
150                libc::TCP_KEEPINTVL,
151                &intvl as *const _ as *const libc::c_void,
152                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
153            ) != 0
154            {
155                return Err(io::Error::last_os_error());
156            }
157
158            // TCP_KEEPCNT = 12
159            let cnt: libc::c_int = 12;
160            if libc::setsockopt(
161                fd,
162                libc::IPPROTO_TCP,
163                libc::TCP_KEEPCNT,
164                &cnt as *const _ as *const libc::c_void,
165                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
166            ) != 0
167            {
168                return Err(io::Error::last_os_error());
169            }
170
171            // TCP_USER_TIMEOUT = 24000 ms
172            let timeout: libc::c_int = 24_000;
173            if libc::setsockopt(
174                fd,
175                libc::IPPROTO_TCP,
176                libc::TCP_USER_TIMEOUT,
177                &timeout as *const _ as *const libc::c_void,
178                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
179            ) != 0
180            {
181                return Err(io::Error::last_os_error());
182            }
183        }
184    }
185    Ok(())
186}
187
188/// Try to connect to the target host:port with timeout.
189fn try_connect(config: &TcpClientConfig) -> io::Result<TcpStream> {
190    let runtime = lock_or_recover(&config.runtime, "tcp client runtime").clone();
191    let addr_str = format!("{}:{}", config.target_host, config.target_port);
192    let addr = addr_str
193        .to_socket_addrs()?
194        .next()
195        .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "no addresses resolved"))?;
196
197    #[cfg(target_os = "linux")]
198    let stream = if let Some(ref device) = config.device {
199        connect_with_device(&addr, device, runtime.connect_timeout)?
200    } else {
201        TcpStream::connect_timeout(&addr, runtime.connect_timeout)?
202    };
203    #[cfg(not(target_os = "linux"))]
204    let stream = TcpStream::connect_timeout(&addr, runtime.connect_timeout)?;
205    set_socket_options(&stream)?;
206    Ok(stream)
207}
208
209/// Create a TCP socket, bind it to a network device, then connect with timeout.
210#[cfg(target_os = "linux")]
211fn connect_with_device(
212    addr: &std::net::SocketAddr,
213    device: &str,
214    timeout: Duration,
215) -> io::Result<TcpStream> {
216    use std::os::unix::io::{FromRawFd, RawFd};
217
218    let domain = if addr.is_ipv4() {
219        libc::AF_INET
220    } else {
221        libc::AF_INET6
222    };
223    let fd: RawFd = unsafe { libc::socket(domain, libc::SOCK_STREAM, 0) };
224    if fd < 0 {
225        return Err(io::Error::last_os_error());
226    }
227
228    // Ensure the fd is closed on error paths
229    let stream = unsafe { TcpStream::from_raw_fd(fd) };
230
231    super::bind_to_device(stream.as_raw_fd(), device)?;
232
233    // Set non-blocking for connect-with-timeout
234    stream.set_nonblocking(true)?;
235
236    let (sockaddr, socklen) = socket_addr_to_raw(addr);
237    let ret = unsafe {
238        libc::connect(
239            stream.as_raw_fd(),
240            &sockaddr as *const libc::sockaddr_storage as *const libc::sockaddr,
241            socklen,
242        )
243    };
244
245    if ret != 0 {
246        let err = io::Error::last_os_error();
247        if err.raw_os_error() != Some(libc::EINPROGRESS) {
248            return Err(err);
249        }
250    }
251
252    // Poll for connect completion
253    let mut pollfd = libc::pollfd {
254        fd: stream.as_raw_fd(),
255        events: libc::POLLOUT,
256        revents: 0,
257    };
258    let timeout_ms = timeout.as_millis() as libc::c_int;
259    let poll_ret = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
260
261    if poll_ret == 0 {
262        return Err(io::Error::new(io::ErrorKind::TimedOut, "connect timed out"));
263    }
264    if poll_ret < 0 {
265        return Err(io::Error::last_os_error());
266    }
267
268    // Check SO_ERROR
269    let mut err_val: libc::c_int = 0;
270    let mut err_len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
271    let ret = unsafe {
272        libc::getsockopt(
273            stream.as_raw_fd(),
274            libc::SOL_SOCKET,
275            libc::SO_ERROR,
276            &mut err_val as *mut _ as *mut libc::c_void,
277            &mut err_len,
278        )
279    };
280    if ret != 0 {
281        return Err(io::Error::last_os_error());
282    }
283    if err_val != 0 {
284        return Err(io::Error::from_raw_os_error(err_val));
285    }
286
287    // Set back to blocking
288    stream.set_nonblocking(false)?;
289
290    Ok(stream)
291}
292
293/// Convert a `SocketAddr` to a raw `sockaddr_storage` for `libc::connect`.
294#[cfg(target_os = "linux")]
295fn socket_addr_to_raw(addr: &std::net::SocketAddr) -> (libc::sockaddr_storage, libc::socklen_t) {
296    let mut storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
297    match addr {
298        std::net::SocketAddr::V4(v4) => {
299            let sin: &mut libc::sockaddr_in = unsafe {
300                &mut *(&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in)
301            };
302            sin.sin_family = libc::AF_INET as libc::sa_family_t;
303            sin.sin_port = v4.port().to_be();
304            sin.sin_addr = libc::in_addr {
305                s_addr: u32::from_ne_bytes(v4.ip().octets()),
306            };
307            (
308                storage,
309                std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
310            )
311        }
312        std::net::SocketAddr::V6(v6) => {
313            let sin6: &mut libc::sockaddr_in6 = unsafe {
314                &mut *(&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in6)
315            };
316            sin6.sin6_family = libc::AF_INET6 as libc::sa_family_t;
317            sin6.sin6_port = v6.port().to_be();
318            sin6.sin6_addr = libc::in6_addr {
319                s6_addr: v6.ip().octets(),
320            };
321            sin6.sin6_flowinfo = v6.flowinfo();
322            sin6.sin6_scope_id = v6.scope_id();
323            (
324                storage,
325                std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
326            )
327        }
328    }
329}
330
331/// Connect and start the reader thread. Returns the writer for the driver.
332pub fn start(config: TcpClientConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
333    start_with_ifac(config, tx, 0)
334}
335
336fn start_with_ifac(
337    config: TcpClientConfig,
338    tx: EventSender,
339    ifac_size: usize,
340) -> io::Result<Box<dyn Writer>> {
341    let stream = try_connect(&config)?;
342    let reader_stream = stream.try_clone()?;
343    let writer_stream = stream.try_clone()?;
344
345    let id = config.interface_id;
346    // Initial connect: writer is None because it's returned directly to the caller
347    let _ = tx.send(Event::InterfaceUp(id, None, None));
348
349    // Spawn reader thread
350    let reader_config = config;
351    let reader_tx = tx;
352    thread::Builder::new()
353        .name(format!("tcp-reader-{}", id.0))
354        .spawn(move || {
355            reader_loop(reader_stream, reader_config, reader_tx, ifac_size);
356        })?;
357
358    Ok(Box::new(TcpWriter {
359        stream: writer_stream,
360    }))
361}
362
363/// Reader thread: reads from socket, HDLC-decodes, sends frames to driver.
364/// On disconnect, attempts reconnection.
365fn reader_loop(mut stream: TcpStream, config: TcpClientConfig, tx: EventSender, ifac_size: usize) {
366    let id = config.interface_id;
367    let mut decoder = hdlc::Decoder::reticulum(HW_MTU, ifac_size);
368    let mut buf = [0u8; 4096];
369
370    loop {
371        match stream.read(&mut buf) {
372            Ok(0) => {
373                // Connection closed by peer
374                log::warn!("[{}] connection closed", config.name);
375                let _ = tx.send(Event::InterfaceDown(id));
376                match reconnect(&config, &tx) {
377                    Some(new_stream) => {
378                        stream = new_stream;
379                        decoder = hdlc::Decoder::reticulum(HW_MTU, ifac_size);
380                        continue;
381                    }
382                    None => {
383                        log::error!("[{}] reconnection failed, giving up", config.name);
384                        return;
385                    }
386                }
387            }
388            Ok(n) => {
389                let decoded = decoder.feed_with_diagnostics(&buf[..n]);
390                for frame_len in decoded.invalid_frame_lengths {
391                    log::debug!(
392                        "[{}] invalid HDLC frame of {} bytes received, dropping frame",
393                        config.name,
394                        frame_len,
395                    );
396                }
397                for frame in decoded.frames {
398                    if tx
399                        .send(Event::Frame {
400                            interface_id: id,
401                            data: frame,
402                            rssi: None,
403                            snr: None,
404                        })
405                        .is_err()
406                    {
407                        // Driver shut down
408                        return;
409                    }
410                }
411            }
412            Err(e) => {
413                log::warn!("[{}] read error: {}", config.name, e);
414                let _ = tx.send(Event::InterfaceDown(id));
415                match reconnect(&config, &tx) {
416                    Some(new_stream) => {
417                        stream = new_stream;
418                        decoder = hdlc::Decoder::reticulum(HW_MTU, ifac_size);
419                        continue;
420                    }
421                    None => {
422                        log::error!("[{}] reconnection failed, giving up", config.name);
423                        return;
424                    }
425                }
426            }
427        }
428    }
429}
430
431/// Attempt to reconnect with retry logic. Returns the new reader stream on success.
432/// Sends the new writer to the driver via InterfaceUp event.
433fn reconnect(config: &TcpClientConfig, tx: &EventSender) -> Option<TcpStream> {
434    let mut attempts = 0u32;
435    loop {
436        let runtime = lock_or_recover(&config.runtime, "tcp client runtime").clone();
437        thread::sleep(runtime.reconnect_wait);
438        attempts += 1;
439
440        if let Some(max) = runtime.max_reconnect_tries {
441            if attempts > max {
442                let _ = tx.send(Event::InterfaceDown(config.interface_id));
443                return None;
444            }
445        }
446
447        log::info!("[{}] reconnect attempt {} ...", config.name, attempts);
448
449        match try_connect(config) {
450            Ok(new_stream) => {
451                // Clone the stream: one for the reader, one for the writer
452                let writer_stream = match new_stream.try_clone() {
453                    Ok(s) => s,
454                    Err(e) => {
455                        log::warn!("[{}] failed to clone stream: {}", config.name, e);
456                        continue;
457                    }
458                };
459                log::info!("[{}] reconnected", config.name);
460                // Send new writer to the driver so it can replace the stale one
461                let new_writer: Box<dyn Writer> = Box::new(TcpWriter {
462                    stream: writer_stream,
463                });
464                let _ = tx.send(Event::InterfaceUp(
465                    config.interface_id,
466                    Some(new_writer),
467                    None,
468                ));
469                return Some(new_stream);
470            }
471            Err(e) => {
472                log::warn!("[{}] reconnect failed: {}", config.name, e);
473            }
474        }
475    }
476}
477
478// --- Factory implementation ---
479
480use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
481use rns_core::transport::types::InterfaceInfo;
482use std::collections::HashMap;
483
484/// Factory for `TCPClientInterface`.
485pub struct TcpClientFactory;
486
487impl InterfaceFactory for TcpClientFactory {
488    fn type_name(&self) -> &str {
489        "TCPClientInterface"
490    }
491
492    fn parse_config(
493        &self,
494        name: &str,
495        id: InterfaceId,
496        params: &HashMap<String, String>,
497    ) -> Result<Box<dyn InterfaceConfigData>, String> {
498        let target_host = params
499            .get("target_host")
500            .cloned()
501            .unwrap_or_else(|| "127.0.0.1".into());
502        let target_port = params
503            .get("target_port")
504            .and_then(|v| v.parse().ok())
505            .unwrap_or(4242);
506
507        Ok(Box::new(TcpClientConfig {
508            name: name.to_string(),
509            target_host,
510            target_port,
511            interface_id: id,
512            device: params.get("device").cloned(),
513            ..TcpClientConfig::default()
514        }))
515    }
516
517    fn start(
518        &self,
519        config: Box<dyn InterfaceConfigData>,
520        ctx: StartContext,
521    ) -> io::Result<StartResult> {
522        let tcp_config = *config
523            .into_any()
524            .downcast::<TcpClientConfig>()
525            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
526
527        let id = tcp_config.interface_id;
528        let name = tcp_config.name.clone();
529        let info = InterfaceInfo {
530            id,
531            name,
532            mode: ctx.mode,
533            gravity: ctx.gravity,
534            recursive_prs: ctx.recursive_prs,
535            announces_from_internal: ctx.announces_from_internal,
536            announces_to_internal: ctx.announces_to_internal,
537            out_capable: true,
538            in_capable: true,
539            bitrate: None,
540            airtime_profile: None,
541            announce_rate_target: None,
542            announce_rate_grace: 0,
543            announce_rate_penalty: 0.0,
544            announce_cap: rns_core::constants::ANNOUNCE_CAP,
545            is_local_client: false,
546            wants_tunnel: false,
547            tunnel_id: None,
548            mtu: 65535,
549            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
550            ia_freq: 0.0,
551            ip_freq: 0.0,
552            op_freq: 0.0,
553            op_samples: 0,
554            started: crate::time::now(),
555        };
556
557        let ifac_size = ctx.ifac.as_ref().map(|ifac| ifac.size).unwrap_or(0);
558        let writer = start_with_ifac(tcp_config, ctx.tx, ifac_size)?;
559
560        Ok(StartResult::Simple {
561            id,
562            info,
563            writer,
564            interface_type_name: "TCPClientInterface".to_string(),
565        })
566    }
567}
568
569pub(crate) fn tcp_client_runtime_handle_from_config(
570    config: &TcpClientConfig,
571) -> TcpClientRuntimeConfigHandle {
572    TcpClientRuntimeConfigHandle {
573        interface_name: config.name.clone(),
574        runtime: Arc::clone(&config.runtime),
575        startup: TcpClientRuntime::from_config(config),
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use std::net::TcpListener;
583    use std::time::Duration;
584
585    fn find_free_port() -> u16 {
586        TcpListener::bind("127.0.0.1:0")
587            .unwrap()
588            .local_addr()
589            .unwrap()
590            .port()
591    }
592
593    fn make_config(port: u16) -> TcpClientConfig {
594        TcpClientConfig {
595            name: format!("test-tcp-{}", port),
596            target_host: "127.0.0.1".into(),
597            target_port: port,
598            interface_id: InterfaceId(1),
599            reconnect_wait: Duration::from_millis(100),
600            max_reconnect_tries: Some(2),
601            connect_timeout: Duration::from_secs(2),
602            runtime: Arc::new(Mutex::new(TcpClientRuntime {
603                target_host: "127.0.0.1".into(),
604                target_port: port,
605                reconnect_wait: Duration::from_millis(100),
606                max_reconnect_tries: Some(2),
607                connect_timeout: Duration::from_secs(2),
608            })),
609            device: None,
610        }
611    }
612
613    #[test]
614    fn connect_to_listener() {
615        let port = find_free_port();
616        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
617        let (tx, rx) = crate::event::channel();
618
619        let config = make_config(port);
620        let _writer = start(config, tx).unwrap();
621
622        // Accept the connection
623        let _server_stream = listener.accept().unwrap();
624
625        // Should receive InterfaceUp event
626        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
627        assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
628    }
629
630    #[test]
631    fn receive_frame() {
632        let port = find_free_port();
633        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
634        let (tx, rx) = crate::event::channel();
635
636        let config = make_config(port);
637        let _writer = start(config, tx).unwrap();
638
639        let (mut server_stream, _) = listener.accept().unwrap();
640
641        // Drain the InterfaceUp event
642        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
643
644        // Send an HDLC frame from server (>= 19 bytes payload)
645        let payload: Vec<u8> = (0..32).collect();
646        let framed = hdlc::frame(&payload);
647        server_stream.write_all(&framed).unwrap();
648
649        // Should receive Frame event
650        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
651        match event {
652            Event::Frame {
653                interface_id,
654                data,
655                rssi: _,
656                snr: _,
657            } => {
658                assert_eq!(interface_id, InterfaceId(1));
659                assert_eq!(data, payload);
660            }
661            other => panic!("expected Frame, got {:?}", other),
662        }
663    }
664
665    #[test]
666    fn reader_enforces_upstream_frame_bounds_and_ifac_allowance() {
667        let port = find_free_port();
668        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
669        let (tx, rx) = crate::event::channel();
670        let ifac_size = 8;
671
672        let config = make_config(port);
673        let _writer = start_with_ifac(config, tx, ifac_size).unwrap();
674        let (mut server_stream, _) = listener.accept().unwrap();
675        assert!(matches!(
676            rx.recv_timeout(Duration::from_secs(1)).unwrap(),
677            Event::InterfaceUp(_, _, _)
678        ));
679
680        // The exact Reticulum header minimum is invalid; the next valid frame
681        // must still be recovered from the same stream.
682        server_stream
683            .write_all(&hdlc::frame(&vec![
684                0x11;
685                rns_core::constants::HEADER_MINSIZE
686            ]))
687            .unwrap();
688        let minimum_valid = vec![0x22; rns_core::constants::HEADER_MINSIZE + 1];
689        server_stream
690            .write_all(&hdlc::frame(&minimum_valid))
691            .unwrap();
692        assert!(matches!(
693            rx.recv_timeout(Duration::from_secs(2)).unwrap(),
694            Event::Frame { data, .. } if data == minimum_valid
695        ));
696
697        // IFAC bytes are part of the on-wire frame allowance.
698        let maximum_valid = vec![0x33; HW_MTU + ifac_size];
699        server_stream
700            .write_all(&hdlc::frame(&maximum_valid))
701            .unwrap();
702        assert!(matches!(
703            rx.recv_timeout(Duration::from_secs(5)).unwrap(),
704            Event::Frame { data, .. } if data == maximum_valid
705        ));
706
707        // An oversized frame is discarded and cannot poison the next frame.
708        server_stream
709            .write_all(&hdlc::frame(&vec![0x44; HW_MTU + ifac_size + 1]))
710            .unwrap();
711        let recovery = vec![0x55; rns_core::constants::HEADER_MINSIZE + 1];
712        server_stream.write_all(&hdlc::frame(&recovery)).unwrap();
713        assert!(matches!(
714            rx.recv_timeout(Duration::from_secs(5)).unwrap(),
715            Event::Frame { data, .. } if data == recovery
716        ));
717    }
718
719    #[test]
720    fn send_frame() {
721        let port = find_free_port();
722        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
723        let (tx, _rx) = crate::event::channel();
724
725        let config = make_config(port);
726        let mut writer = start(config, tx).unwrap();
727
728        let (mut server_stream, _) = listener.accept().unwrap();
729        server_stream
730            .set_read_timeout(Some(Duration::from_secs(2)))
731            .unwrap();
732
733        // Send a frame via writer
734        let payload: Vec<u8> = (0..24).collect();
735        writer.send_frame(&payload).unwrap();
736
737        // Read from server side
738        let mut buf = [0u8; 256];
739        let n = server_stream.read(&mut buf).unwrap();
740        let expected = hdlc::frame(&payload);
741        assert_eq!(&buf[..n], &expected[..]);
742    }
743
744    #[test]
745    fn multiple_frames() {
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        let (mut server_stream, _) = listener.accept().unwrap();
754
755        // Drain InterfaceUp
756        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
757
758        // Send multiple frames
759        let payloads: Vec<Vec<u8>> = (0..3)
760            .map(|i| (0..24).map(|j| j + i * 50).collect())
761            .collect();
762        for p in &payloads {
763            server_stream.write_all(&hdlc::frame(p)).unwrap();
764        }
765
766        // Should receive all frames
767        for expected in &payloads {
768            let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
769            match event {
770                Event::Frame { data, .. } => assert_eq!(&data, expected),
771                other => panic!("expected Frame, got {:?}", other),
772            }
773        }
774    }
775
776    #[test]
777    fn split_across_reads() {
778        let port = find_free_port();
779        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
780        let (tx, rx) = crate::event::channel();
781
782        let config = make_config(port);
783        let _writer = start(config, tx).unwrap();
784
785        let (mut server_stream, _) = listener.accept().unwrap();
786
787        // Drain InterfaceUp
788        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
789
790        // Send frame in two parts
791        let payload: Vec<u8> = (0..32).collect();
792        let framed = hdlc::frame(&payload);
793        let mid = framed.len() / 2;
794
795        server_stream.write_all(&framed[..mid]).unwrap();
796        server_stream.flush().unwrap();
797        thread::sleep(Duration::from_millis(50));
798        server_stream.write_all(&framed[mid..]).unwrap();
799        server_stream.flush().unwrap();
800
801        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
802        match event {
803            Event::Frame { data, .. } => assert_eq!(data, payload),
804            other => panic!("expected Frame, got {:?}", other),
805        }
806    }
807
808    #[test]
809    fn reconnect_on_close() {
810        let port = find_free_port();
811        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
812        listener.set_nonblocking(false).unwrap();
813        let (tx, rx) = crate::event::channel();
814
815        let config = make_config(port);
816        let _writer = start(config, tx).unwrap();
817
818        // Accept first connection and immediately close it
819        let (server_stream, _) = listener.accept().unwrap();
820
821        // Drain InterfaceUp
822        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
823
824        drop(server_stream);
825
826        // Should get InterfaceDown
827        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
828        assert!(matches!(event, Event::InterfaceDown(InterfaceId(1))));
829
830        // Accept the reconnection
831        let _server_stream2 = listener.accept().unwrap();
832
833        // Should get InterfaceUp again
834        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
835        assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
836    }
837
838    #[test]
839    fn socket_options() {
840        let port = find_free_port();
841        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
842
843        let stream = try_connect(&make_config(port)).unwrap();
844        let _server = listener.accept().unwrap();
845
846        // Verify TCP_NODELAY is set
847        let fd = stream.as_raw_fd();
848        let mut val: libc::c_int = 0;
849        let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
850        unsafe {
851            libc::getsockopt(
852                fd,
853                libc::IPPROTO_TCP,
854                libc::TCP_NODELAY,
855                &mut val as *mut _ as *mut libc::c_void,
856                &mut len,
857            );
858        }
859        assert_eq!(val, 1, "TCP_NODELAY should be 1");
860    }
861
862    #[test]
863    fn connect_timeout() {
864        // Use a non-routable address to trigger timeout
865        let config = TcpClientConfig {
866            name: "timeout-test".into(),
867            target_host: "192.0.2.1".into(), // TEST-NET, non-routable
868            target_port: 12345,
869            interface_id: InterfaceId(99),
870            reconnect_wait: Duration::from_millis(100),
871            max_reconnect_tries: Some(0),
872            connect_timeout: Duration::from_millis(500),
873            device: None,
874            runtime: Arc::new(Mutex::new(TcpClientRuntime {
875                target_host: "192.0.2.1".into(),
876                target_port: 12345,
877                reconnect_wait: Duration::from_millis(100),
878                max_reconnect_tries: Some(0),
879                connect_timeout: Duration::from_millis(500),
880            })),
881            ..TcpClientConfig::default()
882        };
883
884        let start_time = std::time::Instant::now();
885        let result = try_connect(&config);
886        let elapsed = start_time.elapsed();
887
888        assert!(result.is_err());
889        // Should timeout roughly around 500ms, definitely under 5s
890        assert!(elapsed < Duration::from_secs(5));
891    }
892}