diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2023-08-08 16:12:57 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2023-10-09 14:39:54 +0200 |
| commit | 2796dda1376e0d7b78fc274ada988a8a371101a2 (patch) | |
| tree | a45b97d306f21ab78e459e9d7d4ee53c47a40a25 | |
| parent | 524a64d100693f3dc3c4b77c5c3b9d478dd6a4b9 (diff) | |
| download | mullvadvpn-2796dda1376e0d7b78fc274ada988a8a371101a2.tar.xz mullvadvpn-2796dda1376e0d7b78fc274ada988a8a371101a2.zip | |
Add `Socks5` as a Proxy setting
- Rename `InnerConnectionMode` variant `Proxied` to `Shadowsocks`
- Move proxy/socket connection logic to `InnerConnectMode` impl block
- Move `handle_x_connection` functions to `InnerConnectionMode` impl block
- These functions does not need to be visible in the entire module, really.
- Refactor some code into standalone functions
- Mostly for visibilities' sake, but it also helps `rustc` with inferring
the return type of each match arm inside of `stream_fut`.
| -rw-r--r-- | Cargo.lock | 13 | ||||
| -rw-r--r-- | mullvad-api/Cargo.toml | 1 | ||||
| -rw-r--r-- | mullvad-api/src/https_client_with_sni.rs | 189 | ||||
| -rw-r--r-- | mullvad-api/src/proxy.rs | 11 | ||||
| -rw-r--r-- | talpid-types/src/net/openvpn.rs | 15 |
5 files changed, 170 insertions, 59 deletions
diff --git a/Cargo.lock b/Cargo.lock index 6ba669fe5e..b3b830e0ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1805,6 +1805,7 @@ dependencies = [ "talpid-types", "tokio", "tokio-rustls", + "tokio-socks", ] [[package]] @@ -3799,6 +3800,18 @@ dependencies = [ ] [[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + +[[package]] name = "tokio-stream" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/mullvad-api/Cargo.toml b/mullvad-api/Cargo.toml index 32d725a2c6..83d8bf0723 100644 --- a/mullvad-api/Cargo.toml +++ b/mullvad-api/Cargo.toml @@ -24,6 +24,7 @@ serde = "1" serde_json = "1.0" tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread", "net", "io-std", "io-util", "fs"] } tokio-rustls = "0.24.1" +tokio-socks = "0.5.1" rustls-pemfile = "1.0.3" once_cell = "1.13" diff --git a/mullvad-api/src/https_client_with_sni.rs b/mullvad-api/src/https_client_with_sni.rs index e8f7fb889c..940bb249d4 100644 --- a/mullvad-api/src/https_client_with_sni.rs +++ b/mullvad-api/src/https_client_with_sni.rs @@ -73,8 +73,112 @@ enum HttpsConnectorRequest { enum InnerConnectionMode { /// Connect directly to the target. Direct, - /// Connect to the destination via a proxy. - Proxied(ParsedShadowsocksConfig), + /// Connect to the destination via a Shadowsocks proxy. + Shadowsocks(ShadowsocksConfig), + /// Connect to the destination via a Socks proxy. + Socks5(SocksConfig), +} + +impl InnerConnectionMode { + async fn connect( + &self, + hostname: &str, + addr: &SocketAddr, + ) -> Result<ApiConnection, std::io::Error> { + use InnerConnectionMode::*; + match self { + Direct => Self::handle_direct_connection(addr, hostname).await, + Shadowsocks(config) => { + Self::handle_shadowsocks_connection(config.clone(), addr, hostname).await + } + Socks5(proxy_config) => { + Self::handle_socks_connection(proxy_config.clone(), addr, hostname).await + } + } + } + + /// Set up a TCP-socket connection. + async fn handle_direct_connection( + addr: &SocketAddr, + hostname: &str, + ) -> Result<ApiConnection, io::Error> { + let socket = HttpsConnectorWithSni::open_socket( + *addr, + #[cfg(target_os = "android")] + socket_bypass_tx.clone(), + ) + .await?; + #[cfg(feature = "api-override")] + if API.disable_tls { + return Ok(ApiConnection::new(Box::new(socket))); + } + + let tls_stream = TlsStream::connect_https(socket, hostname).await?; + Ok(ApiConnection::new(Box::new(tls_stream))) + } + + /// Set up a shadowsocks-socket connection. + async fn handle_shadowsocks_connection( + shadowsocks: ShadowsocksConfig, + addr: &SocketAddr, + hostname: &str, + ) -> Result<ApiConnection, io::Error> { + let socket = HttpsConnectorWithSni::open_socket( + shadowsocks.params.peer, + #[cfg(target_os = "android")] + socket_bypass_tx.clone(), + ) + .await?; + let proxy = ProxyClientStream::from_stream( + shadowsocks.proxy_context, + socket, + &ServerConfig::from(shadowsocks.params), + *addr, + ); + + #[cfg(feature = "api-override")] + if API.disable_tls { + return Ok(ApiConnection::new(Box::new(ConnectionDecorator(proxy)))); + } + + let tls_stream = TlsStream::connect_https(proxy, hostname).await?; + Ok(ApiConnection::new(Box::new(tls_stream))) + } + + /// Set up a SOCKS5-socket connection. + /// + /// TODO: Handle case where the proxy-address is `localhost`. + async fn handle_socks_connection( + proxy_config: SocksConfig, + addr: &SocketAddr, + hostname: &str, + ) -> Result<ApiConnection, io::Error> { + let socket = HttpsConnectorWithSni::open_socket( + proxy_config.peer, + #[cfg(target_os = "android")] + socket_bypass_tx.clone(), + ) + .await?; + let proxy = tokio_socks::tcp::Socks5Stream::connect_with_socket(socket, addr) + .await + .map_err(|error| { + io::Error::new(io::ErrorKind::Other, format!("SOCKS error: {error}")) + })?; + + #[cfg(feature = "api-override")] + if API.disable_tls { + return Ok(ApiConnection::new(Box::new(ConnectionDecorator(proxy)))); + } + + let tls_stream = TlsStream::connect_https(proxy, hostname).await?; + Ok(ApiConnection::new(Box::new(tls_stream))) + } +} + +#[derive(Clone)] +struct ShadowsocksConfig { + proxy_context: SharedContext, + params: ParsedShadowsocksConfig, } #[derive(Clone)] @@ -90,6 +194,11 @@ impl From<ParsedShadowsocksConfig> for ServerConfig { } } +#[derive(Clone)] +struct SocksConfig { + peer: SocketAddr, +} + #[derive(err_derive::Error, Debug)] enum ProxyConfigError { #[error(display = "Unrecognized cipher selected: {}", _0)] @@ -102,14 +211,22 @@ impl TryFrom<ApiConnectionMode> for InnerConnectionMode { fn try_from(config: ApiConnectionMode) -> Result<Self, Self::Error> { Ok(match config { ApiConnectionMode::Direct => InnerConnectionMode::Direct, - ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks(config)) => { - InnerConnectionMode::Proxied(ParsedShadowsocksConfig { - peer: config.peer, - password: config.password, - cipher: CipherKind::from_str(&config.cipher) - .map_err(|_| ProxyConfigError::InvalidCipher(config.cipher))?, - }) - } + ApiConnectionMode::Proxied(proxy_settings) => match proxy_settings { + ProxyConfig::Shadowsocks(config) => { + InnerConnectionMode::Shadowsocks(ShadowsocksConfig { + params: ParsedShadowsocksConfig { + peer: config.peer, + password: config.password, + cipher: CipherKind::from_str(&config.cipher) + .map_err(|_| ProxyConfigError::InvalidCipher(config.cipher))?, + }, + proxy_context: SsContext::new_shared(ServerType::Local), + }) + } + ProxyConfig::Socks(config) => { + InnerConnectionMode::Socks5(SocksConfig { peer: config.peer }) + } + }, }) } } @@ -121,7 +238,6 @@ pub struct HttpsConnectorWithSni { sni_hostname: Option<String>, address_cache: AddressCache, abort_notify: Arc<tokio::sync::Notify>, - proxy_context: SharedContext, #[cfg(target_os = "android")] socket_bypass_tx: Option<mpsc::Sender<SocketBypassRequest>>, } @@ -186,7 +302,6 @@ impl HttpsConnectorWithSni { sni_hostname, address_cache, abort_notify, - proxy_context: SsContext::new_shared(ServerType::Local), #[cfg(target_os = "android")] socket_bypass_tx, }, @@ -194,6 +309,9 @@ impl HttpsConnectorWithSni { ) } + /// Establishes a TCP connection with a peer at the specified socket address. + /// + /// Will timeout after [`CONNECT_TIMEOUT`] seconds. async fn open_socket( addr: SocketAddr, #[cfg(target_os = "android")] socket_bypass_tx: Option<mpsc::Sender<SocketBypassRequest>>, @@ -281,7 +399,6 @@ impl Service<Uri> for HttpsConnectorWithSni { }); let inner = self.inner.clone(); let abort_notify = self.abort_notify.clone(); - let proxy_context = self.proxy_context.clone(); #[cfg(target_os = "android")] let socket_bypass_tx = self.socket_bypass_tx.clone(); let address_cache = self.address_cache.clone(); @@ -301,50 +418,8 @@ impl Service<Uri> for HttpsConnectorWithSni { // is selected while connecting. let stream = loop { let notify = abort_notify.notified(); - let config = { inner.lock().unwrap().proxy_config.clone() }; - let stream_fut = async { - match config { - InnerConnectionMode::Direct => { - let socket = Self::open_socket( - addr, - #[cfg(target_os = "android")] - socket_bypass_tx.clone(), - ) - .await?; - #[cfg(feature = "api-override")] - if API.disable_tls { - return Ok::<_, io::Error>(ApiConnection::new(Box::new(socket))); - } - - let tls_stream = TlsStream::connect_https(socket, &hostname).await?; - Ok::<_, io::Error>(ApiConnection::new(Box::new(tls_stream))) - } - InnerConnectionMode::Proxied(proxy_config) => { - let socket = Self::open_socket( - proxy_config.peer, - #[cfg(target_os = "android")] - socket_bypass_tx.clone(), - ) - .await?; - let proxy = ProxyClientStream::from_stream( - proxy_context.clone(), - socket, - &ServerConfig::from(proxy_config), - addr, - ); - - #[cfg(feature = "api-override")] - if API.disable_tls { - return Ok(ApiConnection::new(Box::new(ConnectionDecorator( - proxy, - )))); - } - - let tls_stream = TlsStream::connect_https(proxy, &hostname).await?; - Ok(ApiConnection::new(Box::new(tls_stream))) - } - } - }; + let proxy_config = { inner.lock().unwrap().proxy_config.clone() }; + let stream_fut = proxy_config.connect(&hostname, &addr); pin_mut!(stream_fut); pin_mut!(notify); diff --git a/mullvad-api/src/proxy.rs b/mullvad-api/src/proxy.rs index 1e6ab41f80..c5c2426933 100644 --- a/mullvad-api/src/proxy.rs +++ b/mullvad-api/src/proxy.rs @@ -8,7 +8,9 @@ use std::{ pin::Pin, task::{self, Poll}, }; -use talpid_types::{net::openvpn::ShadowsocksProxySettings, ErrorExt}; +use talpid_types::{ + net::openvpn::ShadowsocksProxySettings, net::openvpn::SocksProxySettings, ErrorExt, +}; use tokio::{ fs, io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf}, @@ -36,6 +38,7 @@ impl fmt::Display for ApiConnectionMode { #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum ProxyConfig { Shadowsocks(ShadowsocksProxySettings), + Socks(SocksProxySettings), } impl fmt::Display for ProxyConfig { @@ -43,6 +46,7 @@ impl fmt::Display for ProxyConfig { match self { // TODO: Do not hardcode TCP ProxyConfig::Shadowsocks(ss) => write!(f, "Shadowsocks {}/TCP", ss.peer), + ProxyConfig::Socks(s) => write!(f, "Socks5 {}/TCP", s.peer), } } } @@ -110,8 +114,11 @@ impl ApiConnectionMode { /// Returns the remote address, or `None` for `ApiConnectionMode::Direct`. pub fn get_endpoint(&self) -> Option<SocketAddr> { match self { - ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks(ss)) => Some(ss.peer), ApiConnectionMode::Direct => None, + ApiConnectionMode::Proxied(proxy_config) => match proxy_config { + ProxyConfig::Shadowsocks(ss) => Some(ss.peer), + ProxyConfig::Socks(s) => Some(s.peer), + }, } } diff --git a/talpid-types/src/net/openvpn.rs b/talpid-types/src/net/openvpn.rs index 5968331b52..7dc64ca80c 100644 --- a/talpid-types/src/net/openvpn.rs +++ b/talpid-types/src/net/openvpn.rs @@ -132,6 +132,21 @@ impl ShadowsocksProxySettings { } } +/// Options for a bundled SOCKS5 proxy. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct SocksProxySettings { + pub peer: SocketAddr, +} + +impl SocksProxySettings { + pub fn get_endpoint(&self) -> Endpoint { + Endpoint { + address: self.peer, + protocol: TransportProtocol::Tcp, + } + } +} + /// List of ciphers usable by a Shadowsocks proxy. /// Cf. [`ShadowsocksProxySettings::cipher`]. pub const SHADOWSOCKS_CIPHERS: [&str; 19] = [ |
