1use crate::utils::macros::define_inner_service_accessors;
2use crate::{
3 Layer, Service,
4 error::{BoxError, BoxErrorExt},
5 extensions::{Extensions, ExtensionsRef},
6 http::client::proxy::layer::{
7 HttpProxyConnector, HttpProxyConnectorLayer, MaybeHttpProxiedConnection,
8 },
9 io::Io,
10 net::{
11 AuthorityInputExt, HttpVersionInputExt, Protocol, ProtocolInputExt,
12 TargetHttpVersionInputExt,
13 client::{
14 ConnectionError, ConnectionErrorKind, ConnectorService, EstablishedClientConnection,
15 EstablishedProxyRoute, ProxyRoute,
16 },
17 },
18 proxy::socks5::{Socks5ProxyConnector, Socks5ProxyConnectorLayer},
19 telemetry::tracing,
20};
21#[cfg(feature = "tls")]
22use crate::{net::client::ConnectionPolicyScope, tls::TlsTunnel};
23use pin_project_lite::pin_project;
24use std::{
25 fmt::Debug,
26 pin::Pin,
27 task::{self, Poll},
28};
29use tokio::io::{AsyncRead, AsyncWrite};
30
31#[derive(Debug, Clone, ExtensionsRef)]
36pub struct ProxyConnector<S> {
37 inner: S,
38 socks: Socks5ProxyConnector<S>,
39 http: HttpProxyConnector<S>,
40 required: bool,
41}
42
43impl<S: Clone> ProxyConnector<S> {
44 define_inner_service_accessors!();
45
46 fn new(
48 inner: S,
49 socks_proxy_layer: Socks5ProxyConnectorLayer,
50 http_proxy_layer: HttpProxyConnectorLayer,
51 required: bool,
52 ) -> Self {
53 Self {
54 socks: socks_proxy_layer.into_layer(inner.clone()),
55 http: http_proxy_layer.into_layer(inner.clone()),
56 inner,
57 required,
58 }
59 }
60
61 #[inline]
62 pub fn required(
66 inner: S,
67 socks_proxy_layer: Socks5ProxyConnectorLayer,
68 http_proxy_layer: HttpProxyConnectorLayer,
69 ) -> Self {
70 Self::new(inner, socks_proxy_layer, http_proxy_layer, true)
71 }
72
73 #[inline]
74 pub fn optional(
78 inner: S,
79 socks_proxy_layer: Socks5ProxyConnectorLayer,
80 http_proxy_layer: HttpProxyConnectorLayer,
81 ) -> Self {
82 Self::new(inner, socks_proxy_layer, http_proxy_layer, false)
83 }
84}
85
86impl<Input, S> Service<Input> for ProxyConnector<S>
87where
88 S: ConnectorService<Input, Connection: Io + Unpin>,
89 Input: AuthorityInputExt
90 + ProtocolInputExt
91 + HttpVersionInputExt
92 + TargetHttpVersionInputExt
93 + Send
94 + ExtensionsRef
95 + 'static,
96{
97 type Output = EstablishedClientConnection<MaybeProxiedConnection<S::Connection>, Input>;
98 type Error = ConnectionError;
99
100 async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
101 let route = input.extensions().get_ref::<ProxyRoute>();
102 let route_requested = route.is_some();
103
104 match route {
105 None | Some(ProxyRoute::Direct) => {
106 if self.required {
107 return Err(ConnectionError::local(
108 BoxError::from_static_str("proxy required but none is defined"),
109 ConnectionErrorKind::InvalidInput,
110 ));
111 }
112 tracing::trace!("no proxy detected in ctx, using inner connector");
113 let EstablishedClientConnection { input, conn } = self.inner.connect(input).await?;
114
115 if route_requested {
116 conn.extensions().insert(EstablishedProxyRoute::Direct);
117 }
118
119 let conn = MaybeProxiedConnection::direct(conn);
120 Ok(EstablishedClientConnection { input, conn })
121 }
122 Some(ProxyRoute::Proxy(proxy)) => {
123 let protocol = proxy.protocol.as_ref();
124 tracing::trace!(?protocol, "proxy detected in ctx");
125
126 let protocol = protocol.unwrap_or_else(|| {
127 tracing::trace!("no protocol detected, using http as protocol");
128 &Protocol::HTTP
129 });
130
131 if protocol.is_socks5() {
132 tracing::trace!(
133 target = %&proxy.address,
134 "using socks proxy connector",
135 );
136
137 #[cfg(feature = "tls")]
140 let request_tunnel = input.extensions().contains::<TlsTunnel>();
141 let established = self.socks.connect(input).await;
142 #[cfg(feature = "tls")]
143 let established = established.map_err(|error| {
144 if request_tunnel {
145 error.with_policy_scope(ConnectionPolicyScope::Request)
146 } else {
147 error
148 }
149 });
150 let EstablishedClientConnection { input, conn } = established?;
151
152 let conn = MaybeProxiedConnection::socks(conn);
153 Ok(EstablishedClientConnection { input, conn })
154 } else if protocol.is_http() || protocol.is_masque() {
155 tracing::trace!(
158 target = %&proxy.address,
159 "using http proxy connector"
160 );
161
162 let EstablishedClientConnection { input, conn } =
163 self.http.connect(input).await?;
164
165 let conn = MaybeProxiedConnection::http(conn);
166 Ok(EstablishedClientConnection { input, conn })
167 } else {
168 Err(ConnectionError::transport(
169 BoxError::from_static_str("received unsupported proxy protocol"),
170 ConnectionErrorKind::Protocol,
171 )
172 .context_debug_field("protocol", protocol.clone()))
173 }
174 }
175 }
176 }
177}
178
179pin_project! {
180 pub struct MaybeProxiedConnection<S> {
182 #[pin]
183 inner: Connection<S>,
184 }
185}
186
187impl<S: ExtensionsRef> MaybeProxiedConnection<S> {
188 pub fn direct(conn: S) -> Self {
189 Self {
190 inner: Connection::Direct { conn },
191 }
192 }
193
194 pub fn socks(conn: S) -> Self {
195 Self {
196 inner: Connection::Socks { conn },
197 }
198 }
199
200 pub fn http(conn: MaybeHttpProxiedConnection<S>) -> Self {
201 Self {
202 inner: Connection::Http { conn },
203 }
204 }
205}
206
207impl<S: Debug> Debug for MaybeProxiedConnection<S> {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("MaybeProxiedConnection")
210 .field("inner", &self.inner)
211 .finish()
212 }
213}
214
215impl<S: ExtensionsRef> ExtensionsRef for MaybeProxiedConnection<S> {
216 fn extensions(&self) -> &Extensions {
217 match &self.inner {
218 Connection::Direct { conn } | Connection::Socks { conn } => conn.extensions(),
219 Connection::Http { conn } => conn.extensions(),
220 }
221 }
222}
223
224pin_project! {
225 #[project = ConnectionProj]
226 enum Connection<S> {
227 Direct{ #[pin] conn: S },
228 Socks{ #[pin] conn: S },
229 Http{ #[pin] conn: MaybeHttpProxiedConnection<S> },
230
231 }
232}
233
234impl<S: Debug> Debug for Connection<S> {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 match self {
237 Self::Direct { conn } => f.debug_struct("Direct").field("conn", conn).finish(),
238 Self::Socks { conn } => f.debug_struct("Socks").field("conn", conn).finish(),
239 Self::Http { conn } => f.debug_struct("Http").field("conn", conn).finish(),
240 }
241 }
242}
243
244#[warn(clippy::missing_trait_methods)]
245impl<Conn: AsyncWrite> AsyncWrite for MaybeProxiedConnection<Conn> {
246 fn poll_write(
247 self: Pin<&mut Self>,
248 cx: &mut task::Context<'_>,
249 buf: &[u8],
250 ) -> Poll<Result<usize, std::io::Error>> {
251 match self.project().inner.project() {
252 ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
253 conn.poll_write(cx, buf)
254 }
255 ConnectionProj::Http { conn } => conn.poll_write(cx, buf),
256 }
257 }
258
259 fn poll_flush(
260 self: Pin<&mut Self>,
261 cx: &mut task::Context<'_>,
262 ) -> Poll<Result<(), std::io::Error>> {
263 match self.project().inner.project() {
264 ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => conn.poll_flush(cx),
265 ConnectionProj::Http { conn } => conn.poll_flush(cx),
266 }
267 }
268
269 fn poll_shutdown(
270 self: Pin<&mut Self>,
271 cx: &mut task::Context<'_>,
272 ) -> Poll<Result<(), std::io::Error>> {
273 match self.project().inner.project() {
274 ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
275 conn.poll_shutdown(cx)
276 }
277 ConnectionProj::Http { conn } => conn.poll_shutdown(cx),
278 }
279 }
280
281 fn is_write_vectored(&self) -> bool {
282 match &self.inner {
283 Connection::Direct { conn } | Connection::Socks { conn } => conn.is_write_vectored(),
284 Connection::Http { conn } => conn.is_write_vectored(),
285 }
286 }
287
288 fn poll_write_vectored(
289 self: Pin<&mut Self>,
290 cx: &mut task::Context<'_>,
291 bufs: &[std::io::IoSlice<'_>],
292 ) -> Poll<Result<usize, std::io::Error>> {
293 match self.project().inner.project() {
294 ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
295 conn.poll_write_vectored(cx, bufs)
296 }
297 ConnectionProj::Http { conn } => conn.poll_write_vectored(cx, bufs),
298 }
299 }
300}
301
302#[warn(clippy::missing_trait_methods)]
303impl<Conn: AsyncRead> AsyncRead for MaybeProxiedConnection<Conn> {
304 fn poll_read(
305 self: Pin<&mut Self>,
306 cx: &mut task::Context<'_>,
307 buf: &mut tokio::io::ReadBuf<'_>,
308 ) -> Poll<std::io::Result<()>> {
309 match self.project().inner.project() {
310 ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
311 conn.poll_read(cx, buf)
312 }
313 ConnectionProj::Http { conn } => conn.poll_read(cx, buf),
314 }
315 }
316}
317
318pub struct ProxyConnectorLayer {
323 socks_layer: Socks5ProxyConnectorLayer,
324 http_layer: HttpProxyConnectorLayer,
325 required: bool,
326}
327
328impl ProxyConnectorLayer {
329 #[must_use]
330 pub fn required(
334 socks_proxy_layer: Socks5ProxyConnectorLayer,
335 http_proxy_layer: HttpProxyConnectorLayer,
336 ) -> Self {
337 Self {
338 socks_layer: socks_proxy_layer,
339 http_layer: http_proxy_layer,
340 required: true,
341 }
342 }
343
344 #[must_use]
345 pub fn optional(
349 socks_proxy_layer: Socks5ProxyConnectorLayer,
350 http_proxy_layer: HttpProxyConnectorLayer,
351 ) -> Self {
352 Self {
353 socks_layer: socks_proxy_layer,
354 http_layer: http_proxy_layer,
355 required: false,
356 }
357 }
358}
359
360impl<S: Clone> Layer<S> for ProxyConnectorLayer {
361 type Service = ProxyConnector<S>;
362
363 fn layer(&self, inner: S) -> Self::Service {
364 ProxyConnector::new(
365 inner,
366 self.socks_layer.clone(),
367 self.http_layer.clone(),
368 self.required,
369 )
370 }
371
372 fn into_layer(self, inner: S) -> Self::Service {
373 ProxyConnector::new(inner, self.socks_layer, self.http_layer, self.required)
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::{
381 net::{proxy::IoForwardService, test_utils::client::MockSocket},
382 proxy::socks5::{
383 Socks5ProxyConnectorLayer,
384 server::{Connector as EagerSocks5Connector, Socks5Connector},
385 },
386 tcp::client::service::TcpConnector,
387 };
388
389 #[cfg(feature = "tls")]
390 use {
391 crate::{
392 net::{
393 address::HostWithPort,
394 address::ProxyAddress,
395 client::{
396 ConnectRequest, EstablishedProxyRoute, ProxyRouteFailureCache,
397 ProxyRouteFailureCacheConfig, ProxyRouteFailureCacheConnector,
398 ProxyRouteFailureCacheScope,
399 },
400 },
401 service::service_fn,
402 },
403 std::sync::{
404 Arc,
405 atomic::{AtomicUsize, Ordering},
406 },
407 };
408
409 fn assert_socks5_connector<S, C: Socks5Connector<S>>(_: &C) {}
410
411 #[cfg(feature = "tls")]
412 #[tokio::test]
413 async fn a_masque_proxy_receives_its_own_requests_directly_as_forwarded() {
414 let dialed = Arc::new(parking_lot::Mutex::new(None));
415 let transport = service_fn({
416 let dialed = dialed.clone();
417 move |input: ConnectRequest| {
418 *dialed.lock() = Some(input.authority.clone());
419 async move {
420 let (stream, _peer) = tokio::io::duplex(64);
421 Ok::<_, ConnectionError>(EstablishedClientConnection {
422 input,
423 conn: MockSocket::new(stream),
424 })
425 }
426 }
427 });
428 let connector = ProxyConnector::required(
429 transport,
430 Socks5ProxyConnectorLayer::required(),
431 HttpProxyConnectorLayer::required(),
432 );
433 let proxy: ProxyAddress = "masque://user:secret@proxy.example:443".parse().unwrap();
434 let input = ConnectRequest::new(HostWithPort::try_from("proxy.example:443").unwrap())
435 .with_application_protocol(Protocol::HTTPS);
436 input.extensions().insert(ProxyRoute::Proxy(proxy.clone()));
437 let established = connector.serve(input).await.unwrap();
438 assert_eq!(
439 dialed.lock().take().unwrap(),
440 HostWithPort::try_from("proxy.example:443").unwrap()
441 );
442 assert_eq!(
443 established
444 .conn
445 .extensions()
446 .get_ref::<EstablishedProxyRoute>(),
447 Some(&EstablishedProxyRoute::Forward(proxy))
448 );
449 }
450
451 #[cfg(feature = "tls")]
452 #[tokio::test]
453 async fn request_tunnel_failure_does_not_poison_socks_proxy_route() {
454 for scope in [
455 ProxyRouteFailureCacheScope::PerDestination,
456 ProxyRouteFailureCacheScope::PerProxy,
457 ] {
458 let attempts = Arc::new(AtomicUsize::new(0));
459 let transport = service_fn({
460 let attempts = attempts.clone();
461 move |input: ConnectRequest| {
462 attempts.fetch_add(1, Ordering::Relaxed);
463 async move {
464 Err::<EstablishedClientConnection<MockSocket, ConnectRequest>, _>(
465 ConnectionError::transport(
466 BoxError::from_static_str("scripted proxy failure"),
467 if input.extensions().contains::<TlsTunnel>() {
468 ConnectionErrorKind::Protocol
469 } else {
470 ConnectionErrorKind::Unavailable
471 },
472 ),
473 )
474 }
475 }
476 });
477 let mut config = ProxyRouteFailureCacheConfig::default();
478 config.scope = scope;
479 let connector = ProxyRouteFailureCacheConnector::new(
480 ProxyConnector::required(
481 transport,
482 Socks5ProxyConnectorLayer::required(),
483 HttpProxyConnectorLayer::required(),
484 ),
485 ProxyRouteFailureCache::try_new(config).unwrap(),
486 );
487 let request = || {
488 let input = ConnectRequest::new(HostWithPort::example_domain_http())
489 .with_application_protocol(Protocol::HTTP);
490 input.extensions().insert(ProxyRoute::Proxy(
491 "socks5://127.0.0.1:1080".parse().unwrap(),
492 ));
493 input
494 };
495 let customized = request();
496 customized.extensions().insert(TlsTunnel {
497 server_identity: Some("proxy.example".parse().unwrap()),
498 application_protocol: None,
499 alpn: None,
500 });
501 let error = connector.serve(customized).await.unwrap_err();
502 assert_eq!(error.kind(), ConnectionErrorKind::Protocol);
503 assert_eq!(error.policy_scope(), ConnectionPolicyScope::Request);
504
505 let error = connector.serve(request()).await.unwrap_err();
506 assert_eq!(error.kind(), ConnectionErrorKind::Unavailable);
507 assert_eq!(error.policy_scope(), ConnectionPolicyScope::Unknown);
508 assert_eq!(attempts.load(Ordering::Relaxed), 2);
509 _ = connector.serve(request()).await.unwrap_err();
510 assert_eq!(
511 attempts.load(Ordering::Relaxed),
512 2,
513 "ordinary failures remain cached"
514 );
515 }
516 }
517
518 #[test]
519 fn eager_socks5_accepts_combined_proxy_connection() {
520 let proxy_connector = ProxyConnectorLayer::optional(
521 Socks5ProxyConnectorLayer::optional(),
522 HttpProxyConnectorLayer::optional(),
523 )
524 .into_layer(TcpConnector::new());
525 let connector = EagerSocks5Connector::new(proxy_connector, IoForwardService::default());
526
527 assert_socks5_connector::<MockSocket, _>(&connector);
528 }
529}