summaryrefslogtreecommitdiffhomepage
path: root/mullvad-api
diff options
context:
space:
mode:
Diffstat (limited to 'mullvad-api')
-rw-r--r--mullvad-api/Cargo.toml1
-rw-r--r--mullvad-api/src/https_client_with_sni.rs240
-rw-r--r--mullvad-api/src/proxy.rs33
-rw-r--r--mullvad-api/src/rest.rs24
4 files changed, 229 insertions, 69 deletions
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..17d9f7f0d8 100644
--- a/mullvad-api/src/https_client_with_sni.rs
+++ b/mullvad-api/src/https_client_with_sni.rs
@@ -36,6 +36,7 @@ use std::{
use talpid_types::ErrorExt;
use tokio::{
+ io::{AsyncRead, AsyncWrite},
net::{TcpSocket, TcpStream},
time::timeout,
};
@@ -73,8 +74,131 @@ 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,
+ #[cfg(target_os = "android")] socket_bypass_tx: Option<mpsc::Sender<SocketBypassRequest>>,
+ ) -> Result<ApiConnection, std::io::Error> {
+ match self {
+ // Set up a TCP-socket connection.
+ InnerConnectionMode::Direct => {
+ let first_hop = *addr;
+ let make_proxy_stream = |tcp_stream| async { Ok(tcp_stream) };
+ Self::connect_proxied(
+ first_hop,
+ hostname,
+ make_proxy_stream,
+ #[cfg(target_os = "android")]
+ socket_bypass_tx,
+ )
+ .await
+ }
+ // Set up a Shadowsocks-connection.
+ InnerConnectionMode::Shadowsocks(shadowsocks) => {
+ let first_hop = shadowsocks.params.peer;
+ let make_proxy_stream = |tcp_stream| async {
+ Ok(ProxyClientStream::from_stream(
+ shadowsocks.proxy_context,
+ tcp_stream,
+ &ServerConfig::from(shadowsocks.params),
+ *addr,
+ ))
+ };
+ Self::connect_proxied(
+ first_hop,
+ hostname,
+ make_proxy_stream,
+ #[cfg(target_os = "android")]
+ socket_bypass_tx,
+ )
+ .await
+ }
+ // Set up a SOCKS5-connection.
+ InnerConnectionMode::Socks5(socks) => {
+ let first_hop = socks.peer;
+ let make_proxy_stream = |tcp_stream| async {
+ match socks.authentication {
+ SocksAuth::None => {
+ tokio_socks::tcp::Socks5Stream::connect_with_socket(tcp_stream, addr)
+ .await
+ }
+ SocksAuth::Password { username, password } => {
+ tokio_socks::tcp::Socks5Stream::connect_with_password_and_socket(
+ tcp_stream, addr, &username, &password,
+ )
+ .await
+ }
+ }
+ .map_err(|error| {
+ io::Error::new(io::ErrorKind::Other, format!("SOCKS error: {error}"))
+ })
+ };
+ Self::connect_proxied(
+ first_hop,
+ hostname,
+ make_proxy_stream,
+ #[cfg(target_os = "android")]
+ socket_bypass_tx,
+ )
+ .await
+ }
+ }
+ }
+
+ /// Create an [`ApiConnection`] from a [`TcpStream`].
+ ///
+ /// The `make_proxy_stream` closure receives a [`TcpStream`] and produces a
+ /// stream which can send to and receive data from some server using any
+ /// proxy protocol. The only restriction is that this stream must implement
+ /// [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`], as well as
+ /// [`Unpin`] and [`Send`].
+ ///
+ /// If a direct connection is to be established (i.e. the stream will not be
+ /// using any proxy protocol) `make_proxy_stream` may return the
+ /// [`TcpStream`] itself. See for example how a connection is established
+ /// from connection mode [`InnerConnectionMode::Direct`].
+ async fn connect_proxied<ProxyFactory, ProxyFuture, Proxy>(
+ first_hop: SocketAddr,
+ hostname: &str,
+ make_proxy_stream: ProxyFactory,
+ #[cfg(target_os = "android")] socket_bypass_tx: Option<mpsc::Sender<SocketBypassRequest>>,
+ ) -> Result<ApiConnection, io::Error>
+ where
+ ProxyFactory: FnOnce(TcpStream) -> ProxyFuture,
+ ProxyFuture: Future<Output = io::Result<Proxy>>,
+ Proxy: AsyncRead + AsyncWrite + Unpin + Send + 'static,
+ {
+ let socket = HttpsConnectorWithSni::open_socket(
+ first_hop,
+ #[cfg(target_os = "android")]
+ socket_bypass_tx,
+ )
+ .await?;
+
+ let proxy = make_proxy_stream(socket).await?;
+
+ #[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 +214,18 @@ impl From<ParsedShadowsocksConfig> for ServerConfig {
}
}
+#[derive(Clone)]
+struct SocksConfig {
+ peer: SocketAddr,
+ authentication: SocksAuth,
+}
+
+#[derive(Clone)]
+pub enum SocksAuth {
+ None,
+ Password { username: String, password: String },
+}
+
#[derive(err_derive::Error, Debug)]
enum ProxyConfigError {
#[error(display = "Unrecognized cipher selected: {}", _0)]
@@ -100,16 +236,43 @@ impl TryFrom<ApiConnectionMode> for InnerConnectionMode {
type Error = ProxyConfigError;
fn try_from(config: ApiConnectionMode) -> Result<Self, Self::Error> {
+ use mullvad_types::access_method;
+ use std::net::Ipv4Addr;
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) => match config {
+ access_method::Socks5::Local(config) => {
+ InnerConnectionMode::Socks5(SocksConfig {
+ peer: SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), config.port),
+ authentication: SocksAuth::None,
+ })
+ }
+ access_method::Socks5::Remote(config) => {
+ let authentication = match config.authentication {
+ Some(access_method::SocksAuth { username, password }) => {
+ SocksAuth::Password { username, password }
+ }
+ None => SocksAuth::None,
+ };
+ InnerConnectionMode::Socks5(SocksConfig {
+ peer: config.peer,
+ authentication,
+ })
+ }
+ },
+ },
})
}
}
@@ -121,7 +284,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 +348,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 +355,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 +445,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 +464,13 @@ 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,
+ #[cfg(target_os = "android")]
+ socket_bypass_tx.clone(),
+ );
pin_mut!(stream_fut);
pin_mut!(notify);
diff --git a/mullvad-api/src/proxy.rs b/mullvad-api/src/proxy.rs
index 1e6ab41f80..44a2309587 100644
--- a/mullvad-api/src/proxy.rs
+++ b/mullvad-api/src/proxy.rs
@@ -1,5 +1,6 @@
use futures::Stream;
use hyper::client::connect::Connected;
+use mullvad_types::access_method;
use serde::{Deserialize, Serialize};
use std::{
fmt, io,
@@ -8,7 +9,7 @@ use std::{
pin::Pin,
task::{self, Poll},
};
-use talpid_types::{net::openvpn::ShadowsocksProxySettings, ErrorExt};
+use talpid_types::ErrorExt;
use tokio::{
fs,
io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf},
@@ -16,7 +17,7 @@ use tokio::{
const CURRENT_CONFIG_FILENAME: &str = "api-endpoint.json";
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum ApiConnectionMode {
/// Connect directly to the target.
Direct,
@@ -33,9 +34,23 @@ impl fmt::Display for ApiConnectionMode {
}
}
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum ProxyConfig {
- Shadowsocks(ShadowsocksProxySettings),
+ Shadowsocks(access_method::Shadowsocks),
+ Socks(access_method::Socks5),
+}
+
+impl ProxyConfig {
+ /// Returns the remote address to reach the proxy.
+ fn get_endpoint(&self) -> SocketAddr {
+ match self {
+ ProxyConfig::Shadowsocks(ss) => ss.peer,
+ ProxyConfig::Socks(socks) => match socks {
+ access_method::Socks5::Local(s) => s.peer,
+ access_method::Socks5::Remote(s) => s.peer,
+ },
+ }
+ }
}
impl fmt::Display for ProxyConfig {
@@ -43,6 +58,12 @@ impl fmt::Display for ProxyConfig {
match self {
// TODO: Do not hardcode TCP
ProxyConfig::Shadowsocks(ss) => write!(f, "Shadowsocks {}/TCP", ss.peer),
+ ProxyConfig::Socks(socks) => match socks {
+ access_method::Socks5::Local(s) => {
+ write!(f, "Socks5 {}/TCP via localhost:{}", s.peer, s.port)
+ }
+ access_method::Socks5::Remote(s) => write!(f, "Socks5 {}/TCP", s.peer),
+ },
}
}
}
@@ -107,11 +128,11 @@ impl ApiConnectionMode {
}
}
- /// Returns the remote address, or `None` for `ApiConnectionMode::Direct`.
+ /// Returns the remote address required to reach the API, 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) => Some(proxy_config.get_endpoint()),
}
}
diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs
index 0fc31353a7..674bcf8c4e 100644
--- a/mullvad-api/src/rest.rs
+++ b/mullvad-api/src/rest.rs
@@ -77,6 +77,10 @@ pub enum Error {
/// The string given was not a valid URI.
#[error(display = "Not a valid URI")]
UriError(#[error(source)] http::uri::InvalidUri),
+
+ /// A new API config was requested, but the request could not be completed.
+ #[error(display = "Failed to rotate API config")]
+ NextApiConfigError,
}
impl Error {
@@ -207,7 +211,9 @@ impl<
if err.is_network_error() && !api_availability.get_state().is_offline() {
log::error!("{}", err.display_chain_with_msg("HTTP request failed"));
if let Some(tx) = tx {
- let _ = tx.unbounded_send(RequestCommand::NextApiConfig);
+ let (completion_tx, _completion_rx) = oneshot::channel();
+ let _ =
+ tx.unbounded_send(RequestCommand::NextApiConfig(completion_tx));
}
}
}
@@ -223,10 +229,11 @@ impl<
RequestCommand::Reset => {
self.connector_handle.reset();
}
- RequestCommand::NextApiConfig => {
+ RequestCommand::NextApiConfig(completion_tx) => {
#[cfg(feature = "api-override")]
if API.force_direct_connection {
log::debug!("Ignoring API connection mode");
+ let _ = completion_tx.send(Ok(()));
return;
}
@@ -240,6 +247,8 @@ impl<
self.connector_handle.set_connection_mode(new_config);
}
}
+
+ let _ = completion_tx.send(Ok(()));
}
}
}
@@ -274,10 +283,13 @@ impl RequestServiceHandle {
}
/// Forcibly update the connection mode.
- pub fn next_api_endpoint(&self) -> Result<()> {
+ pub async fn next_api_endpoint(&self) -> Result<()> {
+ let (completion_tx, completion_rx) = oneshot::channel();
self.tx
- .unbounded_send(RequestCommand::NextApiConfig)
- .map_err(|_| Error::SendError)
+ .unbounded_send(RequestCommand::NextApiConfig(completion_tx))
+ .map_err(|_| Error::SendError)?;
+
+ completion_rx.await.map_err(|_| Error::NextApiConfigError)?
}
}
@@ -288,7 +300,7 @@ pub(crate) enum RequestCommand {
oneshot::Sender<std::result::Result<Response, Error>>,
),
Reset,
- NextApiConfig,
+ NextApiConfig(oneshot::Sender<std::result::Result<(), Error>>),
}
/// A REST request that is sent to the RequestService to be executed.