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, underlay_mark: Option<u32>) -> 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 config.device.is_some() || underlay_mark.is_some() {
199        super::connect_tcp_with_options(
200            &addr,
201            config.device.as_deref(),
202            underlay_mark,
203            runtime.connect_timeout,
204        )?
205    } else {
206        TcpStream::connect_timeout(&addr, runtime.connect_timeout)?
207    };
208    #[cfg(not(target_os = "linux"))]
209    let stream = TcpStream::connect_timeout(&addr, runtime.connect_timeout)?;
210    set_socket_options(&stream)?;
211    Ok(stream)
212}
213
214/// Connect and start the reader thread. Returns the writer for the driver.
215pub fn start(config: TcpClientConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
216    start_with_ifac(config, tx, 0, None)
217}
218
219fn start_with_ifac(
220    config: TcpClientConfig,
221    tx: EventSender,
222    ifac_size: usize,
223    underlay_mark: Option<u32>,
224) -> io::Result<Box<dyn Writer>> {
225    let stream = try_connect(&config, underlay_mark)?;
226    let reader_stream = stream.try_clone()?;
227    let writer_stream = stream.try_clone()?;
228
229    let id = config.interface_id;
230    // Initial connect: writer is None because it's returned directly to the caller
231    let _ = tx.send(Event::InterfaceUp(id, None, None));
232
233    // Spawn reader thread
234    let reader_config = config;
235    let reader_tx = tx;
236    thread::Builder::new()
237        .name(format!("tcp-reader-{}", id.0))
238        .spawn(move || {
239            reader_loop(
240                reader_stream,
241                reader_config,
242                reader_tx,
243                ifac_size,
244                underlay_mark,
245            );
246        })?;
247
248    Ok(Box::new(TcpWriter {
249        stream: writer_stream,
250    }))
251}
252
253/// Reader thread: reads from socket, HDLC-decodes, sends frames to driver.
254/// On disconnect, attempts reconnection.
255fn reader_loop(
256    mut stream: TcpStream,
257    config: TcpClientConfig,
258    tx: EventSender,
259    ifac_size: usize,
260    underlay_mark: Option<u32>,
261) {
262    let id = config.interface_id;
263    let mut decoder = hdlc::Decoder::reticulum(HW_MTU, ifac_size);
264    let mut buf = [0u8; 4096];
265
266    loop {
267        match stream.read(&mut buf) {
268            Ok(0) => {
269                // Connection closed by peer
270                log::warn!("[{}] connection closed", config.name);
271                let _ = tx.send(Event::InterfaceDown(id));
272                match reconnect(&config, &tx, underlay_mark) {
273                    Some(new_stream) => {
274                        stream = new_stream;
275                        decoder = hdlc::Decoder::reticulum(HW_MTU, ifac_size);
276                        continue;
277                    }
278                    None => {
279                        log::error!("[{}] reconnection failed, giving up", config.name);
280                        return;
281                    }
282                }
283            }
284            Ok(n) => {
285                let decoded = decoder.feed_with_diagnostics(&buf[..n]);
286                for frame_len in decoded.invalid_frame_lengths {
287                    log::debug!(
288                        "[{}] invalid HDLC frame of {} bytes received, dropping frame",
289                        config.name,
290                        frame_len,
291                    );
292                }
293                for frame in decoded.frames {
294                    if tx
295                        .send(Event::Frame {
296                            interface_id: id,
297                            data: frame,
298                            rssi: None,
299                            snr: None,
300                        })
301                        .is_err()
302                    {
303                        // Driver shut down
304                        return;
305                    }
306                }
307            }
308            Err(e) => {
309                log::warn!("[{}] read error: {}", config.name, e);
310                let _ = tx.send(Event::InterfaceDown(id));
311                match reconnect(&config, &tx, underlay_mark) {
312                    Some(new_stream) => {
313                        stream = new_stream;
314                        decoder = hdlc::Decoder::reticulum(HW_MTU, ifac_size);
315                        continue;
316                    }
317                    None => {
318                        log::error!("[{}] reconnection failed, giving up", config.name);
319                        return;
320                    }
321                }
322            }
323        }
324    }
325}
326
327/// Attempt to reconnect with retry logic. Returns the new reader stream on success.
328/// Sends the new writer to the driver via InterfaceUp event.
329fn reconnect(
330    config: &TcpClientConfig,
331    tx: &EventSender,
332    underlay_mark: Option<u32>,
333) -> Option<TcpStream> {
334    let mut attempts = 0u32;
335    loop {
336        let runtime = lock_or_recover(&config.runtime, "tcp client runtime").clone();
337        thread::sleep(runtime.reconnect_wait);
338        attempts += 1;
339
340        if let Some(max) = runtime.max_reconnect_tries {
341            if attempts > max {
342                let _ = tx.send(Event::InterfaceDown(config.interface_id));
343                return None;
344            }
345        }
346
347        log::info!("[{}] reconnect attempt {} ...", config.name, attempts);
348
349        match try_connect(config, underlay_mark) {
350            Ok(new_stream) => {
351                // Clone the stream: one for the reader, one for the writer
352                let writer_stream = match new_stream.try_clone() {
353                    Ok(s) => s,
354                    Err(e) => {
355                        log::warn!("[{}] failed to clone stream: {}", config.name, e);
356                        continue;
357                    }
358                };
359                log::info!("[{}] reconnected", config.name);
360                // Send new writer to the driver so it can replace the stale one
361                let new_writer: Box<dyn Writer> = Box::new(TcpWriter {
362                    stream: writer_stream,
363                });
364                let _ = tx.send(Event::InterfaceUp(
365                    config.interface_id,
366                    Some(new_writer),
367                    None,
368                ));
369                return Some(new_stream);
370            }
371            Err(e) => {
372                log::warn!("[{}] reconnect failed: {}", config.name, e);
373            }
374        }
375    }
376}
377
378// --- Factory implementation ---
379
380use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
381use rns_core::transport::types::InterfaceInfo;
382use std::collections::HashMap;
383
384/// Factory for `TCPClientInterface`.
385pub struct TcpClientFactory;
386
387impl InterfaceFactory for TcpClientFactory {
388    fn type_name(&self) -> &str {
389        "TCPClientInterface"
390    }
391
392    fn parse_config(
393        &self,
394        name: &str,
395        id: InterfaceId,
396        params: &HashMap<String, String>,
397    ) -> Result<Box<dyn InterfaceConfigData>, String> {
398        let target_host = params
399            .get("target_host")
400            .cloned()
401            .unwrap_or_else(|| "127.0.0.1".into());
402        let target_port = params
403            .get("target_port")
404            .and_then(|v| v.parse().ok())
405            .unwrap_or(4242);
406
407        Ok(Box::new(TcpClientConfig {
408            name: name.to_string(),
409            target_host,
410            target_port,
411            interface_id: id,
412            device: params.get("device").cloned(),
413            ..TcpClientConfig::default()
414        }))
415    }
416
417    fn start(
418        &self,
419        config: Box<dyn InterfaceConfigData>,
420        ctx: StartContext,
421    ) -> io::Result<StartResult> {
422        let tcp_config = *config
423            .into_any()
424            .downcast::<TcpClientConfig>()
425            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
426
427        let id = tcp_config.interface_id;
428        let name = tcp_config.name.clone();
429        let info = InterfaceInfo {
430            id,
431            name,
432            mode: ctx.mode,
433            gravity: ctx.gravity,
434            recursive_prs: ctx.recursive_prs,
435            announces_from_internal: ctx.announces_from_internal,
436            announces_to_internal: ctx.announces_to_internal,
437            out_capable: true,
438            in_capable: true,
439            bitrate: None,
440            airtime_profile: None,
441            announce_rate_target: None,
442            announce_rate_grace: 0,
443            announce_rate_penalty: 0.0,
444            announce_cap: rns_core::constants::ANNOUNCE_CAP,
445            is_local_client: false,
446            wants_tunnel: false,
447            tunnel_id: None,
448            mtu: 65535,
449            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
450            ia_freq: 0.0,
451            ip_freq: 0.0,
452            op_freq: 0.0,
453            op_samples: 0,
454            started: crate::time::now(),
455        };
456
457        let ifac_size = ctx.ifac.as_ref().map(|ifac| ifac.size).unwrap_or(0);
458        let writer = start_with_ifac(tcp_config, ctx.tx, ifac_size, ctx.underlay_mark)?;
459
460        Ok(StartResult::Simple {
461            id,
462            info,
463            writer,
464            interface_type_name: "TCPClientInterface".to_string(),
465        })
466    }
467}
468
469pub(crate) fn tcp_client_runtime_handle_from_config(
470    config: &TcpClientConfig,
471) -> TcpClientRuntimeConfigHandle {
472    TcpClientRuntimeConfigHandle {
473        interface_name: config.name.clone(),
474        runtime: Arc::clone(&config.runtime),
475        startup: TcpClientRuntime::from_config(config),
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use std::net::TcpListener;
483    use std::time::Duration;
484
485    fn find_free_port() -> u16 {
486        crate::test_support::port()
487    }
488
489    fn make_config(port: u16) -> TcpClientConfig {
490        TcpClientConfig {
491            name: format!("test-tcp-{}", port),
492            target_host: "127.0.0.1".into(),
493            target_port: port,
494            interface_id: InterfaceId(1),
495            reconnect_wait: Duration::from_millis(100),
496            max_reconnect_tries: Some(2),
497            connect_timeout: Duration::from_secs(2),
498            runtime: Arc::new(Mutex::new(TcpClientRuntime {
499                target_host: "127.0.0.1".into(),
500                target_port: port,
501                reconnect_wait: Duration::from_millis(100),
502                max_reconnect_tries: Some(2),
503                connect_timeout: Duration::from_secs(2),
504            })),
505            device: None,
506        }
507    }
508
509    #[test]
510    fn connect_to_listener() {
511        let port = find_free_port();
512        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
513        let (tx, rx) = crate::event::channel();
514
515        let config = make_config(port);
516        let _writer = start(config, tx).unwrap();
517
518        // Accept the connection
519        let _server_stream = listener.accept().unwrap();
520
521        // Should receive InterfaceUp event
522        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
523        assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
524    }
525
526    #[test]
527    fn receive_frame() {
528        let port = find_free_port();
529        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
530        let (tx, rx) = crate::event::channel();
531
532        let config = make_config(port);
533        let _writer = start(config, tx).unwrap();
534
535        let (mut server_stream, _) = listener.accept().unwrap();
536
537        // Drain the InterfaceUp event
538        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
539
540        // Send an HDLC frame from server (>= 19 bytes payload)
541        let payload: Vec<u8> = (0..32).collect();
542        let framed = hdlc::frame(&payload);
543        server_stream.write_all(&framed).unwrap();
544
545        // Should receive Frame event
546        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
547        match event {
548            Event::Frame {
549                interface_id,
550                data,
551                rssi: _,
552                snr: _,
553            } => {
554                assert_eq!(interface_id, InterfaceId(1));
555                assert_eq!(data, payload);
556            }
557            other => panic!("expected Frame, got {:?}", other),
558        }
559    }
560
561    #[test]
562    fn reader_enforces_upstream_frame_bounds_and_ifac_allowance() {
563        let port = find_free_port();
564        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
565        let (tx, rx) = crate::event::channel();
566        let ifac_size = 8;
567
568        let config = make_config(port);
569        let _writer = start_with_ifac(config, tx, ifac_size, None).unwrap();
570        let (mut server_stream, _) = listener.accept().unwrap();
571        assert!(matches!(
572            rx.recv_timeout(Duration::from_secs(1)).unwrap(),
573            Event::InterfaceUp(_, _, _)
574        ));
575
576        // The exact Reticulum header minimum is invalid; the next valid frame
577        // must still be recovered from the same stream.
578        server_stream
579            .write_all(&hdlc::frame(&[0x11; rns_core::constants::HEADER_MINSIZE]))
580            .unwrap();
581        let minimum_valid = vec![0x22; rns_core::constants::HEADER_MINSIZE + 1];
582        server_stream
583            .write_all(&hdlc::frame(&minimum_valid))
584            .unwrap();
585        assert!(matches!(
586            rx.recv_timeout(Duration::from_secs(2)).unwrap(),
587            Event::Frame { data, .. } if data == minimum_valid
588        ));
589
590        // IFAC bytes are part of the on-wire frame allowance.
591        let maximum_valid = vec![0x33; HW_MTU + ifac_size];
592        server_stream
593            .write_all(&hdlc::frame(&maximum_valid))
594            .unwrap();
595        assert!(matches!(
596            rx.recv_timeout(Duration::from_secs(5)).unwrap(),
597            Event::Frame { data, .. } if data == maximum_valid
598        ));
599
600        // An oversized frame is discarded and cannot poison the next frame.
601        server_stream
602            .write_all(&hdlc::frame(&vec![0x44; HW_MTU + ifac_size + 1]))
603            .unwrap();
604        let recovery = vec![0x55; rns_core::constants::HEADER_MINSIZE + 1];
605        server_stream.write_all(&hdlc::frame(&recovery)).unwrap();
606        assert!(matches!(
607            rx.recv_timeout(Duration::from_secs(5)).unwrap(),
608            Event::Frame { data, .. } if data == recovery
609        ));
610    }
611
612    #[test]
613    fn send_frame() {
614        let port = find_free_port();
615        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
616        let (tx, _rx) = crate::event::channel();
617
618        let config = make_config(port);
619        let mut writer = start(config, tx).unwrap();
620
621        let (mut server_stream, _) = listener.accept().unwrap();
622        server_stream
623            .set_read_timeout(Some(Duration::from_secs(2)))
624            .unwrap();
625
626        // Send a frame via writer
627        let payload: Vec<u8> = (0..24).collect();
628        writer.send_frame(&payload).unwrap();
629
630        // Read from server side
631        let mut buf = [0u8; 256];
632        let n = server_stream.read(&mut buf).unwrap();
633        let expected = hdlc::frame(&payload);
634        assert_eq!(&buf[..n], &expected[..]);
635    }
636
637    #[test]
638    fn multiple_frames() {
639        let port = find_free_port();
640        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
641        let (tx, rx) = crate::event::channel();
642
643        let config = make_config(port);
644        let _writer = start(config, tx).unwrap();
645
646        let (mut server_stream, _) = listener.accept().unwrap();
647
648        // Drain InterfaceUp
649        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
650
651        // Send multiple frames
652        let payloads: Vec<Vec<u8>> = (0..3)
653            .map(|i| (0..24).map(|j| j + i * 50).collect())
654            .collect();
655        for p in &payloads {
656            server_stream.write_all(&hdlc::frame(p)).unwrap();
657        }
658
659        // Should receive all frames
660        for expected in &payloads {
661            let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
662            match event {
663                Event::Frame { data, .. } => assert_eq!(&data, expected),
664                other => panic!("expected Frame, got {:?}", other),
665            }
666        }
667    }
668
669    #[test]
670    fn split_across_reads() {
671        let port = find_free_port();
672        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
673        let (tx, rx) = crate::event::channel();
674
675        let config = make_config(port);
676        let _writer = start(config, tx).unwrap();
677
678        let (mut server_stream, _) = listener.accept().unwrap();
679
680        // Drain InterfaceUp
681        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
682
683        // Send frame in two parts
684        let payload: Vec<u8> = (0..32).collect();
685        let framed = hdlc::frame(&payload);
686        let mid = framed.len() / 2;
687
688        server_stream.write_all(&framed[..mid]).unwrap();
689        server_stream.flush().unwrap();
690        thread::sleep(Duration::from_millis(50));
691        server_stream.write_all(&framed[mid..]).unwrap();
692        server_stream.flush().unwrap();
693
694        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
695        match event {
696            Event::Frame { data, .. } => assert_eq!(data, payload),
697            other => panic!("expected Frame, got {:?}", other),
698        }
699    }
700
701    #[test]
702    fn reconnect_on_close() {
703        let port = find_free_port();
704        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
705        listener.set_nonblocking(false).unwrap();
706        let (tx, rx) = crate::event::channel();
707
708        let config = make_config(port);
709        let _writer = start(config, tx).unwrap();
710
711        // Accept first connection and immediately close it
712        let (server_stream, _) = listener.accept().unwrap();
713
714        // Drain InterfaceUp
715        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
716
717        drop(server_stream);
718
719        // Should get InterfaceDown
720        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
721        assert!(matches!(event, Event::InterfaceDown(InterfaceId(1))));
722
723        // Accept the reconnection
724        let _server_stream2 = listener.accept().unwrap();
725
726        // Should get InterfaceUp again
727        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
728        assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
729    }
730
731    #[test]
732    fn socket_options() {
733        let port = find_free_port();
734        let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
735
736        let stream = try_connect(&make_config(port), None).unwrap();
737        let _server = listener.accept().unwrap();
738
739        // Verify TCP_NODELAY is set
740        let fd = stream.as_raw_fd();
741        let mut val: libc::c_int = 0;
742        let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
743        unsafe {
744            libc::getsockopt(
745                fd,
746                libc::IPPROTO_TCP,
747                libc::TCP_NODELAY,
748                &mut val as *mut _ as *mut libc::c_void,
749                &mut len,
750            );
751        }
752        assert_eq!(val, 1, "TCP_NODELAY should be 1");
753    }
754
755    #[test]
756    fn connect_timeout() {
757        // Use a non-routable address to trigger timeout
758        let config = TcpClientConfig {
759            name: "timeout-test".into(),
760            target_host: "192.0.2.1".into(), // TEST-NET, non-routable
761            target_port: 12345,
762            interface_id: InterfaceId(99),
763            reconnect_wait: Duration::from_millis(100),
764            max_reconnect_tries: Some(0),
765            connect_timeout: Duration::from_millis(500),
766            device: None,
767            runtime: Arc::new(Mutex::new(TcpClientRuntime {
768                target_host: "192.0.2.1".into(),
769                target_port: 12345,
770                reconnect_wait: Duration::from_millis(100),
771                max_reconnect_tries: Some(0),
772                connect_timeout: Duration::from_millis(500),
773            })),
774        };
775
776        let start_time = std::time::Instant::now();
777        let result = try_connect(&config, None);
778        let elapsed = start_time.elapsed();
779
780        assert!(result.is_err());
781        // Should timeout roughly around 500ms, definitely under 5s
782        assert!(elapsed < Duration::from_secs(5));
783    }
784}