Skip to main content

pingora_core/connectors/
mod.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Connecting to servers
16
17pub mod http;
18pub mod l4;
19#[cfg(feature = "any_tls")]
20mod tls;
21
22#[cfg(not(feature = "any_tls"))]
23use crate::tls::connectors as tls;
24
25use crate::protocols::l4::ext::attach_connect_local_addr;
26use crate::protocols::{GetSocketDigest, Stream};
27use crate::server::configuration::ServerConf;
28use crate::upstreams::peer::{Peer, ALPN};
29
30use crate::offload::OffloadRuntime;
31pub use l4::Connect as L4Connect;
32use l4::{connect as l4_connect, BindTo};
33use log::{debug, error, warn};
34use parking_lot::RwLock;
35use pingora_error::{Error, ErrorType::*, OrErr, Result};
36use pingora_pool::{ConnectionMeta, ConnectionPool};
37use std::collections::HashMap;
38use std::net::SocketAddr;
39use std::sync::atomic::{AtomicU64, Ordering};
40use std::sync::Arc;
41use std::time::{Duration, Instant};
42use tls::TlsConnector;
43use tokio::sync::Mutex;
44
45#[derive(Clone, Debug)]
46pub(crate) struct IdleConnection {
47    connection: ConnectionMeta,
48    idle_since: Instant,
49}
50
51impl IdleConnection {
52    pub(crate) fn new(connection: ConnectionMeta) -> Self {
53        Self {
54            connection,
55            idle_since: Instant::now(),
56        }
57    }
58
59    fn elapsed(&self) -> Duration {
60        self.idle_since.elapsed()
61    }
62}
63
64/// Callback invoked when an idle upstream connection leaves the keep-alive pool
65/// without being reused.
66pub type PoolCallback = Arc<dyn Fn(Duration) + Send + Sync>;
67
68/// The options to configure a [TransportConnector]
69#[derive(Clone)]
70pub struct ConnectorOptions {
71    /// Path to the CA file used to validate server certs.
72    ///
73    /// If `None`, the CA in the [default](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_default_verify_paths.html)
74    /// locations will be loaded
75    pub ca_file: Option<String>,
76    /// The maximum number of unique s2n configs to cache. Creating a new s2n config is an
77    /// expensive operation, so we cache and re-use config objects with identical configurations.
78    /// Defaults to a cache size of 10. A value of 0 disables the cache.
79    ///
80    /// WARNING: Disabling the s2n config cache can result in poor performance
81    #[cfg(feature = "s2n")]
82    pub s2n_config_cache_size: Option<usize>,
83    /// The default client cert and key to use for mTLS
84    ///
85    /// Each individual connection can use their own cert key to override this.
86    pub cert_key_file: Option<(String, String)>,
87    /// When enabled allows TLS keys to be written to a file specified by the SSLKEYLOG
88    /// env variable. This can be used by tools like Wireshark to decrypt traffic
89    /// for debugging purposes.
90    pub debug_ssl_keylog: bool,
91    /// Effective global cap for the keepalive pool. Derived from
92    /// `server_conf.upstream_keepalive_pool_size * server_conf.threads`
93    /// in [`Self::from_server_conf`] so that operator-facing config keeps
94    /// its per-worker meaning even though the pool itself now uses a single
95    /// global LRU.
96    pub keepalive_pool_size: usize,
97    /// Optionally offload the connection establishment to dedicated thread pools
98    ///
99    /// TCP and TLS connection establishment can be CPU intensive. Sometimes such tasks can slow
100    /// down the entire service, which causes timeouts which leads to more connections which
101    /// snowballs the issue. Use this option to isolate these CPU intensive tasks from impacting
102    /// other traffic.
103    ///
104    /// Syntax: (#pools, #thread in each pool)
105    pub offload_threadpool: Option<(usize, usize)>,
106    /// Bind to any of the given source IPv6 addresses
107    pub bind_to_v4: Vec<SocketAddr>,
108    /// Bind to any of the given source IPv4 addresses
109    pub bind_to_v6: Vec<SocketAddr>,
110    /// Optional callback for observing how long upstream connections stayed idle
111    /// before leaving the keep-alive pool without reuse.
112    pub keepalive_pool_callback: Option<PoolCallback>,
113}
114
115impl ConnectorOptions {
116    /// Derive the [ConnectorOptions] from a [ServerConf]
117    pub fn from_server_conf(server_conf: &ServerConf) -> Self {
118        // create SocketAddrs with port 0 for src addr bind
119
120        let bind_to_v4 = server_conf
121            .client_bind_to_ipv4
122            .iter()
123            .map(|v4| {
124                let ip = v4.parse().unwrap();
125                SocketAddr::new(ip, 0)
126            })
127            .collect();
128
129        let bind_to_v6 = server_conf
130            .client_bind_to_ipv6
131            .iter()
132            .map(|v6| {
133                let ip = v6.parse().unwrap();
134                SocketAddr::new(ip, 0)
135            })
136            .collect();
137        ConnectorOptions {
138            ca_file: server_conf.ca_file.clone(),
139            cert_key_file: None, // TODO: use it
140            #[cfg(feature = "s2n")]
141            s2n_config_cache_size: server_conf.s2n_config_cache_size,
142            debug_ssl_keylog: server_conf.upstream_debug_ssl_keylog,
143            keepalive_pool_size: server_conf
144                .upstream_keepalive_pool_size
145                .saturating_mul(server_conf.threads.max(1)),
146            offload_threadpool: server_conf.upstream_connect_offload_threadpool(),
147            bind_to_v4,
148            bind_to_v6,
149            keepalive_pool_callback: None,
150        }
151    }
152
153    /// Create a new [ConnectorOptions] with the given keepalive pool size
154    pub fn new(keepalive_pool_size: usize) -> Self {
155        ConnectorOptions {
156            ca_file: None,
157            #[cfg(feature = "s2n")]
158            s2n_config_cache_size: None,
159            cert_key_file: None,
160            debug_ssl_keylog: false,
161            keepalive_pool_size,
162            offload_threadpool: None,
163            bind_to_v4: vec![],
164            bind_to_v6: vec![],
165            keepalive_pool_callback: None,
166        }
167    }
168}
169
170/// [TransportConnector] provides APIs to connect to servers via TCP or TLS with connection reuse
171pub struct TransportConnector {
172    tls_ctx: tls::Connector,
173    connection_pool: Arc<ConnectionPool<Arc<Mutex<Stream>>>>,
174    offload: Option<OffloadRuntime>,
175    bind_to_v4: Vec<SocketAddr>,
176    bind_to_v6: Vec<SocketAddr>,
177    preferred_http_version: PreferredHttpVersion,
178    /// Wrapped in `Arc` so external consumers (e.g. proxy services) can clone a reference
179    /// for periodic metric reporting without needing access to the connector itself.
180    unexpected_data_conn_count: Arc<AtomicU64>,
181    keepalive_pool_callback: Option<PoolCallback>,
182}
183
184const DEFAULT_POOL_SIZE: usize = 128;
185
186impl TransportConnector {
187    /// Create a new [TransportConnector] with the given [ConnectorOptions]
188    pub fn new(mut options: Option<ConnectorOptions>) -> Self {
189        let pool_size = options
190            .as_ref()
191            .map_or(DEFAULT_POOL_SIZE, |c| c.keepalive_pool_size);
192        // Take the offloading setting there because this layer has implement offloading,
193        // so no need for stacks at lower layer to offload again.
194        let offload = options.as_mut().and_then(|o| o.offload_threadpool.take());
195        let bind_to_v4 = options
196            .as_ref()
197            .map_or_else(Vec::new, |o| o.bind_to_v4.clone());
198        let bind_to_v6 = options
199            .as_ref()
200            .map_or_else(Vec::new, |o| o.bind_to_v6.clone());
201        let keepalive_pool_callback = options
202            .as_ref()
203            .and_then(|o| o.keepalive_pool_callback.clone());
204        TransportConnector {
205            tls_ctx: tls::Connector::new(options),
206            connection_pool: Arc::new(ConnectionPool::new(pool_size)),
207            offload: offload.map(|v| OffloadRuntime::new("upstream connect offload", v.0, v.1)),
208            bind_to_v4,
209            bind_to_v6,
210            preferred_http_version: PreferredHttpVersion::new(),
211            unexpected_data_conn_count: Arc::new(AtomicU64::new(0)),
212            keepalive_pool_callback,
213        }
214    }
215
216    /// Connect to the given server [Peer]
217    ///
218    /// No connection is reused.
219    pub async fn new_stream<P: Peer + Send + Sync + 'static>(&self, peer: &P) -> Result<Stream> {
220        let rt = self
221            .offload
222            .as_ref()
223            .map(|o| o.get_runtime(peer.reuse_hash()));
224        let bind_to = l4::bind_to_random(peer, &self.bind_to_v4, &self.bind_to_v6);
225        let alpn_override = self.preferred_http_version.get(peer);
226        let stream = if let Some(rt) = rt {
227            let peer = peer.clone();
228            let tls_ctx = self.tls_ctx.clone();
229            let offload_start = Instant::now();
230            rt.spawn(async move {
231                let offload_wait = offload_start.elapsed();
232                do_connect(
233                    &peer,
234                    bind_to,
235                    alpn_override,
236                    &tls_ctx.ctx,
237                    Some(offload_wait),
238                )
239                .await
240            })
241            .await
242            .or_err(InternalError, "offload runtime failure")??
243        } else {
244            do_connect(peer, bind_to, alpn_override, &self.tls_ctx.ctx, None).await?
245        };
246
247        Ok(stream)
248    }
249
250    /// Try to find a reusable connection to the given server [Peer]
251    pub async fn reused_stream<P: Peer + Send + Sync>(&self, peer: &P) -> Option<Stream> {
252        match self.connection_pool.get(&peer.reuse_hash()) {
253            Some(s) => {
254                debug!("find reusable stream, trying to acquire it");
255                {
256                    let _ = s.lock().await;
257                } // wait for the idle poll to release it
258                match Arc::try_unwrap(s) {
259                    Ok(l) => {
260                        let mut stream = l.into_inner();
261                        // test_reusable_stream: we assume server would never actively send data
262                        // first on an idle stream.
263                        #[cfg(unix)]
264                        if peer.matches_fd(stream.id())
265                            && test_reusable_stream(&mut stream, &self.unexpected_data_conn_count)
266                        {
267                            Some(stream)
268                        } else {
269                            None
270                        }
271                        #[cfg(windows)]
272                        {
273                            use std::os::windows::io::{AsRawSocket, RawSocket};
274                            struct WrappedRawSocket(RawSocket);
275                            impl AsRawSocket for WrappedRawSocket {
276                                fn as_raw_socket(&self) -> RawSocket {
277                                    self.0
278                                }
279                            }
280                            if peer.matches_sock(WrappedRawSocket(stream.id() as RawSocket))
281                                && test_reusable_stream(
282                                    &mut stream,
283                                    &self.unexpected_data_conn_count,
284                                )
285                            {
286                                Some(stream)
287                            } else {
288                                None
289                            }
290                        }
291                    }
292                    Err(_) => {
293                        error!("failed to acquire reusable stream");
294                        None
295                    }
296                }
297            }
298            None => {
299                debug!("No reusable connection found for {peer}");
300                None
301            }
302        }
303    }
304
305    /// Return the [Stream] to the [TransportConnector] for connection reuse.
306    ///
307    /// Not all TCP/TLS connections can be reused. It is the caller's responsibility to make sure
308    /// that protocol over the [Stream] supports connection reuse and the [Stream] itself is ready
309    /// to be reused.
310    ///
311    /// If a [Stream] is dropped instead of being returned via this function. it will be closed.
312    pub fn release_stream(
313        &self,
314        mut stream: Stream,
315        key: u64, // usually peer.reuse_hash()
316        idle_timeout: Option<Duration>,
317    ) {
318        if !test_reusable_stream(&mut stream, &self.unexpected_data_conn_count) {
319            return;
320        }
321        let id = stream.id();
322        let meta = ConnectionMeta::new(key, id);
323        debug!("Try to keepalive client session");
324        let stream = Arc::new(Mutex::new(stream));
325        let locked_stream = stream.clone().try_lock_owned().unwrap(); // safe as we just created it
326        let (notify_close, watch_use) = self.connection_pool.put(&meta, stream);
327        let idle_meta = IdleConnection::new(meta);
328        let pool = self.connection_pool.clone(); //clone the arc
329        let keepalive_pool_callback = self.keepalive_pool_callback.clone();
330        let rt = pingora_runtime::current_handle();
331        rt.spawn(async move {
332            if pool
333                .idle_poll(
334                    locked_stream,
335                    &idle_meta.connection,
336                    idle_timeout,
337                    notify_close,
338                    watch_use,
339                )
340                .await
341            {
342                if let Some(callback) = keepalive_pool_callback {
343                    callback(idle_meta.elapsed());
344                }
345            }
346        });
347    }
348
349    /// Get a stream to the given server [Peer]
350    ///
351    /// This function will try to find a reusable [Stream] first. If there is none, a new connection
352    /// will be made to the server.
353    ///
354    /// The returned boolean will indicate whether the stream is reused.
355    pub async fn get_stream<P: Peer + Send + Sync + 'static>(
356        &self,
357        peer: &P,
358    ) -> Result<(Stream, bool)> {
359        let reused_stream = self.reused_stream(peer).await;
360        if let Some(s) = reused_stream {
361            Ok((s, true))
362        } else {
363            let s = self.new_stream(peer).await?;
364            Ok((s, false))
365        }
366    }
367
368    /// Tell the connector to always send h1 for ALPN for the given peer in the future.
369    pub fn prefer_h1(&self, peer: &impl Peer) {
370        self.preferred_http_version.add(peer, 1);
371    }
372
373    /// Return the number of times a pooled connection was found to contain unexpected data
374    /// from the server.
375    pub fn unexpected_data_connection_count(&self) -> u64 {
376        self.unexpected_data_conn_count.load(Ordering::Relaxed)
377    }
378
379    /// Return a shared reference to the unexpected data connection counter.
380    ///
381    /// This allows external consumers (e.g. proxy services) to clone the `Arc` and
382    /// periodically read the counter for metric reporting without needing ongoing
383    /// access to the connector.
384    pub fn unexpected_data_connection_counter(&self) -> Arc<AtomicU64> {
385        self.unexpected_data_conn_count.clone()
386    }
387}
388
389// Perform the actual L4 and tls connection steps while respecting the peer's
390// connection timeout if there is one
391async fn do_connect<P: Peer + Send + Sync>(
392    peer: &P,
393    bind_to: Option<BindTo>,
394    alpn_override: Option<ALPN>,
395    tls_ctx: &TlsConnector,
396    offload_wait_duration: Option<Duration>,
397) -> Result<Stream> {
398    // Create the future that does the connections, but don't evaluate it until
399    // we decide if we need a timeout or not
400    let connect_future =
401        do_connect_inner(peer, bind_to, alpn_override, tls_ctx, offload_wait_duration);
402
403    match peer.total_connection_timeout() {
404        Some(t) => match pingora_timeout::timeout(t, connect_future).await {
405            Ok(res) => res,
406            Err(_) => Error::e_explain(
407                ConnectTimedout,
408                format!("connecting to server {peer}, total-connection timeout {t:?}"),
409            ),
410        },
411        None => connect_future.await,
412    }
413}
414
415// Perform the actual L4 and tls connection steps with no timeout
416async fn do_connect_inner<P: Peer + Send + Sync>(
417    peer: &P,
418    bind_to: Option<BindTo>,
419    alpn_override: Option<ALPN>,
420    tls_ctx: &TlsConnector,
421    offload_wait_duration: Option<Duration>,
422) -> Result<Stream> {
423    let l4_connect_start = Instant::now();
424    let mut stream = l4_connect(peer, bind_to).await?;
425    stream.set_establishment_timing(l4_connect_start.elapsed(), offload_wait_duration);
426    if peer.tls() {
427        // Capture the local address before the handshake consumes the stream, so a TLS failure
428        // can still report the address we connected from.
429        // Arc clone plus a copy of the small SocketAddr, negligible next to the handshake.
430        let local_addr = stream
431            .get_socket_digest()
432            .and_then(|digest| digest.local_addr().and_then(|addr| addr.as_inet()).copied());
433        let tls_stream = tls::connect(stream, peer, alpn_override, tls_ctx)
434            .await
435            .map_err(|e| attach_connect_local_addr(e, local_addr))?;
436        Ok(Box::new(tls_stream))
437    } else {
438        Ok(Box::new(stream))
439    }
440}
441
442struct PreferredHttpVersion {
443    // TODO: shard to avoid the global lock
444    versions: RwLock<HashMap<u64, u8>>, // <hash of peer, version>
445}
446
447// TODO: limit the size of this
448
449impl PreferredHttpVersion {
450    pub fn new() -> Self {
451        PreferredHttpVersion {
452            versions: RwLock::default(),
453        }
454    }
455
456    pub fn add(&self, peer: &impl Peer, version: u8) {
457        let key = peer.reuse_hash();
458        let mut v = self.versions.write();
459        v.insert(key, version);
460    }
461
462    pub fn get(&self, peer: &impl Peer) -> Option<ALPN> {
463        let key = peer.reuse_hash();
464        let v = self.versions.read();
465        v.get(&key)
466            .copied()
467            .map(|v| if v == 1 { ALPN::H1 } else { ALPN::H2H1 })
468    }
469}
470
471use futures::future::FutureExt;
472use tokio::io::AsyncReadExt;
473
474/// Test whether a stream is already closed or not reusable (server sent unexpected data)
475fn test_reusable_stream(stream: &mut Stream, unexpected_data_conn_count: &AtomicU64) -> bool {
476    let mut buf = [0; 1];
477    // tokio::task::unconstrained because now_or_never may yield None when the future is ready
478    let result = tokio::task::unconstrained(stream.read(&mut buf[..])).now_or_never();
479    if let Some(data_result) = result {
480        match data_result {
481            Ok(n) => {
482                if n == 0 {
483                    debug!("Idle connection is closed");
484                } else {
485                    warn!("Unexpected data read in idle connection");
486                    unexpected_data_conn_count.fetch_add(1, Ordering::Relaxed);
487                }
488            }
489            Err(e) => {
490                debug!("Idle connection is broken: {e:?}");
491            }
492        }
493        false
494    } else {
495        true
496    }
497}
498
499/// Test utilities for creating mock acceptors.
500#[cfg(all(test, unix))]
501pub(crate) mod test_utils {
502    use tokio::io::AsyncWriteExt;
503    use tokio::net::UnixListener;
504
505    /// Generates a unique socket path for testing to avoid conflicts when running in parallel
506    pub fn unique_uds_path(test_name: &str) -> String {
507        format!(
508            "/tmp/test_{test_name}_{:?}_{}.sock",
509            std::thread::current().id(),
510            std::process::id()
511        )
512    }
513
514    /// A mock UDS server that accepts one connection, sends data, and waits for shutdown signal
515    ///
516    /// Returns: (ready_rx, shutdown_tx, server_handle)
517    /// - ready_rx: Wait on this to know when server is ready to accept connections
518    /// - shutdown_tx: Send on this to tell server to shut down
519    /// - server_handle: Join handle for the server task
520    pub fn spawn_mock_uds_server(
521        socket_path: String,
522        response: &'static [u8],
523    ) -> (
524        tokio::sync::oneshot::Receiver<()>,
525        tokio::sync::oneshot::Sender<()>,
526        tokio::task::JoinHandle<()>,
527    ) {
528        let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
529        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
530
531        let server_handle = tokio::spawn(async move {
532            let _ = std::fs::remove_file(&socket_path);
533            let listener = UnixListener::bind(&socket_path).unwrap();
534            // Signal that the server is ready to accept connections
535            let _ = ready_tx.send(());
536
537            if let Ok((mut stream, _addr)) = listener.accept().await {
538                let _ = stream.write_all(response).await;
539                // Keep the connection open until the test tells us to shutdown
540                let _ = shutdown_rx.await;
541            }
542            let _ = std::fs::remove_file(&socket_path);
543        });
544
545        (ready_rx, shutdown_tx, server_handle)
546    }
547
548    /// A mock UDS server that immediately closes connections (for testing error handling)
549    ///
550    /// Returns: (ready_rx, shutdown_tx, server_handle)
551    pub fn spawn_mock_uds_server_close_immediate(
552        socket_path: String,
553    ) -> (
554        tokio::sync::oneshot::Receiver<()>,
555        tokio::sync::oneshot::Sender<()>,
556        tokio::task::JoinHandle<()>,
557    ) {
558        let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
559        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
560
561        let server_handle = tokio::spawn(async move {
562            let _ = std::fs::remove_file(&socket_path);
563            let listener = UnixListener::bind(&socket_path).unwrap();
564            // Signal that the server is ready to accept connections
565            let _ = ready_tx.send(());
566
567            if let Ok((mut stream, _addr)) = listener.accept().await {
568                let _ = stream.shutdown().await;
569                // Wait for shutdown signal before cleaning up
570                let _ = shutdown_rx.await;
571            }
572            let _ = std::fs::remove_file(&socket_path);
573        });
574
575        (ready_rx, shutdown_tx, server_handle)
576    }
577}
578
579#[cfg(test)]
580#[cfg(feature = "any_tls")]
581mod tests {
582    use std::time::Duration;
583
584    use pingora_error::ErrorType;
585    use tls::Connector;
586
587    use super::*;
588    use crate::upstreams::peer::BasicPeer;
589
590    #[tokio::test]
591    async fn test_connect() {
592        let connector = TransportConnector::new(None);
593        let peer = BasicPeer::new("1.1.1.1:80");
594        // make a new connection to 1.1.1.1
595        let stream = connector.new_stream(&peer).await.unwrap();
596        let timing = stream.get_timing_digest();
597        assert_eq!(timing.len(), 1);
598        let l4_timing = timing[0].as_ref().unwrap();
599        assert!(l4_timing.establishment_duration.is_some());
600        assert!(l4_timing.offload_wait_duration.is_none());
601        connector.release_stream(stream, peer.reuse_hash(), None);
602
603        let (_, reused) = connector.get_stream(&peer).await.unwrap();
604        assert!(reused);
605    }
606
607    #[tokio::test]
608    async fn test_connect_with_offload_timing() {
609        let mut conf = ConnectorOptions::new(1);
610        conf.offload_threadpool = Some((1, 1));
611        let connector = TransportConnector::new(Some(conf));
612        let peer = BasicPeer::new("1.1.1.1:80");
613
614        let stream = connector.new_stream(&peer).await.unwrap();
615        let timing = stream.get_timing_digest();
616        let l4_timing = timing[0].as_ref().unwrap();
617        assert!(l4_timing.establishment_duration.is_some());
618        assert!(l4_timing.offload_wait_duration.is_some());
619    }
620
621    #[tokio::test]
622    async fn test_connect_tls() {
623        let connector = TransportConnector::new(None);
624        let mut peer = BasicPeer::new("1.1.1.1:443");
625        // BasicPeer will use tls when SNI is set
626        peer.sni = "one.one.one.one".to_string();
627        // make a new connection to https://1.1.1.1
628        let stream = connector.new_stream(&peer).await.unwrap();
629        let timing = stream.get_timing_digest();
630        assert_eq!(timing.len(), 2);
631        let l4_timing = timing[0].as_ref().unwrap();
632        assert!(l4_timing.establishment_duration.is_some());
633        assert!(l4_timing.offload_wait_duration.is_none());
634        let tls_timing = timing[1].as_ref().unwrap();
635        assert!(tls_timing.establishment_duration.is_some());
636        assert!(tls_timing.offload_wait_duration.is_none());
637        connector.release_stream(stream, peer.reuse_hash(), None);
638
639        let (_, reused) = connector.get_stream(&peer).await.unwrap();
640        assert!(reused);
641    }
642
643    #[tokio::test(flavor = "multi_thread")]
644    #[cfg(unix)]
645    async fn test_connect_uds() {
646        let socket_path = test_utils::unique_uds_path("transport_connector");
647        let (ready_rx, shutdown_tx, server_handle) =
648            test_utils::spawn_mock_uds_server(socket_path.clone(), b"it works!");
649
650        // Wait for the server to be ready before connecting
651        ready_rx.await.unwrap();
652
653        // create a new service at /tmp
654        let connector = TransportConnector::new(None);
655        let peer = BasicPeer::new_uds(&socket_path).unwrap();
656        // make a new connection to mock uds
657        let mut stream = connector.new_stream(&peer).await.unwrap();
658        let mut buf = [0; 9];
659        stream.read_exact(&mut buf).await.unwrap();
660        assert_eq!(&buf, b"it works!");
661
662        // Test connection reuse by releasing and getting the stream back
663        connector.release_stream(stream, peer.reuse_hash(), None);
664        let (stream, reused) = connector.get_stream(&peer).await.unwrap();
665        assert!(reused);
666
667        // Clean up: drop the stream, tell server to shutdown, and wait for it
668        drop(stream);
669        let _ = shutdown_tx.send(());
670        server_handle.await.unwrap();
671    }
672
673    // 192.0.2.1 is TEST-NET-1 (RFC 5737) — SYN packets are silently
674    // dropped on Linux, producing ConnectTimedout. On macOS the kernel
675    // may instead return ENETUNREACH (ConnectNoRoute).
676    const BLACKHOLE: &str = "192.0.2.1:79";
677
678    async fn do_test_conn_timeout(conf: Option<ConnectorOptions>) {
679        let connector = TransportConnector::new(conf);
680        let mut peer = BasicPeer::new(BLACKHOLE);
681        peer.options.connection_timeout = Some(Duration::from_millis(1));
682        let Err(e) = connector.new_stream(&peer).await else {
683            panic!("should throw an error");
684        };
685        assert!(
686            e.etype() == &ConnectTimedout || e.etype() == &ConnectNoRoute,
687            "unexpected error type: {:?}",
688            e.etype()
689        );
690    }
691
692    #[tokio::test]
693    async fn test_conn_timeout() {
694        do_test_conn_timeout(None).await;
695    }
696
697    #[tokio::test]
698    async fn test_conn_timeout_with_offload() {
699        let mut conf = ConnectorOptions::new(8);
700        conf.offload_threadpool = Some((2, 2));
701        do_test_conn_timeout(Some(conf)).await;
702    }
703
704    #[tokio::test]
705    async fn test_connector_bind_to() {
706        // connect to remote while bind to localhost will fail
707        let peer = BasicPeer::new("240.0.0.1:80");
708        let mut conf = ConnectorOptions::new(1);
709        conf.bind_to_v4.push("127.0.0.1:0".parse().unwrap());
710        let connector = TransportConnector::new(Some(conf));
711
712        let stream = connector.new_stream(&peer).await;
713        let error = stream.unwrap_err();
714        // The exact error varies by platform: Linux may return ConnectError,
715        // some systems time out (ConnectTimedout), and macOS/others may
716        // return ConnectNoRoute (ENETUNREACH) for unreachable addresses.
717        assert!(
718            error.etype() == &ConnectError
719                || error.etype() == &ConnectTimedout
720                || error.etype() == &ConnectNoRoute,
721            "unexpected error type: {:?}",
722            error.etype()
723        )
724    }
725
726    /// Helper function for testing error handling in the `do_connect` function.
727    /// This assumes that the connection will fail on the peer and returns
728    /// the decomposed error type and message.
729    async fn get_do_connect_failure_with_peer(peer: &BasicPeer) -> (ErrorType, String) {
730        let tls_connector = Connector::new(None);
731        let stream = do_connect(peer, None, None, &tls_connector.ctx, None).await;
732        match stream {
733            Ok(_) => panic!("should throw an error"),
734            Err(e) => (
735                e.etype().clone(),
736                e.context
737                    .as_ref()
738                    .map(|ctx| ctx.as_str().to_owned())
739                    .unwrap_or_default(),
740            ),
741        }
742    }
743
744    #[tokio::test]
745    async fn test_do_connect_with_total_timeout() {
746        let mut peer = BasicPeer::new(BLACKHOLE);
747        peer.options.total_connection_timeout = Some(Duration::from_millis(1));
748        let (etype, context) = get_do_connect_failure_with_peer(&peer).await;
749        assert!(
750            etype == ConnectTimedout || etype == ConnectNoRoute,
751            "unexpected error type: {etype:?}"
752        );
753        if etype == ConnectTimedout {
754            assert!(context.contains("total-connection timeout"));
755        }
756    }
757
758    #[tokio::test]
759    async fn test_tls_connect_timeout_supersedes_total() {
760        let mut peer = BasicPeer::new(BLACKHOLE);
761        peer.options.total_connection_timeout = Some(Duration::from_millis(10));
762        peer.options.connection_timeout = Some(Duration::from_millis(1));
763        let (etype, context) = get_do_connect_failure_with_peer(&peer).await;
764        assert!(
765            etype == ConnectTimedout || etype == ConnectNoRoute,
766            "unexpected error type: {etype:?}"
767        );
768        if etype == ConnectTimedout {
769            assert!(!context.contains("total-connection timeout"));
770        }
771    }
772
773    #[tokio::test]
774    async fn test_do_connect_without_total_timeout() {
775        let peer = BasicPeer::new(BLACKHOLE);
776        let (etype, context) = get_do_connect_failure_with_peer(&peer).await;
777        assert!(etype != ConnectTimedout || !context.contains("total-connection timeout"));
778    }
779
780    #[tokio::test]
781    async fn test_tls_handshake_failure_records_local_addr() {
782        use crate::connectors::l4::ConnectErrorExt;
783
784        // A plaintext listener that closes each connection immediately, so the TLS handshake fails
785        // after the TCP connection is established.
786        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
787        let server_addr = listener.local_addr().unwrap();
788        tokio::spawn(async move {
789            while let Ok((stream, _)) = listener.accept().await {
790                drop(stream);
791            }
792        });
793
794        // Non-empty SNI makes BasicPeer request TLS against the plaintext listener.
795        let mut peer = BasicPeer::new(&server_addr.to_string());
796        peer.sni = "openrusty.org".to_string();
797        // Safety net so the test fails fast rather than hanging if a platform does not surface the
798        // handshake failure immediately; a handshake timeout still exercises the same code path.
799        peer.options.connection_timeout = Some(Duration::from_secs(5));
800
801        let tls_connector = Connector::new(None);
802        let err = do_connect(&peer, None, None, &tls_connector.ctx, None)
803            .await
804            .unwrap_err();
805
806        let local_addr = err
807            .connect_local_addr()
808            .expect("TLS handshake failure should retain the assigned local address");
809        assert_ne!(local_addr.port(), 0);
810    }
811
812    #[tokio::test]
813    async fn test_unexpected_data_connection_count_increments() {
814        // Create a duplex stream where we control both ends
815        let (mut server, client) = tokio::io::duplex(64);
816
817        let counter = AtomicU64::new(0);
818        let mut stream: Stream = Box::new(client);
819
820        // With no data available, the stream should be considered reusable
821        assert!(test_reusable_stream(&mut stream, &counter));
822        assert_eq!(counter.load(Ordering::Relaxed), 0);
823
824        // Write unexpected data from the server side
825        use tokio::io::AsyncWriteExt;
826        server.write_all(b"unexpected").await.unwrap();
827
828        // Give the data a moment to be buffered
829        tokio::task::yield_now().await;
830
831        // Now test_reusable_stream should detect the unexpected data
832        assert!(!test_reusable_stream(&mut stream, &counter));
833        assert_eq!(
834            counter.load(Ordering::Relaxed),
835            1,
836            "unexpected_data_connection_count should have incremented"
837        );
838    }
839}