Skip to main content

eggress_core/
chain.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use eggress_uri::{EndpointSpec, ProtocolSpec, ProxyHopSpec};
5
6use crate::connector::{ConnectOptions, DirectConnector};
7use crate::{BoxStream, ConnectError, TargetAddr, TargetHost};
8
9/// A boxed future that resolves to a handshake result.
10type HandshakeFuture<'a> = Pin<
11    Box<
12        dyn Future<Output = Result<BoxStream, Box<dyn std::error::Error + Send + Sync>>>
13            + Send
14            + 'a,
15    >,
16>;
17
18/// Errors that can occur during chain execution.
19#[derive(Debug, thiserror::Error)]
20pub enum ChainError {
21    #[error("hop {hop_index}: connection to {endpoint} failed: {source}")]
22    ConnectFailed {
23        hop_index: usize,
24        endpoint: String,
25        source: ConnectError,
26    },
27
28    #[error("hop {hop_index}: {protocol} handshake failed: {source}")]
29    HandshakeFailed {
30        hop_index: usize,
31        protocol: String,
32        source: Box<dyn std::error::Error + Send + Sync>,
33    },
34
35    #[error("chain is empty, at least one hop is required")]
36    EmptyChain,
37
38    #[error("invalid chain: {reason}")]
39    InvalidChain { reason: String },
40}
41
42/// Error type for protocol handshake operations.
43#[derive(Debug, thiserror::Error)]
44pub enum HandshakeError {
45    #[error("IO error: {0}")]
46    Io(#[from] std::io::Error),
47
48    #[error("protocol error: {0}")]
49    Protocol(String),
50
51    #[error("connection refused")]
52    ConnectionRefused,
53
54    #[error("authentication failed")]
55    AuthFailed,
56
57    #[error("{0}")]
58    Other(String),
59}
60
61/// Trait for performing protocol-specific handshakes through proxy hops.
62///
63/// Each protocol (HTTP CONNECT, SOCKS4, SOCKS5) provides an implementation
64/// of this trait to handle its specific handshake logic.
65///
66/// # Dyn Compatibility
67///
68/// This trait is dyn-compatible and can be used with `Box<dyn HopHandler>`.
69pub trait HopHandler: Send + Sync {
70    /// Returns the protocol this handler supports.
71    fn protocol(&self) -> ProtocolSpec;
72
73    /// Establish the transport for a hop whose protocol is not carried over
74    /// a TCP socket (for example QUIC). TCP-backed handlers use the default.
75    fn open<'a>(
76        &'a self,
77        _endpoint: &'a EndpointSpec,
78        _hop: &'a ProxyHopSpec,
79        _target: &'a TargetAddr,
80    ) -> Option<HandshakeFuture<'a>> {
81        None
82    }
83
84    /// Perform the protocol handshake over the given stream.
85    ///
86    /// The handler should:
87    /// 1. Perform the protocol-specific handshake (e.g., HTTP CONNECT, SOCKS5 greeting)
88    /// 2. Request connection to the specified target
89    /// 3. Return the upgraded stream on success
90    ///
91    /// `hop_index` is the 0-based position of this hop in the chain. Handlers
92    /// that use connection pooling (e.g., H2) must include this value in pool
93    /// keys to prevent cross-chain connection reuse.
94    fn handshake<'a>(
95        &'a self,
96        stream: BoxStream,
97        target: &'a TargetAddr,
98        hop: &'a ProxyHopSpec,
99        hop_index: usize,
100    ) -> HandshakeFuture<'a>;
101}
102
103/// A function that wraps a `BoxStream` in TLS for upstream connections.
104///
105/// Returns the TLS-wrapped stream, or an error if the handshake fails.
106/// The optional `alpn` parameter sets Application-Layer Protocol Negotiation
107/// protocols (e.g. `["h2", "http/1.1"]` for H2 connections).
108pub type TlsWrapper = Box<
109    dyn Fn(
110            BoxStream,
111            String,
112            Option<Vec<Vec<u8>>>,
113        ) -> std::pin::Pin<
114            Box<
115                dyn std::future::Future<
116                        Output = Result<BoxStream, Box<dyn std::error::Error + Send + Sync>>,
117                    > + Send,
118            >,
119        > + Send
120        + Sync,
121>;
122
123/// Executor for proxy chains.
124///
125/// Establishes a connection through a series of proxy hops, performing
126/// the appropriate protocol handshake at each step.
127///
128/// # Chain Execution Flow
129///
130/// ```text
131/// Transport to hop 1
132/// → TLS wrap (if hop.tls)
133/// → protocol handshake requesting hop 2
134/// → TLS wrap (if hop.tls)
135/// → protocol handshake requesting hop 3
136/// → final protocol handshake requesting destination
137/// ```
138pub struct ChainExecutor {
139    direct_connector: DirectConnector,
140    handlers: Vec<Box<dyn HopHandler>>,
141    tls_wrapper: Option<TlsWrapper>,
142    shared_tls_config: Option<std::sync::Arc<rustls::ClientConfig>>,
143}
144
145impl ChainExecutor {
146    /// Creates a new `ChainExecutor` with the given protocol handlers.
147    pub fn new(handlers: Vec<Box<dyn HopHandler>>) -> Self {
148        Self {
149            direct_connector: DirectConnector,
150            handlers,
151            tls_wrapper: None,
152            shared_tls_config: None,
153        }
154    }
155
156    /// Set a TLS wrapper for upstream hops with `tls: true`.
157    pub fn with_tls_wrapper(mut self, wrapper: TlsWrapper) -> Self {
158        self.tls_wrapper = Some(wrapper);
159        self
160    }
161
162    /// Set a shared TLS client config for protocols that need their own TLS
163    /// handshake (e.g., Trojan).
164    pub fn with_shared_tls_config(
165        mut self,
166        config: Option<std::sync::Arc<rustls::ClientConfig>>,
167    ) -> Self {
168        self.shared_tls_config = config;
169        self
170    }
171
172    /// Get the shared TLS client config, if set.
173    pub fn shared_tls_config(&self) -> Option<&std::sync::Arc<rustls::ClientConfig>> {
174        self.shared_tls_config.as_ref()
175    }
176
177    /// Execute a proxy chain to connect to the target.
178    ///
179    /// # Arguments
180    /// * `chain` - The ordered list of proxy hops
181    /// * `target` - The final destination to connect to
182    ///
183    /// # Returns
184    /// A connected stream ready for data transfer, or a `ChainError`.
185    pub async fn execute(
186        &self,
187        chain: &[ProxyHopSpec],
188        target: &TargetAddr,
189    ) -> Result<BoxStream, ChainError> {
190        if chain.is_empty() {
191            return Err(ChainError::EmptyChain);
192        }
193
194        self.validate_chain(chain)?;
195
196        // Pre-flight: verify handlers exist for all application protocols before connecting.
197        for (i, hop) in chain.iter().enumerate() {
198            let application_protocols = application_protocols(&hop.protocols);
199            if !application_protocols.is_empty() {
200                find_handler(&self.handlers, &application_protocols).map_err(|_| {
201                    ChainError::InvalidChain {
202                        reason: format!(
203                            "hop {i}: no handler for protocols: [{}]",
204                            application_protocols
205                                .iter()
206                                .map(|p| format!("{p:?}"))
207                                .collect::<Vec<_>>()
208                                .join(", ")
209                        ),
210                    }
211                })?;
212            }
213        }
214
215        // Step 1: Connect to the first hop's endpoint
216        let first_hop = &chain[0];
217        let first_target = if chain.len() > 1 {
218            endpoint_to_target_addr(&chain[1].endpoint)?
219        } else {
220            target.clone()
221        };
222        let first_hop_addr = endpoint_to_target_addr(&first_hop.endpoint)?;
223
224        let mut current_stream: BoxStream = if first_hop.protocols.contains(&ProtocolSpec::Http3)
225            || first_hop.protocols.contains(&ProtocolSpec::Quic)
226        {
227            let transport_protocol = if first_hop.protocols.contains(&ProtocolSpec::Http3) {
228                ProtocolSpec::Http3
229            } else {
230                ProtocolSpec::Quic
231            };
232            let handler = find_handler(&self.handlers, &[transport_protocol])?;
233            handler
234                .open(&first_hop.endpoint, first_hop, &first_target)
235                .ok_or_else(|| ChainError::InvalidChain {
236                    reason: format!("hop 0: transport {transport_protocol:?} cannot be opened"),
237                })?
238                .await
239                .map_err(|e| ChainError::HandshakeFailed {
240                    hop_index: 0,
241                    protocol: format!("{transport_protocol:?}"),
242                    source: e,
243                })?
244        } else if first_hop.protocols.contains(&ProtocolSpec::Unix) {
245            #[cfg(unix)]
246            {
247                Box::new(
248                    tokio::net::UnixStream::connect(&first_hop.endpoint.host)
249                        .await
250                        .map_err(|e| ChainError::ConnectFailed {
251                            hop_index: 0,
252                            endpoint: first_hop.endpoint.host.clone(),
253                            source: crate::ConnectError::Io(e),
254                        })?,
255                ) as BoxStream
256            }
257            #[cfg(not(unix))]
258            {
259                return Err(ChainError::InvalidChain {
260                    reason: "unix upstreams are unsupported on this platform".to_string(),
261                });
262            }
263        } else {
264            let local_bind = first_hop
265                .local_bind
266                .as_deref()
267                .map(|value| {
268                    value.parse().map_err(|e| ChainError::InvalidChain {
269                        reason: format!("hop 0: invalid local bind '{}': {}", value, e),
270                    })
271                })
272                .transpose()?;
273            self.direct_connector
274                .connect_with_options(&first_hop_addr, &ConnectOptions { local_bind })
275                .await
276                .map_err(|e| ChainError::ConnectFailed {
277                    hop_index: 0,
278                    endpoint: first_hop_addr.to_string(),
279                    source: e,
280                })?
281        };
282
283        // Step 2: For each hop, perform the protocol handshake
284        for (i, hop) in chain.iter().enumerate() {
285            // Apply TLS wrapping if configured for this hop
286            if hop.tls {
287                if let Some(ref tls_wrapper) = self.tls_wrapper {
288                    let server_name = hop
289                        .server_name
290                        .clone()
291                        .unwrap_or_else(|| hop.endpoint.host.clone());
292                    // Set H2 ALPN if this hop uses the HTTP/2 protocol
293                    let alpn = if hop.protocols.contains(&ProtocolSpec::Http2) {
294                        Some(vec![b"h2".to_vec(), b"http/1.1".to_vec()])
295                    } else {
296                        None
297                    };
298                    current_stream = tls_wrapper(current_stream, server_name, alpn)
299                        .await
300                        .map_err(|e| ChainError::HandshakeFailed {
301                            hop_index: i,
302                            protocol: "tls".to_string(),
303                            source: e,
304                        })?;
305                }
306            }
307
308            // Determine the target for this hop's handshake
309            let next_target = if i + 1 < chain.len() {
310                // Target is the next hop's endpoint
311                endpoint_to_target_addr(&chain[i + 1].endpoint)?
312            } else {
313                // Last hop targets the actual destination
314                target.clone()
315            };
316
317            // QUIC/H3 opening already performs the application handshake. For
318            // raw QUIC, continue with the ordinary proxy application handler
319            // (e.g. quic+http) over the newly opened stream.
320            let application_protocols = application_protocols(&hop.protocols);
321            if application_protocols.is_empty() {
322                continue;
323            }
324            let handler = find_handler(&self.handlers, &application_protocols)?;
325
326            current_stream = handler
327                .handshake(current_stream, &next_target, hop, i)
328                .await
329                .map_err(|e| ChainError::HandshakeFailed {
330                    hop_index: i,
331                    protocol: format_protocols(&hop.protocols),
332                    source: e,
333                })?;
334        }
335
336        Ok(current_stream)
337    }
338
339    /// Validate the chain configuration.
340    fn validate_chain(&self, chain: &[ProxyHopSpec]) -> Result<(), ChainError> {
341        for (i, hop) in chain.iter().enumerate() {
342            if hop.protocols.is_empty() {
343                return Err(ChainError::InvalidChain {
344                    reason: format!("hop {i}: no protocols specified"),
345                });
346            }
347            if hop.endpoint.host.is_empty() {
348                return Err(ChainError::InvalidChain {
349                    reason: format!("hop {i}: empty endpoint host"),
350                });
351            }
352            if hop.endpoint.port == 0 && !hop.protocols.contains(&ProtocolSpec::Unix) {
353                return Err(ChainError::InvalidChain {
354                    reason: format!("hop {i}: port cannot be 0"),
355                });
356            }
357        }
358        Ok(())
359    }
360}
361
362/// Convert an `EndpointSpec` to a `TargetAddr`.
363fn endpoint_to_target_addr(endpoint: &EndpointSpec) -> Result<TargetAddr, ChainError> {
364    let host = if let Ok(ip) = endpoint.host.parse::<std::net::IpAddr>() {
365        TargetHost::Ip(ip)
366    } else {
367        TargetHost::Domain(endpoint.host.clone())
368    };
369    Ok(TargetAddr {
370        host,
371        port: endpoint.port,
372    })
373}
374
375/// Find a handler that supports one of the given protocols.
376fn find_handler<'a>(
377    handlers: &'a [Box<dyn HopHandler>],
378    protocols: &[ProtocolSpec],
379) -> Result<&'a dyn HopHandler, ChainError> {
380    for handler in handlers {
381        if protocols.contains(&handler.protocol()) {
382            return Ok(handler.as_ref());
383        }
384    }
385    Err(ChainError::InvalidChain {
386        reason: format!(
387            "no handler for protocols: [{}]",
388            protocols
389                .iter()
390                .map(|p| format!("{p:?}"))
391                .collect::<Vec<_>>()
392                .join(", ")
393        ),
394    })
395}
396
397fn application_protocols(protocols: &[ProtocolSpec]) -> Vec<ProtocolSpec> {
398    protocols
399        .iter()
400        .copied()
401        .filter(|protocol| !matches!(protocol, ProtocolSpec::Http3 | ProtocolSpec::Quic))
402        .collect()
403}
404
405/// Format a list of protocols as a string.
406fn format_protocols(protocols: &[ProtocolSpec]) -> String {
407    protocols
408        .iter()
409        .map(|p| format!("{p:?}"))
410        .collect::<Vec<_>>()
411        .join("+")
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use crate::TargetHost;
418    use eggress_uri::CredentialSpec;
419    use std::sync::Arc;
420
421    /// A mock handler that records the target and returns a successful result.
422    struct MockHandler {
423        protocol: ProtocolSpec,
424        captured_target: std::sync::Arc<std::sync::Mutex<Option<TargetAddr>>>,
425    }
426
427    impl MockHandler {
428        fn new(
429            protocol: ProtocolSpec,
430        ) -> (Self, std::sync::Arc<std::sync::Mutex<Option<TargetAddr>>>) {
431            let captured_target = std::sync::Arc::new(std::sync::Mutex::new(None));
432            let handler = Self {
433                protocol,
434                captured_target: captured_target.clone(),
435            };
436            (handler, captured_target)
437        }
438    }
439
440    impl HopHandler for MockHandler {
441        fn protocol(&self) -> ProtocolSpec {
442            self.protocol
443        }
444
445        fn handshake<'a>(
446            &'a self,
447            stream: BoxStream,
448            target: &'a TargetAddr,
449            _hop: &'a ProxyHopSpec,
450            _hop_index: usize,
451        ) -> HandshakeFuture<'a> {
452            Box::pin(async move {
453                *self.captured_target.lock().unwrap() = Some(target.clone());
454                Ok(stream)
455            })
456        }
457    }
458
459    /// A mock handler that always fails.
460    struct FailingHandler {
461        protocol: ProtocolSpec,
462        error_message: String,
463    }
464
465    impl HopHandler for FailingHandler {
466        fn protocol(&self) -> ProtocolSpec {
467            self.protocol
468        }
469
470        fn handshake<'a>(
471            &'a self,
472            _stream: BoxStream,
473            _target: &'a TargetAddr,
474            _hop: &'a ProxyHopSpec,
475            _hop_index: usize,
476        ) -> HandshakeFuture<'a> {
477            let msg = self.error_message.clone();
478            Box::pin(async move { Err(msg.into()) })
479        }
480    }
481
482    fn make_hop(protocol: ProtocolSpec, host: &str, port: u16) -> ProxyHopSpec {
483        ProxyHopSpec {
484            protocols: vec![protocol],
485            endpoint: EndpointSpec {
486                host: host.to_string(),
487                port,
488            },
489            credentials: None,
490            rule: None,
491            local_bind: None,
492            tls: false,
493            server_name: None,
494            insecure: false,
495            plugins: Vec::new(),
496            auth_prefix: None,
497        }
498    }
499
500    fn make_hop_with_creds(
501        protocol: ProtocolSpec,
502        host: &str,
503        port: u16,
504        username: &str,
505        password: &str,
506    ) -> ProxyHopSpec {
507        ProxyHopSpec {
508            protocols: vec![protocol],
509            endpoint: EndpointSpec {
510                host: host.to_string(),
511                port,
512            },
513            credentials: Some(CredentialSpec {
514                username: username.to_string(),
515                password: password.to_string(),
516            }),
517            rule: None,
518            local_bind: None,
519            tls: false,
520            server_name: None,
521            insecure: false,
522            plugins: Vec::new(),
523            auth_prefix: None,
524        }
525    }
526
527    fn make_target(domain: &str, port: u16) -> TargetAddr {
528        TargetAddr {
529            host: TargetHost::Domain(domain.to_string()),
530            port,
531        }
532    }
533
534    fn make_ip_target(ip: std::net::IpAddr, port: u16) -> TargetAddr {
535        TargetAddr {
536            host: TargetHost::Ip(ip),
537            port,
538        }
539    }
540
541    // ===== Empty/Invalid Chain Tests =====
542
543    #[tokio::test]
544    async fn test_empty_chain() {
545        let executor = ChainExecutor::new(vec![]);
546        let target = make_target("example.com", 80);
547        let result = executor.execute(&[], &target).await;
548        match result {
549            Err(e) => {
550                assert!(matches!(e, ChainError::EmptyChain));
551                assert_eq!(
552                    e.to_string(),
553                    "chain is empty, at least one hop is required"
554                );
555            }
556            Ok(_) => panic!("expected EmptyChain error"),
557        }
558    }
559
560    #[tokio::test]
561    async fn test_hop_no_protocols() {
562        let hop = ProxyHopSpec {
563            protocols: vec![],
564            endpoint: EndpointSpec {
565                host: "127.0.0.1".to_string(),
566                port: 8080,
567            },
568            credentials: None,
569            rule: None,
570            local_bind: None,
571            tls: false,
572            server_name: None,
573            insecure: false,
574            plugins: Vec::new(),
575            auth_prefix: None,
576        };
577        let executor = ChainExecutor::new(vec![]);
578        let target = make_target("example.com", 80);
579        let result = executor.execute(&[hop], &target).await;
580        match result {
581            Err(ChainError::InvalidChain { reason }) => {
582                assert!(reason.contains("no protocols specified"));
583            }
584            _ => panic!("expected InvalidChain error"),
585        }
586    }
587
588    #[tokio::test]
589    async fn test_hop_empty_host() {
590        let hop = make_hop(ProtocolSpec::Http, "", 8080);
591        let executor = ChainExecutor::new(vec![]);
592        let target = make_target("example.com", 80);
593        let result = executor.execute(&[hop], &target).await;
594        match result {
595            Err(ChainError::InvalidChain { reason }) => {
596                assert!(reason.contains("empty endpoint host"));
597            }
598            _ => panic!("expected InvalidChain error"),
599        }
600    }
601
602    #[tokio::test]
603    async fn test_hop_zero_port() {
604        let hop = ProxyHopSpec {
605            protocols: vec![ProtocolSpec::Http],
606            endpoint: EndpointSpec {
607                host: "127.0.0.1".to_string(),
608                port: 0,
609            },
610            credentials: None,
611            rule: None,
612            local_bind: None,
613            tls: false,
614            server_name: None,
615            insecure: false,
616            plugins: Vec::new(),
617            auth_prefix: None,
618        };
619        let executor = ChainExecutor::new(vec![]);
620        let target = make_target("example.com", 80);
621        let result = executor.execute(&[hop], &target).await;
622        match result {
623            Err(ChainError::InvalidChain { reason }) => {
624                assert!(reason.contains("port cannot be 0"));
625            }
626            _ => panic!("expected InvalidChain error"),
627        }
628    }
629
630    // ===== Missing Handler Tests =====
631
632    #[tokio::test]
633    async fn test_no_handler_for_protocol() {
634        let executor = ChainExecutor::new(vec![]);
635        let hop = make_hop(ProtocolSpec::Http, "127.0.0.1", 8080);
636        let target = make_target("example.com", 80);
637        let result = executor.execute(&[hop], &target).await;
638        match result {
639            Err(ChainError::InvalidChain { reason }) => {
640                assert!(reason.contains("no handler for protocols"));
641            }
642            _ => panic!("expected InvalidChain error"),
643        }
644    }
645
646    // ===== Connection Failure Tests =====
647
648    #[tokio::test]
649    async fn test_connect_failed() {
650        let (handler, _) = MockHandler::new(ProtocolSpec::Http);
651        let executor = ChainExecutor::new(vec![Box::new(handler)]);
652        let hop = make_hop(ProtocolSpec::Http, "127.0.0.1", 1);
653        let target = make_target("example.com", 80);
654        let result = executor.execute(&[hop], &target).await;
655        match result {
656            Err(ChainError::ConnectFailed {
657                hop_index, source, ..
658            }) => {
659                assert_eq!(hop_index, 0);
660                assert!(matches!(source, ConnectError::Io(_)));
661            }
662            Err(e) => panic!("expected ConnectFailed, got: {e}"),
663            Ok(_) => panic!("expected error"),
664        }
665    }
666
667    // ===== Handshake Failure Tests =====
668
669    #[tokio::test]
670    async fn test_handshake_failed() {
671        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
672        let addr = listener.local_addr().unwrap();
673
674        let server_jh = tokio::spawn(async move {
675            let (_stream, _) = listener.accept().await.unwrap();
676            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
677        });
678
679        let failing_handler: Box<dyn HopHandler> = Box::new(FailingHandler {
680            protocol: ProtocolSpec::Http,
681            error_message: "handshake timeout".to_string(),
682        });
683
684        let executor = ChainExecutor::new(vec![failing_handler]);
685        let hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
686        let target = make_target("example.com", 80);
687        let result = executor.execute(&[hop], &target).await;
688
689        match result {
690            Err(ChainError::HandshakeFailed {
691                hop_index,
692                protocol,
693                source,
694            }) => {
695                assert_eq!(hop_index, 0);
696                assert_eq!(protocol, "Http");
697                assert_eq!(source.to_string(), "handshake timeout");
698            }
699            Err(e) => panic!("expected HandshakeFailed, got: {e}"),
700            Ok(_) => panic!("expected error"),
701        }
702
703        server_jh.abort();
704    }
705
706    // ===== Domain Name Preservation Tests =====
707
708    #[tokio::test]
709    async fn test_domain_preserved_for_single_hop() {
710        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
711        let addr = listener.local_addr().unwrap();
712
713        let server_jh = tokio::spawn(async move {
714            let (_stream, _) = listener.accept().await.unwrap();
715            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
716        });
717
718        let (handler, captured) = MockHandler::new(ProtocolSpec::Socks5);
719        let executor = ChainExecutor::new(vec![Box::new(handler)]);
720
721        let hop = make_hop(ProtocolSpec::Socks5, &addr.ip().to_string(), addr.port());
722        let target = make_target("example.com", 443);
723        let result = executor.execute(&[hop], &target).await;
724
725        assert!(result.is_ok());
726
727        let captured_target = captured.lock().unwrap().take().unwrap();
728        assert_eq!(
729            captured_target.host,
730            TargetHost::Domain("example.com".to_string())
731        );
732        assert_eq!(captured_target.port, 443);
733
734        server_jh.abort();
735    }
736
737    #[tokio::test]
738    async fn test_domain_preserved_through_two_hops() {
739        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
740        let addr1 = listener1.local_addr().unwrap();
741
742        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
743        let addr2 = listener2.local_addr().unwrap();
744
745        let server_jh1 = tokio::spawn(async move {
746            let (_stream, _) = listener1.accept().await.unwrap();
747            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
748        });
749
750        let server_jh2 = tokio::spawn(async move {
751            let (_stream, _) = listener2.accept().await.unwrap();
752            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
753        });
754
755        let (handler1, captured1) = MockHandler::new(ProtocolSpec::Socks5);
756        let (handler2, captured2) = MockHandler::new(ProtocolSpec::Http);
757        let executor = ChainExecutor::new(vec![Box::new(handler1), Box::new(handler2)]);
758
759        let hop1 = make_hop(ProtocolSpec::Socks5, &addr1.ip().to_string(), addr1.port());
760        let hop2 = make_hop(ProtocolSpec::Http, &addr2.ip().to_string(), addr2.port());
761        let target = make_target("example.com", 443);
762        let result = executor.execute(&[hop1, hop2], &target).await;
763
764        assert!(result.is_ok());
765
766        let target1 = captured1.lock().unwrap().take().unwrap();
767        assert_eq!(target1, make_ip_target(addr2.ip(), addr2.port()));
768
769        let target2 = captured2.lock().unwrap().take().unwrap();
770        assert_eq!(target2.host, TargetHost::Domain("example.com".to_string()));
771        assert_eq!(target2.port, 443);
772
773        server_jh1.abort();
774        server_jh2.abort();
775    }
776
777    // ===== Multi-hop Chain Tests =====
778
779    #[tokio::test]
780    async fn test_single_hop_chain() {
781        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
782        let addr = listener.local_addr().unwrap();
783
784        let server_jh = tokio::spawn(async move {
785            let (_stream, _) = listener.accept().await.unwrap();
786            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
787        });
788
789        let (handler, captured) = MockHandler::new(ProtocolSpec::Http);
790        let executor = ChainExecutor::new(vec![Box::new(handler)]);
791
792        let hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
793        let target = make_target("destination.example.com", 443);
794        let result = executor.execute(&[hop], &target).await;
795
796        assert!(result.is_ok());
797
798        let captured_target = captured.lock().unwrap().take().unwrap();
799        assert_eq!(
800            captured_target.host,
801            TargetHost::Domain("destination.example.com".to_string())
802        );
803        assert_eq!(captured_target.port, 443);
804
805        server_jh.abort();
806    }
807
808    #[tokio::test]
809    async fn test_two_hop_chain() {
810        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
811        let addr1 = listener1.local_addr().unwrap();
812
813        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
814        let addr2 = listener2.local_addr().unwrap();
815
816        let server_jh1 = tokio::spawn(async move {
817            let (_stream, _) = listener1.accept().await.unwrap();
818            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
819        });
820
821        let server_jh2 = tokio::spawn(async move {
822            let (_stream, _) = listener2.accept().await.unwrap();
823            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
824        });
825
826        let (handler1, captured1) = MockHandler::new(ProtocolSpec::Socks5);
827        let (handler2, captured2) = MockHandler::new(ProtocolSpec::Http);
828        let executor = ChainExecutor::new(vec![Box::new(handler1), Box::new(handler2)]);
829
830        let hop1 = make_hop(ProtocolSpec::Socks5, &addr1.ip().to_string(), addr1.port());
831        let hop2 = make_hop(ProtocolSpec::Http, &addr2.ip().to_string(), addr2.port());
832        let target = make_target("final.example.com", 443);
833        let result = executor.execute(&[hop1, hop2], &target).await;
834
835        assert!(result.is_ok());
836
837        let target1 = captured1.lock().unwrap().take().unwrap();
838        assert_eq!(target1.host, TargetHost::Ip(addr2.ip()));
839        assert_eq!(target1.port, addr2.port());
840
841        let target2 = captured2.lock().unwrap().take().unwrap();
842        assert_eq!(
843            target2.host,
844            TargetHost::Domain("final.example.com".to_string())
845        );
846        assert_eq!(target2.port, 443);
847
848        server_jh1.abort();
849        server_jh2.abort();
850    }
851
852    #[tokio::test]
853    async fn test_three_hop_chain() {
854        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
855        let addr1 = listener1.local_addr().unwrap();
856
857        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
858        let addr2 = listener2.local_addr().unwrap();
859
860        let listener3 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
861        let addr3 = listener3.local_addr().unwrap();
862
863        let server_jh1 = tokio::spawn(async move {
864            let (_stream, _) = listener1.accept().await.unwrap();
865            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
866        });
867
868        let server_jh2 = tokio::spawn(async move {
869            let (_stream, _) = listener2.accept().await.unwrap();
870            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
871        });
872
873        let server_jh3 = tokio::spawn(async move {
874            let (_stream, _) = listener3.accept().await.unwrap();
875            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
876        });
877
878        let (handler1, captured1) = MockHandler::new(ProtocolSpec::Socks5);
879        let (handler2, captured2) = MockHandler::new(ProtocolSpec::Http);
880        let (handler3, captured3) = MockHandler::new(ProtocolSpec::Socks4);
881        let executor = ChainExecutor::new(vec![
882            Box::new(handler1),
883            Box::new(handler2),
884            Box::new(handler3),
885        ]);
886
887        let hop1 = make_hop(ProtocolSpec::Socks5, &addr1.ip().to_string(), addr1.port());
888        let hop2 = make_hop(ProtocolSpec::Http, &addr2.ip().to_string(), addr2.port());
889        let hop3 = make_hop(ProtocolSpec::Socks4, &addr3.ip().to_string(), addr3.port());
890        let target = make_target("final.example.com", 8080);
891        let result = executor.execute(&[hop1, hop2, hop3], &target).await;
892
893        assert!(result.is_ok());
894
895        let target1 = captured1.lock().unwrap().take().unwrap();
896        assert_eq!(target1.host, TargetHost::Ip(addr2.ip()));
897        assert_eq!(target1.port, addr2.port());
898
899        let target2 = captured2.lock().unwrap().take().unwrap();
900        assert_eq!(target2.host, TargetHost::Ip(addr3.ip()));
901        assert_eq!(target2.port, addr3.port());
902
903        let target3 = captured3.lock().unwrap().take().unwrap();
904        assert_eq!(
905            target3.host,
906            TargetHost::Domain("final.example.com".to_string())
907        );
908        assert_eq!(target3.port, 8080);
909
910        server_jh1.abort();
911        server_jh2.abort();
912        server_jh3.abort();
913    }
914
915    // ===== Credential Passing Tests =====
916
917    #[tokio::test]
918    async fn test_credentials_passed_to_handler() {
919        use std::sync::Arc;
920
921        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
922        let addr = listener.local_addr().unwrap();
923
924        let server_jh = tokio::spawn(async move {
925            let (_stream, _) = listener.accept().await.unwrap();
926            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
927        });
928
929        struct CapturingHandler {
930            protocol: ProtocolSpec,
931            captured_creds: Arc<std::sync::Mutex<Option<CredentialSpec>>>,
932        }
933
934        impl HopHandler for CapturingHandler {
935            fn protocol(&self) -> ProtocolSpec {
936                self.protocol
937            }
938
939            fn handshake<'a>(
940                &'a self,
941                stream: BoxStream,
942                _target: &'a TargetAddr,
943                hop: &'a ProxyHopSpec,
944                _hop_index: usize,
945            ) -> HandshakeFuture<'a> {
946                Box::pin(async move {
947                    if let Some(creds) = hop.credentials.as_ref() {
948                        *self.captured_creds.lock().unwrap() = Some(creds.clone());
949                    }
950                    Ok(stream)
951                })
952            }
953        }
954
955        let captured_creds = Arc::new(std::sync::Mutex::new(None));
956        let handler: Box<dyn HopHandler> = Box::new(CapturingHandler {
957            protocol: ProtocolSpec::Http,
958            captured_creds: captured_creds.clone(),
959        });
960
961        let executor = ChainExecutor::new(vec![handler]);
962
963        let hop = make_hop_with_creds(
964            ProtocolSpec::Http,
965            &addr.ip().to_string(),
966            addr.port(),
967            "testuser",
968            "testpass",
969        );
970        let target = make_target("example.com", 80);
971        let result = executor.execute(&[hop], &target).await;
972
973        assert!(result.is_ok());
974
975        let creds = captured_creds.lock().unwrap().take().unwrap();
976        assert_eq!(creds.username, "testuser");
977        assert_eq!(creds.password, "testpass");
978
979        server_jh.abort();
980    }
981
982    // ===== Handler Selection Tests =====
983
984    #[tokio::test]
985    async fn test_handler_selection_first_matching() {
986        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
987        let addr = listener.local_addr().unwrap();
988
989        let server_jh = tokio::spawn(async move {
990            let (_stream, _) = listener.accept().await.unwrap();
991            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
992        });
993
994        let (handler, _) = MockHandler::new(ProtocolSpec::Http);
995        let executor = ChainExecutor::new(vec![Box::new(handler)]);
996
997        let hop = ProxyHopSpec {
998            protocols: vec![ProtocolSpec::Http, ProtocolSpec::Socks5],
999            endpoint: EndpointSpec {
1000                host: addr.ip().to_string(),
1001                port: addr.port(),
1002            },
1003            credentials: None,
1004            rule: None,
1005            local_bind: None,
1006            tls: false,
1007            server_name: None,
1008            insecure: false,
1009            plugins: Vec::new(),
1010            auth_prefix: None,
1011        };
1012        let target = make_target("example.com", 80);
1013        let result = executor.execute(&[hop], &target).await;
1014
1015        assert!(result.is_ok());
1016
1017        server_jh.abort();
1018    }
1019
1020    // ===== Error Chain Index Tests =====
1021
1022    #[tokio::test]
1023    async fn test_error_identifies_failing_hop() {
1024        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1025        let addr1 = listener1.local_addr().unwrap();
1026
1027        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1028        let addr2 = listener2.local_addr().unwrap();
1029
1030        let server_jh1 = tokio::spawn(async move {
1031            let (_stream, _) = listener1.accept().await.unwrap();
1032            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1033        });
1034
1035        let server_jh2 = tokio::spawn(async move {
1036            let (_stream, _) = listener2.accept().await.unwrap();
1037            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1038        });
1039
1040        let (good_handler, _) = MockHandler::new(ProtocolSpec::Socks5);
1041        let bad_handler: Box<dyn HopHandler> = Box::new(FailingHandler {
1042            protocol: ProtocolSpec::Http,
1043            error_message: "proxy refused connection".to_string(),
1044        });
1045
1046        let executor = ChainExecutor::new(vec![Box::new(good_handler), bad_handler]);
1047
1048        let hop1 = make_hop(ProtocolSpec::Socks5, &addr1.ip().to_string(), addr1.port());
1049        let hop2 = make_hop(ProtocolSpec::Http, &addr2.ip().to_string(), addr2.port());
1050        let target = make_target("example.com", 80);
1051        let result = executor.execute(&[hop1, hop2], &target).await;
1052
1053        match result {
1054            Err(ChainError::HandshakeFailed {
1055                hop_index, source, ..
1056            }) => {
1057                assert_eq!(hop_index, 1, "error should identify hop 1 (second hop)");
1058                assert_eq!(source.to_string(), "proxy refused connection");
1059            }
1060            Err(e) => panic!("expected HandshakeFailed, got: {e}"),
1061            Ok(_) => panic!("expected error"),
1062        }
1063
1064        server_jh1.abort();
1065        server_jh2.abort();
1066    }
1067
1068    // ===== IP Address Endpoint Tests =====
1069
1070    #[tokio::test]
1071    async fn test_ip_endpoint_resolved() {
1072        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1073        let addr = listener.local_addr().unwrap();
1074
1075        let server_jh = tokio::spawn(async move {
1076            let (_stream, _) = listener.accept().await.unwrap();
1077            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1078        });
1079
1080        let (handler, captured) = MockHandler::new(ProtocolSpec::Http);
1081        let executor = ChainExecutor::new(vec![Box::new(handler)]);
1082
1083        let hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
1084        let target = make_ip_target("93.184.216.34".parse().unwrap(), 443);
1085        let result = executor.execute(&[hop], &target).await;
1086
1087        assert!(result.is_ok());
1088
1089        let captured_target = captured.lock().unwrap().take().unwrap();
1090        assert_eq!(
1091            captured_target.host,
1092            TargetHost::Ip("93.184.216.34".parse().unwrap())
1093        );
1094        assert_eq!(captured_target.port, 443);
1095
1096        server_jh.abort();
1097    }
1098
1099    // ===== Mixed Protocol Chain Tests =====
1100
1101    #[tokio::test]
1102    async fn test_socks5_to_http_chain() {
1103        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1104        let addr1 = listener1.local_addr().unwrap();
1105
1106        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1107        let addr2 = listener2.local_addr().unwrap();
1108
1109        let server_jh1 = tokio::spawn(async move {
1110            let (_stream, _) = listener1.accept().await.unwrap();
1111            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1112        });
1113
1114        let server_jh2 = tokio::spawn(async move {
1115            let (_stream, _) = listener2.accept().await.unwrap();
1116            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1117        });
1118
1119        let (handler1, captured1) = MockHandler::new(ProtocolSpec::Socks5);
1120        let (handler2, captured2) = MockHandler::new(ProtocolSpec::Http);
1121        let executor = ChainExecutor::new(vec![Box::new(handler1), Box::new(handler2)]);
1122
1123        let hop1 = make_hop(ProtocolSpec::Socks5, &addr1.ip().to_string(), addr1.port());
1124        let hop2 = make_hop(ProtocolSpec::Http, &addr2.ip().to_string(), addr2.port());
1125        let target = make_target("target.example.com", 8080);
1126        let result = executor.execute(&[hop1, hop2], &target).await;
1127
1128        assert!(result.is_ok());
1129
1130        let target1 = captured1.lock().unwrap().take().unwrap();
1131        assert_eq!(target1.host, TargetHost::Ip(addr2.ip()));
1132
1133        let target2 = captured2.lock().unwrap().take().unwrap();
1134        assert_eq!(
1135            target2.host,
1136            TargetHost::Domain("target.example.com".to_string())
1137        );
1138
1139        server_jh1.abort();
1140        server_jh2.abort();
1141    }
1142
1143    #[tokio::test]
1144    async fn test_http_to_socks5_chain() {
1145        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1146        let addr1 = listener1.local_addr().unwrap();
1147
1148        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1149        let addr2 = listener2.local_addr().unwrap();
1150
1151        let server_jh1 = tokio::spawn(async move {
1152            let (_stream, _) = listener1.accept().await.unwrap();
1153            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1154        });
1155
1156        let server_jh2 = tokio::spawn(async move {
1157            let (_stream, _) = listener2.accept().await.unwrap();
1158            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1159        });
1160
1161        let (handler1, captured1) = MockHandler::new(ProtocolSpec::Http);
1162        let (handler2, captured2) = MockHandler::new(ProtocolSpec::Socks5);
1163        let executor = ChainExecutor::new(vec![Box::new(handler1), Box::new(handler2)]);
1164
1165        let hop1 = make_hop(ProtocolSpec::Http, &addr1.ip().to_string(), addr1.port());
1166        let hop2 = make_hop(ProtocolSpec::Socks5, &addr2.ip().to_string(), addr2.port());
1167        let target = make_target("target.example.com", 443);
1168        let result = executor.execute(&[hop1, hop2], &target).await;
1169
1170        assert!(result.is_ok());
1171
1172        let target1 = captured1.lock().unwrap().take().unwrap();
1173        assert_eq!(target1.host, TargetHost::Ip(addr2.ip()));
1174
1175        let target2 = captured2.lock().unwrap().take().unwrap();
1176        assert_eq!(
1177            target2.host,
1178            TargetHost::Domain("target.example.com".to_string())
1179        );
1180
1181        server_jh1.abort();
1182        server_jh2.abort();
1183    }
1184
1185    #[tokio::test]
1186    async fn test_socks5_to_socks5_chain() {
1187        use std::sync::Arc;
1188
1189        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1190        let addr1 = listener1.local_addr().unwrap();
1191
1192        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1193        let addr2 = listener2.local_addr().unwrap();
1194
1195        let server_jh1 = tokio::spawn(async move {
1196            let (_stream, _) = listener1.accept().await.unwrap();
1197            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1198        });
1199
1200        let server_jh2 = tokio::spawn(async move {
1201            let (_stream, _) = listener2.accept().await.unwrap();
1202            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1203        });
1204
1205        struct MultiTargetRecorder {
1206            protocol: ProtocolSpec,
1207            targets: Arc<std::sync::Mutex<Vec<TargetAddr>>>,
1208        }
1209
1210        impl HopHandler for MultiTargetRecorder {
1211            fn protocol(&self) -> ProtocolSpec {
1212                self.protocol
1213            }
1214
1215            fn handshake<'a>(
1216                &'a self,
1217                stream: BoxStream,
1218                target: &'a TargetAddr,
1219                _hop: &'a ProxyHopSpec,
1220                _hop_index: usize,
1221            ) -> HandshakeFuture<'a> {
1222                let targets = self.targets.clone();
1223                let target_clone = target.clone();
1224                Box::pin(async move {
1225                    targets.lock().unwrap().push(target_clone);
1226                    Ok(stream)
1227                })
1228            }
1229        }
1230
1231        let targets = Arc::new(std::sync::Mutex::new(Vec::new()));
1232        let handler: Box<dyn HopHandler> = Box::new(MultiTargetRecorder {
1233            protocol: ProtocolSpec::Socks5,
1234            targets: targets.clone(),
1235        });
1236        let executor = ChainExecutor::new(vec![handler]);
1237
1238        let hop1 = make_hop(ProtocolSpec::Socks5, &addr1.ip().to_string(), addr1.port());
1239        let hop2 = make_hop(ProtocolSpec::Socks5, &addr2.ip().to_string(), addr2.port());
1240        let target = make_target("target.example.com", 443);
1241        let result = executor.execute(&[hop1, hop2], &target).await;
1242
1243        assert!(result.is_ok());
1244
1245        let captured_targets = targets.lock().unwrap();
1246        assert_eq!(captured_targets.len(), 2);
1247
1248        // First SOCKS5 hop targets second SOCKS5 proxy
1249        assert_eq!(captured_targets[0].host, TargetHost::Ip(addr2.ip()));
1250        assert_eq!(captured_targets[0].port, addr2.port());
1251
1252        // Second SOCKS5 hop targets final destination (domain preserved)
1253        assert_eq!(
1254            captured_targets[1].host,
1255            TargetHost::Domain("target.example.com".to_string())
1256        );
1257        assert_eq!(captured_targets[1].port, 443);
1258
1259        server_jh1.abort();
1260        server_jh2.abort();
1261    }
1262
1263    #[tokio::test]
1264    async fn test_http_to_http_chain() {
1265        use std::sync::Arc;
1266
1267        let listener1 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1268        let addr1 = listener1.local_addr().unwrap();
1269
1270        let listener2 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1271        let addr2 = listener2.local_addr().unwrap();
1272
1273        let server_jh1 = tokio::spawn(async move {
1274            let (_stream, _) = listener1.accept().await.unwrap();
1275            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1276        });
1277
1278        let server_jh2 = tokio::spawn(async move {
1279            let (_stream, _) = listener2.accept().await.unwrap();
1280            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1281        });
1282
1283        // Handler that records all targets it receives
1284        struct MultiTargetRecorder {
1285            protocol: ProtocolSpec,
1286            targets: Arc<std::sync::Mutex<Vec<TargetAddr>>>,
1287        }
1288
1289        impl HopHandler for MultiTargetRecorder {
1290            fn protocol(&self) -> ProtocolSpec {
1291                self.protocol
1292            }
1293
1294            fn handshake<'a>(
1295                &'a self,
1296                stream: BoxStream,
1297                target: &'a TargetAddr,
1298                _hop: &'a ProxyHopSpec,
1299                _hop_index: usize,
1300            ) -> HandshakeFuture<'a> {
1301                let targets = self.targets.clone();
1302                let target_clone = target.clone();
1303                Box::pin(async move {
1304                    targets.lock().unwrap().push(target_clone);
1305                    Ok(stream)
1306                })
1307            }
1308        }
1309
1310        let targets = Arc::new(std::sync::Mutex::new(Vec::new()));
1311        let handler: Box<dyn HopHandler> = Box::new(MultiTargetRecorder {
1312            protocol: ProtocolSpec::Http,
1313            targets: targets.clone(),
1314        });
1315        let executor = ChainExecutor::new(vec![handler]);
1316
1317        let hop1 = make_hop(ProtocolSpec::Http, &addr1.ip().to_string(), addr1.port());
1318        let hop2 = make_hop(ProtocolSpec::Http, &addr2.ip().to_string(), addr2.port());
1319        let target = make_target("target.example.com", 80);
1320        let result = executor.execute(&[hop1, hop2], &target).await;
1321
1322        assert!(result.is_ok());
1323
1324        let captured_targets = targets.lock().unwrap();
1325        assert_eq!(captured_targets.len(), 2);
1326
1327        // First hop should target second proxy
1328        assert_eq!(captured_targets[0].host, TargetHost::Ip(addr2.ip()));
1329        assert_eq!(captured_targets[0].port, addr2.port());
1330
1331        // Second hop should target final destination
1332        assert_eq!(
1333            captured_targets[1].host,
1334            TargetHost::Domain("target.example.com".to_string())
1335        );
1336        assert_eq!(captured_targets[1].port, 80);
1337
1338        server_jh1.abort();
1339        server_jh2.abort();
1340    }
1341
1342    // ===== Chain Validation Tests =====
1343
1344    #[test]
1345    fn test_validate_chain_valid() {
1346        let executor = ChainExecutor::new(vec![]);
1347        let chain = vec![
1348            make_hop(ProtocolSpec::Http, "127.0.0.1", 8080),
1349            make_hop(ProtocolSpec::Socks5, "127.0.0.1", 1080),
1350        ];
1351        assert!(executor.validate_chain(&chain).is_ok());
1352    }
1353
1354    #[test]
1355    fn test_validate_chain_empty_protocols() {
1356        let executor = ChainExecutor::new(vec![]);
1357        let chain = vec![ProxyHopSpec {
1358            protocols: vec![],
1359            endpoint: EndpointSpec {
1360                host: "127.0.0.1".to_string(),
1361                port: 8080,
1362            },
1363            credentials: None,
1364            rule: None,
1365            local_bind: None,
1366            tls: false,
1367            server_name: None,
1368            insecure: false,
1369            plugins: Vec::new(),
1370            auth_prefix: None,
1371        }];
1372        assert!(executor.validate_chain(&chain).is_err());
1373    }
1374
1375    #[test]
1376    fn test_validate_chain_empty_host() {
1377        let executor = ChainExecutor::new(vec![]);
1378        let chain = vec![make_hop(ProtocolSpec::Http, "", 8080)];
1379        assert!(executor.validate_chain(&chain).is_err());
1380    }
1381
1382    #[test]
1383    fn test_validate_chain_zero_port() {
1384        let executor = ChainExecutor::new(vec![]);
1385        let chain = vec![ProxyHopSpec {
1386            protocols: vec![ProtocolSpec::Http],
1387            endpoint: EndpointSpec {
1388                host: "127.0.0.1".to_string(),
1389                port: 0,
1390            },
1391            credentials: None,
1392            rule: None,
1393            local_bind: None,
1394            tls: false,
1395            server_name: None,
1396            insecure: false,
1397            plugins: Vec::new(),
1398            auth_prefix: None,
1399        }];
1400        assert!(executor.validate_chain(&chain).is_err());
1401    }
1402
1403    // ===== Error Display Tests =====
1404
1405    #[test]
1406    fn test_chain_error_display() {
1407        let err = ChainError::EmptyChain;
1408        assert_eq!(
1409            err.to_string(),
1410            "chain is empty, at least one hop is required"
1411        );
1412
1413        let err = ChainError::InvalidChain {
1414            reason: "test reason".to_string(),
1415        };
1416        assert_eq!(err.to_string(), "invalid chain: test reason");
1417
1418        let err = ChainError::ConnectFailed {
1419            hop_index: 0,
1420            endpoint: "127.0.0.1:8080".to_string(),
1421            source: ConnectError::ConnectionRefused,
1422        };
1423        assert!(err.to_string().contains("hop 0"));
1424        assert!(err.to_string().contains("127.0.0.1:8080"));
1425        assert!(err.to_string().contains("connection refused"));
1426    }
1427
1428    // ===== TLS Wrapping Tests =====
1429
1430    #[tokio::test]
1431    async fn test_tls_wrapper_called_when_hop_tls_true() {
1432        use std::sync::atomic::{AtomicBool, Ordering};
1433
1434        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1435        let addr = listener.local_addr().unwrap();
1436
1437        let server_jh = tokio::spawn(async move {
1438            let (_stream, _) = listener.accept().await.unwrap();
1439            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1440        });
1441
1442        let (_handler, _captured) = MockHandler::new(ProtocolSpec::Http);
1443        let executor = ChainExecutor::new(vec![Box::new(_handler)]);
1444
1445        let tls_called = Arc::new(AtomicBool::new(false));
1446        let tls_called_clone = tls_called.clone();
1447
1448        let tls_wrapper: TlsWrapper = Box::new(move |stream, _server_name, _alpn| {
1449            let called = tls_called_clone.clone();
1450            Box::pin(async move {
1451                called.store(true, Ordering::Relaxed);
1452                // Just pass through - don't actually do TLS in this test
1453                Ok(stream)
1454            })
1455        });
1456
1457        let executor = executor.with_tls_wrapper(tls_wrapper);
1458
1459        let mut hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
1460        hop.tls = true;
1461        hop.server_name = Some("test.example.com".to_string());
1462
1463        let target = make_target("example.com", 80);
1464        let result = executor.execute(&[hop], &target).await;
1465
1466        assert!(result.is_ok());
1467        assert!(
1468            tls_called.load(Ordering::Relaxed),
1469            "TLS wrapper should have been called"
1470        );
1471
1472        server_jh.abort();
1473    }
1474
1475    #[tokio::test]
1476    async fn test_tls_wrapper_not_called_when_hop_tls_false() {
1477        use std::sync::atomic::{AtomicBool, Ordering};
1478
1479        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1480        let addr = listener.local_addr().unwrap();
1481
1482        let server_jh = tokio::spawn(async move {
1483            let (_stream, _) = listener.accept().await.unwrap();
1484            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1485        });
1486
1487        let (_handler, _captured) = MockHandler::new(ProtocolSpec::Http);
1488        let executor = ChainExecutor::new(vec![Box::new(_handler)]);
1489
1490        let tls_called = Arc::new(AtomicBool::new(false));
1491        let tls_called_clone = tls_called.clone();
1492
1493        let tls_wrapper: TlsWrapper = Box::new(move |stream, _server_name, _alpn| {
1494            let called = tls_called_clone.clone();
1495            Box::pin(async move {
1496                called.store(true, Ordering::Relaxed);
1497                Ok(stream)
1498            })
1499        });
1500
1501        let executor = executor.with_tls_wrapper(tls_wrapper);
1502
1503        let hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
1504        // hop.tls defaults to false
1505
1506        let target = make_target("example.com", 80);
1507        let result = executor.execute(&[hop], &target).await;
1508
1509        assert!(result.is_ok());
1510        assert!(
1511            !tls_called.load(Ordering::Relaxed),
1512            "TLS wrapper should NOT have been called"
1513        );
1514
1515        server_jh.abort();
1516    }
1517
1518    #[tokio::test]
1519    async fn test_tls_wrapper_uses_server_name_from_hop() {
1520        use std::sync::Mutex;
1521
1522        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1523        let addr = listener.local_addr().unwrap();
1524
1525        let server_jh = tokio::spawn(async move {
1526            let (_stream, _) = listener.accept().await.unwrap();
1527            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1528        });
1529
1530        let (handler, _) = MockHandler::new(ProtocolSpec::Http);
1531        let executor = ChainExecutor::new(vec![Box::new(handler)]);
1532
1533        let captured_name = Arc::new(Mutex::new(None::<String>));
1534        let captured_name_clone = captured_name.clone();
1535
1536        let tls_wrapper: TlsWrapper = Box::new(move |stream, server_name, _alpn| {
1537            let captured = captured_name_clone.clone();
1538            Box::pin(async move {
1539                *captured.lock().unwrap() = Some(server_name);
1540                Ok(stream)
1541            })
1542        });
1543
1544        let executor = executor.with_tls_wrapper(tls_wrapper);
1545
1546        let mut hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
1547        hop.tls = true;
1548        hop.server_name = Some("custom-sni.example.com".to_string());
1549
1550        let target = make_target("example.com", 80);
1551        let result = executor.execute(&[hop], &target).await;
1552
1553        assert!(result.is_ok());
1554        let name = captured_name.lock().unwrap().take().unwrap();
1555        assert_eq!(name, "custom-sni.example.com");
1556
1557        server_jh.abort();
1558    }
1559
1560    #[tokio::test]
1561    async fn test_tls_wrapper_falls_back_to_endpoint_host() {
1562        use std::sync::Mutex;
1563
1564        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1565        let addr = listener.local_addr().unwrap();
1566
1567        let server_jh = tokio::spawn(async move {
1568            let (_stream, _) = listener.accept().await.unwrap();
1569            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1570        });
1571
1572        let (handler, _) = MockHandler::new(ProtocolSpec::Http);
1573        let executor = ChainExecutor::new(vec![Box::new(handler)]);
1574
1575        let captured_name = Arc::new(Mutex::new(None::<String>));
1576        let captured_name_clone = captured_name.clone();
1577
1578        let tls_wrapper: TlsWrapper = Box::new(move |stream, server_name, _alpn| {
1579            let captured = captured_name_clone.clone();
1580            Box::pin(async move {
1581                *captured.lock().unwrap() = Some(server_name);
1582                Ok(stream)
1583            })
1584        });
1585
1586        let executor = executor.with_tls_wrapper(tls_wrapper);
1587
1588        // hop with no server_name - should use endpoint host
1589        let hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
1590        // hop.tls defaults to false, so set it to true
1591        let mut hop = hop;
1592        hop.tls = true;
1593        // No server_name set - should fallback to endpoint.host
1594
1595        let target = make_target("example.com", 80);
1596        let result = executor.execute(&[hop], &target).await;
1597
1598        assert!(result.is_ok());
1599        let name = captured_name.lock().unwrap().take().unwrap();
1600        assert_eq!(name, addr.ip().to_string());
1601
1602        server_jh.abort();
1603    }
1604
1605    #[tokio::test]
1606    async fn test_tls_failure_propagates_as_handshake_error() {
1607        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1608        let addr = listener.local_addr().unwrap();
1609
1610        let server_jh = tokio::spawn(async move {
1611            let (_stream, _) = listener.accept().await.unwrap();
1612            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
1613        });
1614
1615        let (handler, _) = MockHandler::new(ProtocolSpec::Http);
1616        let executor = ChainExecutor::new(vec![Box::new(handler)]);
1617
1618        let tls_wrapper: TlsWrapper = Box::new(|_stream, _server_name, _alpn| {
1619            Box::pin(async move {
1620                Err(Box::<dyn std::error::Error + Send + Sync>::from(
1621                    "TLS handshake failed: certificate rejected",
1622                ))
1623            })
1624        });
1625
1626        let executor = executor.with_tls_wrapper(tls_wrapper);
1627
1628        let mut hop = make_hop(ProtocolSpec::Http, &addr.ip().to_string(), addr.port());
1629        hop.tls = true;
1630
1631        let target = make_target("example.com", 80);
1632        let result = executor.execute(&[hop], &target).await;
1633
1634        match result {
1635            Err(ChainError::HandshakeFailed {
1636                hop_index,
1637                protocol,
1638                source,
1639            }) => {
1640                assert_eq!(hop_index, 0);
1641                assert_eq!(protocol, "tls");
1642                assert!(source.to_string().contains("TLS handshake failed"));
1643            }
1644            Err(e) => panic!("expected HandshakeFailed, got: {e}"),
1645            Ok(_) => panic!("expected error"),
1646        }
1647
1648        server_jh.abort();
1649    }
1650}