From ce04666b8b4012a99eb06b46b323c1eded8be1d8 Mon Sep 17 00:00:00 2001 From: Islam Nofl Date: Wed, 31 Jul 2024 22:11:13 +0300 Subject: [PATCH] Add forward traffic to another HTTP proxy for the server (only for LocalProtocol::Tcp) (#326) * Add forward traffic to another HTTP proxy for the server (only for LocalProtocol::Tcp) * Update to the newest code: - Add `connect_with_http_proxy` to `TunnelConnector` * Remove unnecessary error checks * Resolve conflict again --- src/main.rs | 63 +++++++++++++++++++++++++++++++++- src/tunnel/connectors/mod.rs | 15 +++++--- src/tunnel/connectors/sock5.rs | 9 +++++ src/tunnel/connectors/tcp.rs | 30 ++++++++++++++-- src/tunnel/connectors/udp.rs | 15 ++++++-- src/tunnel/server/server.rs | 27 +++++++++------ 6 files changed, 138 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index c6f75f08..2c4d26fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -351,6 +351,29 @@ struct Server { /// The ca will be automatically reloaded if it changes #[arg(long, value_name = "FILE_PATH", verbatim_doc_comment)] tls_client_ca_certs: Option, + + /// If set, will use this http proxy to connect to the client + #[arg( + short = 'p', + long, + value_name = "USER:PASS@HOST:PORT", + verbatim_doc_comment, + env = "HTTP_PROXY" + )] + http_proxy: Option, + + /// If set, will use this login to connect to the http proxy. Override the one from --http-proxy + #[arg(long, value_name = "LOGIN", verbatim_doc_comment, env = "WSTUNNEL_HTTP_PROXY_LOGIN")] + http_proxy_login: Option, + + /// If set, will use this password to connect to the http proxy. Override the one from --http-proxy + #[arg( + long, + value_name = "PASSWORD", + verbatim_doc_comment, + env = "WSTUNNEL_HTTP_PROXY_PASSWORD" + )] + http_proxy_password: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -750,7 +773,24 @@ async fn main() -> anyhow::Result<()> { TransportScheme::from_str(args.remote_addr.scheme()).expect("invalid scheme in server url"); let tls = match transport_scheme { TransportScheme::Ws | TransportScheme::Http => None, - TransportScheme::Wss | TransportScheme::Https => Some(TlsClientConfig { + TransportScheme::Wss => Some(TlsClientConfig { + tls_connector: Arc::new(RwLock::new( + tls::tls_connector( + args.tls_verify_certificate, + transport_scheme.alpn_protocols(), + !args.tls_sni_disable, + tls_certificate, + tls_key, + ) + .expect("Cannot create tls connector"), + )), + tls_sni_override: args.tls_sni_override, + tls_verify_certificate: args.tls_verify_certificate, + tls_sni_disabled: args.tls_sni_disable, + tls_certificate_path: args.tls_certificate.clone(), + tls_key_path: args.tls_private_key.clone(), + }), + TransportScheme::Https => Some(TlsClientConfig { tls_connector: Arc::new(RwLock::new( tls::tls_connector( args.tls_verify_certificate, @@ -1136,6 +1176,26 @@ async fn main() -> anyhow::Result<()> { restriction_cfg }; + let http_proxy = if let Some(proxy) = args.http_proxy { + let mut proxy = if proxy.starts_with("http://") { + Url::parse(&proxy).expect("Invalid http proxy url") + } else { + Url::parse(&format!("http://{}", proxy)).expect("Invalid http proxy url") + }; + + if let Some(login) = args.http_proxy_login { + proxy.set_username(login.as_str()).expect("Cannot set http proxy login"); + } + if let Some(password) = args.http_proxy_password { + proxy + .set_password(Some(password.as_str())) + .expect("Cannot set http proxy password"); + } + Some(proxy) + } else { + None + }; + let server_config = WsServerConfig { socket_so_mark: args.socket_so_mark, bind: args.remote_addr.socket_addrs(|| Some(8080)).unwrap()[0], @@ -1151,6 +1211,7 @@ async fn main() -> anyhow::Result<()> { ) .expect("Cannot create DNS resolver"), restriction_config: args.restrict_config, + http_proxy, }; let server = WsServer::new(server_config); diff --git a/src/tunnel/connectors/mod.rs b/src/tunnel/connectors/mod.rs index f129f41d..b4162b50 100644 --- a/src/tunnel/connectors/mod.rs +++ b/src/tunnel/connectors/mod.rs @@ -1,17 +1,24 @@ -mod sock5; -mod tcp; -mod udp; +use tokio::io::{AsyncRead, AsyncWrite}; +use url::Url; pub use sock5::Socks5TunnelConnector; pub use tcp::TcpTunnelConnector; pub use udp::UdpTunnelConnector; use crate::tunnel::RemoteAddr; -use tokio::io::{AsyncRead, AsyncWrite}; + +mod sock5; +mod tcp; +mod udp; pub trait TunnelConnector { type Reader: AsyncRead + Send + 'static; type Writer: AsyncWrite + Send + 'static; async fn connect(&self, remote: &Option) -> anyhow::Result<(Self::Reader, Self::Writer)>; + async fn connect_with_http_proxy( + &self, + proxy: &Url, + remote: &Option, + ) -> anyhow::Result<(Self::Reader, Self::Writer)>; } diff --git a/src/tunnel/connectors/sock5.rs b/src/tunnel/connectors/sock5.rs index 694f0a4b..66003827 100644 --- a/src/tunnel/connectors/sock5.rs +++ b/src/tunnel/connectors/sock5.rs @@ -6,6 +6,7 @@ use std::time::Duration; use anyhow::anyhow; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; +use url::Url; use crate::protocols::dns::DnsResolver; use crate::protocols::udp; @@ -61,6 +62,14 @@ impl TunnelConnector for Socks5TunnelConnector<'_> { _ => Err(anyhow!("Invalid protocol for reverse socks5 {:?}", remote.protocol)), } } + + async fn connect_with_http_proxy( + &self, + proxy: &Url, + remote: &Option, + ) -> anyhow::Result<(Self::Reader, Self::Writer)> { + Err(anyhow!("SOCKS5 tunneling is not supported with HTTP proxy")) + } } pub enum Socks5Reader { diff --git a/src/tunnel/connectors/tcp.rs b/src/tunnel/connectors/tcp.rs index 96a07aa7..7623f3dc 100644 --- a/src/tunnel/connectors/tcp.rs +++ b/src/tunnel/connectors/tcp.rs @@ -1,10 +1,12 @@ +use std::time::Duration; + +use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; +use url::{Host, Url}; + use crate::protocols; use crate::protocols::dns::DnsResolver; use crate::tunnel::connectors::TunnelConnector; use crate::tunnel::RemoteAddr; -use std::time::Duration; -use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; -use url::Host; pub struct TcpTunnelConnector<'a> { host: &'a Host, @@ -45,4 +47,26 @@ impl TunnelConnector for TcpTunnelConnector<'_> { let stream = protocols::tcp::connect(host, port, self.so_mark, self.connect_timeout, self.dns_resolver).await?; Ok(stream.into_split()) } + + async fn connect_with_http_proxy( + &self, + proxy: &Url, + remote: &Option, + ) -> anyhow::Result<(Self::Reader, Self::Writer)> { + let (host, port) = match remote { + Some(remote) => (&remote.host, remote.port), + None => (self.host, self.port), + }; + + let stream = protocols::tcp::connect_with_http_proxy( + proxy, + host, + port, + self.so_mark, + self.connect_timeout, + self.dns_resolver, + ) + .await?; + Ok(stream.into_split()) + } } diff --git a/src/tunnel/connectors/udp.rs b/src/tunnel/connectors/udp.rs index 341dc918..6d832311 100644 --- a/src/tunnel/connectors/udp.rs +++ b/src/tunnel/connectors/udp.rs @@ -1,10 +1,13 @@ +use std::time::Duration; + +use anyhow::anyhow; +use url::{Host, Url}; + use crate::protocols; use crate::protocols::dns::DnsResolver; use crate::protocols::udp::WsUdpSocket; use crate::tunnel::connectors::TunnelConnector; use crate::tunnel::RemoteAddr; -use std::time::Duration; -use url::Host; pub struct UdpTunnelConnector<'a> { host: &'a Host, @@ -43,4 +46,12 @@ impl TunnelConnector for UdpTunnelConnector<'_> { Ok((stream.clone(), stream)) } + + async fn connect_with_http_proxy( + &self, + proxy: &Url, + remote: &Option, + ) -> anyhow::Result<(Self::Reader, Self::Writer)> { + Err(anyhow!("UDP tunneling is not supported with HTTP proxy")) + } } diff --git a/src/tunnel/server/server.rs b/src/tunnel/server/server.rs index d4b8986f..c4edd50b 100644 --- a/src/tunnel/server/server.rs +++ b/src/tunnel/server/server.rs @@ -48,7 +48,7 @@ use tokio::sync::mpsc; use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; use tokio_rustls::TlsAcceptor; use tracing::{error, info, span, warn, Instrument, Level, Span}; -use url::Host; +use url::{Host, Url}; #[derive(Debug)] pub struct TlsServerConfig { @@ -69,6 +69,7 @@ pub struct WsServerConfig { pub tls: Option, pub dns_resolver: DnsResolver, pub restriction_config: Option, + pub http_proxy: Option, } #[derive(Clone)] @@ -172,28 +173,32 @@ impl WsServer { ) -> anyhow::Result<(RemoteAddr, Pin>, Pin>)> { match remote.protocol { LocalProtocol::Udp { timeout, .. } => { - let (rx, tx) = UdpTunnelConnector::new( + let connector = UdpTunnelConnector::new( &remote.host, remote.port, self.config.socket_so_mark, timeout.unwrap_or(Duration::from_secs(10)), &self.config.dns_resolver, - ) - .connect(&None) - .await?; + ); + let (rx, tx) = match &self.config.http_proxy { + None => connector.connect(&None).await?, + Some(_) => Err(anyhow!("UDP tunneling is not supported with HTTP proxy"))?, + }; Ok((remote, Box::pin(rx), Box::pin(tx))) } LocalProtocol::Tcp { proxy_protocol } => { - let (rx, mut tx) = TcpTunnelConnector::new( + let connector = TcpTunnelConnector::new( &remote.host, remote.port, self.config.socket_so_mark, Duration::from_secs(10), &self.config.dns_resolver, - ) - .connect(&None) - .await?; + ); + let (rx, mut tx) = match &self.config.http_proxy { + None => connector.connect(&None).await?, + Some(proxy_url) => connector.connect_with_http_proxy(proxy_url, &None).await?, + }; if proxy_protocol { let header = ppp::v2::Builder::with_addresses( @@ -201,8 +206,8 @@ impl WsServer { ppp::v2::Protocol::Stream, (client_address, tx.local_addr().unwrap()), ) - .build() - .unwrap(); + .build() + .unwrap(); let _ = tx.write_all(&header).await; }