Skip to main content

rns_net/interface/
tcp.rs

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