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