From 8b7dbc8a6bf9db0342a18fe7037a7e9c779fa438 Mon Sep 17 00:00:00 2001 From: zonyitoo Date: Mon, 4 May 2020 15:26:16 +0800 Subject: [PATCH] Support HTTPS proxy targets with both native and rustls --- Cargo.toml | 10 ++- src/relay/socks5.rs | 8 ++ src/relay/tcprelay/http_local.rs | 144 +++++++++++++++++++++++++++++-- 3 files changed, 152 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d8464771..2fb7b0de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ incremental = false panic = "abort" [features] -default = ["sodium", "rc4", "aes-cfb", "aes-ctr", "trust-dns", "local-http"] +default = ["sodium", "rc4", "aes-cfb", "aes-ctr", "trust-dns", "local-http", "local-http-rustls"] sodium = ["libsodium-sys"] rc4 = ["openssl"] aes-cfb = ["openssl"] @@ -46,10 +46,12 @@ single-threaded = [] trust-dns = ["trust-dns-resolver"] dns-over-tls = ["trust-dns-resolver/dns-over-rustls"] dns-over-https = ["trust-dns-resolver/dns-over-https-rustls"] -openssl-vendored = ["openssl/vendored"] +openssl-vendored = ["openssl/vendored", "native-tls/vendored"] local-dns-relay = [] local-flow-stat = [] local-http = ["hyper", "http", "tower"] +local-http-native-tls = ["tokio-tls", "native-tls"] +local-http-rustls = ["tokio-rustls", "webpki-roots"] local-redir = [] [dependencies] @@ -61,6 +63,10 @@ chrono = "0.4" openssl = { version = "0.10", optional = true } libc = "^0.2.68" tokio = { version = "^0.2.7", features = ["net", "signal", "time", "sync", "process", "rt-threaded", "rt-core", "stream", "io-util"] } +tokio-tls = { version = "0.3", optional = true } +native-tls = { version = "0.2", optional = true } +tokio-rustls = { version = "0.13", optional = true } +webpki-roots = { version = "0.19", optional = true } futures = "0.3" json5 = "0.2" base64 = "0.12" diff --git a/src/relay/socks5.rs b/src/relay/socks5.rs index 4a2885fc..7c2d07f5 100644 --- a/src/relay/socks5.rs +++ b/src/relay/socks5.rs @@ -317,6 +317,14 @@ impl Address { Address::DomainNameAddress(.., port) => port, } } + + /// Get host address string + pub fn host(&self) -> String { + match *self { + Address::SocketAddress(ref addr) => addr.ip().to_string(), + Address::DomainNameAddress(ref domain, ..) => domain.to_owned(), + } + } } impl Debug for Address { diff --git a/src/relay/tcprelay/http_local.rs b/src/relay/tcprelay/http_local.rs index 2ae95b3a..21a18e85 100644 --- a/src/relay/tcprelay/http_local.rs +++ b/src/relay/tcprelay/http_local.rs @@ -36,6 +36,7 @@ use hyper::{ }; use log::{debug, error, info, trace}; use pin_project::pin_project; +use tokio::io::{AsyncRead, AsyncWrite}; use crate::{ context::SharedContext, @@ -53,6 +54,108 @@ use crate::{ use super::ProxyStream; +#[pin_project] +enum ProxyHttpStream { + Http(#[pin] ProxyStream), + #[cfg(feature = "local-http-native-tls")] + Https(#[pin] tokio_tls::TlsStream), + #[cfg(feature = "local-http-rustls")] + Https(#[pin] tokio_rustls::client::TlsStream), +} + +impl ProxyHttpStream { + fn connect_http(stream: ProxyStream) -> ProxyHttpStream { + ProxyHttpStream::Http(stream) + } + + #[cfg(feature = "local-http-native-tls")] + async fn connect_https(stream: ProxyStream, domain: &str) -> io::Result { + use native_tls::TlsConnector; + + let cx = match TlsConnector::builder().build() { + Ok(c) => c, + Err(err) => { + return Err(io::Error::new(ErrorKind::Other, format!("tls build: {}", err))); + } + }; + let cx = tokio_tls::TlsConnector::from(cx); + + cx.connect(domain, stream) + .await + .map(ProxyHttpStream::Https) + .map_err(|err| io::Error::new(ErrorKind::Other, format!("tls connect: {}", err))) + } + + #[cfg(feature = "local-http-rustls")] + async fn connect_https(stream: ProxyStream, domain: &str) -> io::Result { + use lazy_static::lazy_static; + use tokio_rustls::{rustls::ClientConfig, webpki::DNSNameRef, TlsConnector}; + + lazy_static! { + static ref TLS_CONFIG: Arc = { + let mut config = ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + Arc::new(config) + }; + } + + let connector = TlsConnector::from(TLS_CONFIG.clone()); + + let host = match DNSNameRef::try_from_ascii_str(domain) { + Ok(n) => n, + Err(_) => { + return Err(io::Error::new(ErrorKind::InvalidInput, "invalid dnsname")); + } + }; + + connector.connect(host, stream).await.map(ProxyHttpStream::Https) + } + + #[cfg(not(any(feature = "local-http-native-tls", feature = "local-http-rustls")))] + async fn connect_https(stream: ProxyStream, domain: &str) -> io::Result { + let err = io::Error::new( + ErrorKind::Other, + "https is not supported, consider enable it by feature \"local-http-native-tls\" or \"local-http-rustls\"", + ); + Err(err) + } +} + +macro_rules! forward_call { + ($self:expr, $method:ident $(, $param:expr)*) => { + // #[project] + match $self.as_mut().project() { + // ProxyHttpStream::Http(stream) => stream.$method($($param),*), + __ProxyHttpStreamProjection::Http(stream) => stream.$method($($param),*), + #[cfg(any(feature = "local-http-native-tls", feature = "local-http-rustls"))] + // ProxyHttpStream::Https(stream) => stream.$method($($param),*), + __ProxyHttpStreamProjection::Https(stream) => stream.$method($($param),*), + } + }; +} + +impl AsyncRead for ProxyHttpStream { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &mut [u8]) -> Poll> { + forward_call!(self, poll_read, cx, buf) + } +} + +impl AsyncWrite for ProxyHttpStream { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { + forward_call!(self, poll_write, cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + forward_call!(self, poll_flush, cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + forward_call!(self, poll_shutdown, cx) + } +} + #[derive(Clone)] struct ShadowSocksConnector { context: SharedContext, @@ -69,7 +172,7 @@ impl ShadowSocksConnector { impl tower::Service for ShadowSocksConnector { type Error = io::Error; type Future = ShadowSocksConnecting; - type Response = ProxyStream; + type Response = ProxyHttpStream; fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -83,6 +186,7 @@ impl tower::Service for ShadowSocksConnector { ShadowSocksConnecting { fut: async move { let svr_cfg = context.server_config(svr_idx); + let is_https = dst.scheme_str() == Some("https"); match host_addr(&dst) { None => { @@ -95,7 +199,14 @@ impl tower::Service for ShadowSocksConnector { } Some(addr) => { match ProxyStream::connect_proxied(context.clone(), svr_cfg, &addr).await { - Ok(s) => Ok(s), + Ok(s) => { + if is_https { + let host = dst.host().unwrap().trim_start_matches('[').trim_start_matches(']'); + ProxyHttpStream::connect_https(s, host).await + } else { + Ok(ProxyHttpStream::connect_http(s)) + } + } Err(err) => { // Report failure to global statistic stat.report_failure().await; @@ -113,11 +224,11 @@ impl tower::Service for ShadowSocksConnector { #[pin_project] struct ShadowSocksConnecting { #[pin] - fut: BoxFuture<'static, io::Result>, + fut: BoxFuture<'static, io::Result>, } impl Future for ShadowSocksConnecting { - type Output = io::Result; + type Output = io::Result; fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { self.project().fut.poll(cx) @@ -138,7 +249,7 @@ impl DirectConnector { impl tower::Service for DirectConnector { type Error = io::Error; type Future = DirectConnecting; - type Response = ProxyStream; + type Response = ProxyHttpStream; fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -149,6 +260,8 @@ impl tower::Service for DirectConnector { DirectConnecting { fut: async move { + let is_https = dst.scheme_str() == Some("https"); + match host_addr(&dst) { None => { use std::io::Error; @@ -158,7 +271,16 @@ impl tower::Service for DirectConnector { let err = Error::new(ErrorKind::Other, "URI must be a valid Address"); Err(err) } - Some(addr) => ProxyStream::connect_direct(context, &addr).await, + Some(addr) => { + let s = ProxyStream::connect_direct(context, &addr).await?; + + if is_https { + let host = dst.host().unwrap().trim_start_matches('[').trim_start_matches(']'); + ProxyHttpStream::connect_https(s, host).await + } else { + Ok(ProxyHttpStream::connect_http(s)) + } + } } } .boxed(), @@ -169,11 +291,11 @@ impl tower::Service for DirectConnector { #[pin_project] struct DirectConnecting { #[pin] - fut: BoxFuture<'static, io::Result>, + fut: BoxFuture<'static, io::Result>, } impl Future for DirectConnecting { - type Output = io::Result; + type Output = io::Result; fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { self.project().fut.poll(cx) @@ -359,6 +481,12 @@ impl Connection for ProxyStream { } } +impl Connection for ProxyHttpStream { + fn connected(&self) -> Connected { + Connected::new() + } +} + type ShadowSocksHttpClient = Client; type DirectHttpClient = Client;