1use 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#[derive(Debug, Clone)]
20pub struct TcpClientConfig {
21 pub name: String,
22 pub target_host: String,
23 pub target_port: u16,
24 pub interface_id: InterfaceId,
25 pub reconnect_wait: Duration,
26 pub max_reconnect_tries: Option<u32>,
27 pub connect_timeout: Duration,
28 pub device: Option<String>,
30 pub runtime: Arc<Mutex<TcpClientRuntime>>,
31}
32
33#[derive(Debug, Clone)]
34pub struct TcpClientRuntime {
35 pub target_host: String,
36 pub target_port: u16,
37 pub reconnect_wait: Duration,
38 pub max_reconnect_tries: Option<u32>,
39 pub connect_timeout: Duration,
40}
41
42impl TcpClientRuntime {
43 pub fn from_config(config: &TcpClientConfig) -> Self {
44 Self {
45 target_host: config.target_host.clone(),
46 target_port: config.target_port,
47 reconnect_wait: config.reconnect_wait,
48 max_reconnect_tries: config.max_reconnect_tries,
49 connect_timeout: config.connect_timeout,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
55pub struct TcpClientRuntimeConfigHandle {
56 pub interface_name: String,
57 pub runtime: Arc<Mutex<TcpClientRuntime>>,
58 pub startup: TcpClientRuntime,
59}
60
61impl Default for TcpClientConfig {
62 fn default() -> Self {
63 let mut config = TcpClientConfig {
64 name: String::new(),
65 target_host: "127.0.0.1".into(),
66 target_port: 4242,
67 interface_id: InterfaceId(0),
68 reconnect_wait: Duration::from_secs(5),
69 max_reconnect_tries: None,
70 connect_timeout: Duration::from_secs(5),
71 device: None,
72 runtime: Arc::new(Mutex::new(TcpClientRuntime {
73 target_host: "127.0.0.1".into(),
74 target_port: 4242,
75 reconnect_wait: Duration::from_secs(5),
76 max_reconnect_tries: None,
77 connect_timeout: Duration::from_secs(5),
78 })),
79 };
80 let startup = TcpClientRuntime::from_config(&config);
81 config.runtime = Arc::new(Mutex::new(startup));
82 config
83 }
84}
85
86struct TcpWriter {
88 stream: TcpStream,
89}
90
91impl Writer for TcpWriter {
92 fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
93 self.stream.write_all(&hdlc::frame(data))
94 }
95}
96
97fn set_socket_options(stream: &TcpStream) -> io::Result<()> {
99 let fd = stream.as_raw_fd();
100 unsafe {
101 let val: libc::c_int = 1;
103 if libc::setsockopt(
104 fd,
105 libc::IPPROTO_TCP,
106 libc::TCP_NODELAY,
107 &val as *const _ as *const libc::c_void,
108 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
109 ) != 0
110 {
111 return Err(io::Error::last_os_error());
112 }
113
114 if libc::setsockopt(
116 fd,
117 libc::SOL_SOCKET,
118 libc::SO_KEEPALIVE,
119 &val as *const _ as *const libc::c_void,
120 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
121 ) != 0
122 {
123 return Err(io::Error::last_os_error());
124 }
125
126 #[cfg(target_os = "linux")]
128 {
129 let idle: libc::c_int = 5;
131 if libc::setsockopt(
132 fd,
133 libc::IPPROTO_TCP,
134 libc::TCP_KEEPIDLE,
135 &idle as *const _ as *const libc::c_void,
136 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
137 ) != 0
138 {
139 return Err(io::Error::last_os_error());
140 }
141
142 let intvl: libc::c_int = 2;
144 if libc::setsockopt(
145 fd,
146 libc::IPPROTO_TCP,
147 libc::TCP_KEEPINTVL,
148 &intvl as *const _ as *const libc::c_void,
149 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
150 ) != 0
151 {
152 return Err(io::Error::last_os_error());
153 }
154
155 let cnt: libc::c_int = 12;
157 if libc::setsockopt(
158 fd,
159 libc::IPPROTO_TCP,
160 libc::TCP_KEEPCNT,
161 &cnt as *const _ as *const libc::c_void,
162 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
163 ) != 0
164 {
165 return Err(io::Error::last_os_error());
166 }
167
168 let timeout: libc::c_int = 24_000;
170 if libc::setsockopt(
171 fd,
172 libc::IPPROTO_TCP,
173 libc::TCP_USER_TIMEOUT,
174 &timeout as *const _ as *const libc::c_void,
175 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
176 ) != 0
177 {
178 return Err(io::Error::last_os_error());
179 }
180 }
181 }
182 Ok(())
183}
184
185fn try_connect(config: &TcpClientConfig) -> io::Result<TcpStream> {
187 let runtime = lock_or_recover(&config.runtime, "tcp client runtime").clone();
188 let addr_str = format!("{}:{}", config.target_host, config.target_port);
189 let addr = addr_str
190 .to_socket_addrs()?
191 .next()
192 .ok_or_else(|| io::Error::new(io::ErrorKind::AddrNotAvailable, "no addresses resolved"))?;
193
194 #[cfg(target_os = "linux")]
195 let stream = if let Some(ref device) = config.device {
196 connect_with_device(&addr, device, runtime.connect_timeout)?
197 } else {
198 TcpStream::connect_timeout(&addr, runtime.connect_timeout)?
199 };
200 #[cfg(not(target_os = "linux"))]
201 let stream = TcpStream::connect_timeout(&addr, runtime.connect_timeout)?;
202 set_socket_options(&stream)?;
203 Ok(stream)
204}
205
206#[cfg(target_os = "linux")]
208fn connect_with_device(
209 addr: &std::net::SocketAddr,
210 device: &str,
211 timeout: Duration,
212) -> io::Result<TcpStream> {
213 use std::os::unix::io::{FromRawFd, RawFd};
214
215 let domain = if addr.is_ipv4() {
216 libc::AF_INET
217 } else {
218 libc::AF_INET6
219 };
220 let fd: RawFd = unsafe { libc::socket(domain, libc::SOCK_STREAM, 0) };
221 if fd < 0 {
222 return Err(io::Error::last_os_error());
223 }
224
225 let stream = unsafe { TcpStream::from_raw_fd(fd) };
227
228 super::bind_to_device(stream.as_raw_fd(), device)?;
229
230 stream.set_nonblocking(true)?;
232
233 let (sockaddr, socklen) = socket_addr_to_raw(addr);
234 let ret = unsafe {
235 libc::connect(
236 stream.as_raw_fd(),
237 &sockaddr as *const libc::sockaddr_storage as *const libc::sockaddr,
238 socklen,
239 )
240 };
241
242 if ret != 0 {
243 let err = io::Error::last_os_error();
244 if err.raw_os_error() != Some(libc::EINPROGRESS) {
245 return Err(err);
246 }
247 }
248
249 let mut pollfd = libc::pollfd {
251 fd: stream.as_raw_fd(),
252 events: libc::POLLOUT,
253 revents: 0,
254 };
255 let timeout_ms = timeout.as_millis() as libc::c_int;
256 let poll_ret = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
257
258 if poll_ret == 0 {
259 return Err(io::Error::new(io::ErrorKind::TimedOut, "connect timed out"));
260 }
261 if poll_ret < 0 {
262 return Err(io::Error::last_os_error());
263 }
264
265 let mut err_val: libc::c_int = 0;
267 let mut err_len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
268 let ret = unsafe {
269 libc::getsockopt(
270 stream.as_raw_fd(),
271 libc::SOL_SOCKET,
272 libc::SO_ERROR,
273 &mut err_val as *mut _ as *mut libc::c_void,
274 &mut err_len,
275 )
276 };
277 if ret != 0 {
278 return Err(io::Error::last_os_error());
279 }
280 if err_val != 0 {
281 return Err(io::Error::from_raw_os_error(err_val));
282 }
283
284 stream.set_nonblocking(false)?;
286
287 Ok(stream)
288}
289
290#[cfg(target_os = "linux")]
292fn socket_addr_to_raw(addr: &std::net::SocketAddr) -> (libc::sockaddr_storage, libc::socklen_t) {
293 let mut storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
294 match addr {
295 std::net::SocketAddr::V4(v4) => {
296 let sin: &mut libc::sockaddr_in = unsafe {
297 &mut *(&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in)
298 };
299 sin.sin_family = libc::AF_INET as libc::sa_family_t;
300 sin.sin_port = v4.port().to_be();
301 sin.sin_addr = libc::in_addr {
302 s_addr: u32::from_ne_bytes(v4.ip().octets()),
303 };
304 (
305 storage,
306 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
307 )
308 }
309 std::net::SocketAddr::V6(v6) => {
310 let sin6: &mut libc::sockaddr_in6 = unsafe {
311 &mut *(&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in6)
312 };
313 sin6.sin6_family = libc::AF_INET6 as libc::sa_family_t;
314 sin6.sin6_port = v6.port().to_be();
315 sin6.sin6_addr = libc::in6_addr {
316 s6_addr: v6.ip().octets(),
317 };
318 sin6.sin6_flowinfo = v6.flowinfo();
319 sin6.sin6_scope_id = v6.scope_id();
320 (
321 storage,
322 std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
323 )
324 }
325 }
326}
327
328pub fn start(config: TcpClientConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
330 let stream = try_connect(&config)?;
331 let reader_stream = stream.try_clone()?;
332 let writer_stream = stream.try_clone()?;
333
334 let id = config.interface_id;
335 let _ = tx.send(Event::InterfaceUp(id, None, None));
337
338 let reader_config = config;
340 let reader_tx = tx;
341 thread::Builder::new()
342 .name(format!("tcp-reader-{}", id.0))
343 .spawn(move || {
344 reader_loop(reader_stream, reader_config, reader_tx);
345 })?;
346
347 Ok(Box::new(TcpWriter {
348 stream: writer_stream,
349 }))
350}
351
352fn reader_loop(mut stream: TcpStream, config: TcpClientConfig, tx: EventSender) {
355 let id = config.interface_id;
356 let mut decoder = hdlc::Decoder::new();
357 let mut buf = [0u8; 4096];
358
359 loop {
360 match stream.read(&mut buf) {
361 Ok(0) => {
362 log::warn!("[{}] connection closed", config.name);
364 let _ = tx.send(Event::InterfaceDown(id));
365 match reconnect(&config, &tx) {
366 Some(new_stream) => {
367 stream = new_stream;
368 decoder = hdlc::Decoder::new();
369 continue;
370 }
371 None => {
372 log::error!("[{}] reconnection failed, giving up", config.name);
373 return;
374 }
375 }
376 }
377 Ok(n) => {
378 for frame in decoder.feed(&buf[..n]) {
379 if tx
380 .send(Event::Frame {
381 interface_id: id,
382 data: frame,
383 rssi: None,
384 snr: None,
385 })
386 .is_err()
387 {
388 return;
390 }
391 }
392 }
393 Err(e) => {
394 log::warn!("[{}] read error: {}", config.name, e);
395 let _ = tx.send(Event::InterfaceDown(id));
396 match reconnect(&config, &tx) {
397 Some(new_stream) => {
398 stream = new_stream;
399 decoder = hdlc::Decoder::new();
400 continue;
401 }
402 None => {
403 log::error!("[{}] reconnection failed, giving up", config.name);
404 return;
405 }
406 }
407 }
408 }
409 }
410}
411
412fn reconnect(config: &TcpClientConfig, tx: &EventSender) -> Option<TcpStream> {
415 let mut attempts = 0u32;
416 loop {
417 let runtime = lock_or_recover(&config.runtime, "tcp client runtime").clone();
418 thread::sleep(runtime.reconnect_wait);
419 attempts += 1;
420
421 if let Some(max) = runtime.max_reconnect_tries {
422 if attempts > max {
423 let _ = tx.send(Event::InterfaceDown(config.interface_id));
424 return None;
425 }
426 }
427
428 log::info!("[{}] reconnect attempt {} ...", config.name, attempts);
429
430 match try_connect(config) {
431 Ok(new_stream) => {
432 let writer_stream = match new_stream.try_clone() {
434 Ok(s) => s,
435 Err(e) => {
436 log::warn!("[{}] failed to clone stream: {}", config.name, e);
437 continue;
438 }
439 };
440 log::info!("[{}] reconnected", config.name);
441 let new_writer: Box<dyn Writer> = Box::new(TcpWriter {
443 stream: writer_stream,
444 });
445 let _ = tx.send(Event::InterfaceUp(
446 config.interface_id,
447 Some(new_writer),
448 None,
449 ));
450 return Some(new_stream);
451 }
452 Err(e) => {
453 log::warn!("[{}] reconnect failed: {}", config.name, e);
454 }
455 }
456 }
457}
458
459use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
462use rns_core::transport::types::InterfaceInfo;
463use std::collections::HashMap;
464
465pub struct TcpClientFactory;
467
468impl InterfaceFactory for TcpClientFactory {
469 fn type_name(&self) -> &str {
470 "TCPClientInterface"
471 }
472
473 fn parse_config(
474 &self,
475 name: &str,
476 id: InterfaceId,
477 params: &HashMap<String, String>,
478 ) -> Result<Box<dyn InterfaceConfigData>, String> {
479 let target_host = params
480 .get("target_host")
481 .cloned()
482 .unwrap_or_else(|| "127.0.0.1".into());
483 let target_port = params
484 .get("target_port")
485 .and_then(|v| v.parse().ok())
486 .unwrap_or(4242);
487
488 Ok(Box::new(TcpClientConfig {
489 name: name.to_string(),
490 target_host,
491 target_port,
492 interface_id: id,
493 device: params.get("device").cloned(),
494 ..TcpClientConfig::default()
495 }))
496 }
497
498 fn start(
499 &self,
500 config: Box<dyn InterfaceConfigData>,
501 ctx: StartContext,
502 ) -> io::Result<StartResult> {
503 let tcp_config = *config
504 .into_any()
505 .downcast::<TcpClientConfig>()
506 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
507
508 let id = tcp_config.interface_id;
509 let name = tcp_config.name.clone();
510 let info = InterfaceInfo {
511 id,
512 name,
513 mode: ctx.mode,
514 recursive_prs: ctx.recursive_prs,
515 announces_from_internal: ctx.announces_from_internal,
516 out_capable: true,
517 in_capable: true,
518 bitrate: None,
519 airtime_profile: None,
520 announce_rate_target: None,
521 announce_rate_grace: 0,
522 announce_rate_penalty: 0.0,
523 announce_cap: rns_core::constants::ANNOUNCE_CAP,
524 is_local_client: false,
525 wants_tunnel: false,
526 tunnel_id: None,
527 mtu: 65535,
528 ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
529 ia_freq: 0.0,
530 ip_freq: 0.0,
531 op_freq: 0.0,
532 op_samples: 0,
533 started: crate::time::now(),
534 };
535
536 let writer = start(tcp_config, ctx.tx)?;
537
538 Ok(StartResult::Simple {
539 id,
540 info,
541 writer,
542 interface_type_name: "TCPClientInterface".to_string(),
543 })
544 }
545}
546
547pub(crate) fn tcp_client_runtime_handle_from_config(
548 config: &TcpClientConfig,
549) -> TcpClientRuntimeConfigHandle {
550 TcpClientRuntimeConfigHandle {
551 interface_name: config.name.clone(),
552 runtime: Arc::clone(&config.runtime),
553 startup: TcpClientRuntime::from_config(config),
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use std::net::TcpListener;
561 use std::time::Duration;
562
563 fn find_free_port() -> u16 {
564 TcpListener::bind("127.0.0.1:0")
565 .unwrap()
566 .local_addr()
567 .unwrap()
568 .port()
569 }
570
571 fn make_config(port: u16) -> TcpClientConfig {
572 TcpClientConfig {
573 name: format!("test-tcp-{}", port),
574 target_host: "127.0.0.1".into(),
575 target_port: port,
576 interface_id: InterfaceId(1),
577 reconnect_wait: Duration::from_millis(100),
578 max_reconnect_tries: Some(2),
579 connect_timeout: Duration::from_secs(2),
580 runtime: Arc::new(Mutex::new(TcpClientRuntime {
581 target_host: "127.0.0.1".into(),
582 target_port: port,
583 reconnect_wait: Duration::from_millis(100),
584 max_reconnect_tries: Some(2),
585 connect_timeout: Duration::from_secs(2),
586 })),
587 device: None,
588 }
589 }
590
591 #[test]
592 fn connect_to_listener() {
593 let port = find_free_port();
594 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
595 let (tx, rx) = crate::event::channel();
596
597 let config = make_config(port);
598 let _writer = start(config, tx).unwrap();
599
600 let _server_stream = listener.accept().unwrap();
602
603 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
605 assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
606 }
607
608 #[test]
609 fn receive_frame() {
610 let port = find_free_port();
611 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
612 let (tx, rx) = crate::event::channel();
613
614 let config = make_config(port);
615 let _writer = start(config, tx).unwrap();
616
617 let (mut server_stream, _) = listener.accept().unwrap();
618
619 let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
621
622 let payload: Vec<u8> = (0..32).collect();
624 let framed = hdlc::frame(&payload);
625 server_stream.write_all(&framed).unwrap();
626
627 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
629 match event {
630 Event::Frame {
631 interface_id,
632 data,
633 rssi: _,
634 snr: _,
635 } => {
636 assert_eq!(interface_id, InterfaceId(1));
637 assert_eq!(data, payload);
638 }
639 other => panic!("expected Frame, got {:?}", other),
640 }
641 }
642
643 #[test]
644 fn send_frame() {
645 let port = find_free_port();
646 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
647 let (tx, _rx) = crate::event::channel();
648
649 let config = make_config(port);
650 let mut writer = start(config, tx).unwrap();
651
652 let (mut server_stream, _) = listener.accept().unwrap();
653 server_stream
654 .set_read_timeout(Some(Duration::from_secs(2)))
655 .unwrap();
656
657 let payload: Vec<u8> = (0..24).collect();
659 writer.send_frame(&payload).unwrap();
660
661 let mut buf = [0u8; 256];
663 let n = server_stream.read(&mut buf).unwrap();
664 let expected = hdlc::frame(&payload);
665 assert_eq!(&buf[..n], &expected[..]);
666 }
667
668 #[test]
669 fn multiple_frames() {
670 let port = find_free_port();
671 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
672 let (tx, rx) = crate::event::channel();
673
674 let config = make_config(port);
675 let _writer = start(config, tx).unwrap();
676
677 let (mut server_stream, _) = listener.accept().unwrap();
678
679 let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
681
682 let payloads: Vec<Vec<u8>> = (0..3)
684 .map(|i| (0..24).map(|j| j + i * 50).collect())
685 .collect();
686 for p in &payloads {
687 server_stream.write_all(&hdlc::frame(p)).unwrap();
688 }
689
690 for expected in &payloads {
692 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
693 match event {
694 Event::Frame { data, .. } => assert_eq!(&data, expected),
695 other => panic!("expected Frame, got {:?}", other),
696 }
697 }
698 }
699
700 #[test]
701 fn split_across_reads() {
702 let port = find_free_port();
703 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
704 let (tx, rx) = crate::event::channel();
705
706 let config = make_config(port);
707 let _writer = start(config, tx).unwrap();
708
709 let (mut server_stream, _) = listener.accept().unwrap();
710
711 let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
713
714 let payload: Vec<u8> = (0..32).collect();
716 let framed = hdlc::frame(&payload);
717 let mid = framed.len() / 2;
718
719 server_stream.write_all(&framed[..mid]).unwrap();
720 server_stream.flush().unwrap();
721 thread::sleep(Duration::from_millis(50));
722 server_stream.write_all(&framed[mid..]).unwrap();
723 server_stream.flush().unwrap();
724
725 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
726 match event {
727 Event::Frame { data, .. } => assert_eq!(data, payload),
728 other => panic!("expected Frame, got {:?}", other),
729 }
730 }
731
732 #[test]
733 fn reconnect_on_close() {
734 let port = find_free_port();
735 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
736 listener.set_nonblocking(false).unwrap();
737 let (tx, rx) = crate::event::channel();
738
739 let config = make_config(port);
740 let _writer = start(config, tx).unwrap();
741
742 let (server_stream, _) = listener.accept().unwrap();
744
745 let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
747
748 drop(server_stream);
749
750 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
752 assert!(matches!(event, Event::InterfaceDown(InterfaceId(1))));
753
754 let _server_stream2 = listener.accept().unwrap();
756
757 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
759 assert!(matches!(event, Event::InterfaceUp(InterfaceId(1), _, _)));
760 }
761
762 #[test]
763 fn socket_options() {
764 let port = find_free_port();
765 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
766
767 let stream = try_connect(&make_config(port)).unwrap();
768 let _server = listener.accept().unwrap();
769
770 let fd = stream.as_raw_fd();
772 let mut val: libc::c_int = 0;
773 let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
774 unsafe {
775 libc::getsockopt(
776 fd,
777 libc::IPPROTO_TCP,
778 libc::TCP_NODELAY,
779 &mut val as *mut _ as *mut libc::c_void,
780 &mut len,
781 );
782 }
783 assert_eq!(val, 1, "TCP_NODELAY should be 1");
784 }
785
786 #[test]
787 fn connect_timeout() {
788 let config = TcpClientConfig {
790 name: "timeout-test".into(),
791 target_host: "192.0.2.1".into(), target_port: 12345,
793 interface_id: InterfaceId(99),
794 reconnect_wait: Duration::from_millis(100),
795 max_reconnect_tries: Some(0),
796 connect_timeout: Duration::from_millis(500),
797 device: None,
798 runtime: Arc::new(Mutex::new(TcpClientRuntime {
799 target_host: "192.0.2.1".into(),
800 target_port: 12345,
801 reconnect_wait: Duration::from_millis(100),
802 max_reconnect_tries: Some(0),
803 connect_timeout: Duration::from_millis(500),
804 })),
805 ..TcpClientConfig::default()
806 };
807
808 let start_time = std::time::Instant::now();
809 let result = try_connect(&config);
810 let elapsed = start_time.elapsed();
811
812 assert!(result.is_err());
813 assert!(elapsed < Duration::from_secs(5));
815 }
816}