summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2023-08-08 16:12:57 +0200
committerDavid Lönnhager <david.l@mullvad.net>2023-10-09 14:39:54 +0200
commit2796dda1376e0d7b78fc274ada988a8a371101a2 (patch)
treea45b97d306f21ab78e459e9d7d4ee53c47a40a25
parent524a64d100693f3dc3c4b77c5c3b9d478dd6a4b9 (diff)
downloadmullvadvpn-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.lock13
-rw-r--r--mullvad-api/Cargo.toml1
-rw-r--r--mullvad-api/src/https_client_with_sni.rs189
-rw-r--r--mullvad-api/src/proxy.rs11
-rw-r--r--talpid-types/src/net/openvpn.rs15
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] = [