summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2023-10-09 14:41:30 +0200
committerDavid Lönnhager <david.l@mullvad.net>2023-10-09 14:41:30 +0200
commitbb2caa8fa93b9272817d22c08ed6f4e5371a35ac (patch)
tree22620c408bd2e0e01c96329c4b4b7838b4939628
parent524a64d100693f3dc3c4b77c5c3b9d478dd6a4b9 (diff)
parentfbefa3f172cf324de8790a383ceb3f5329e94c7c (diff)
downloadmullvadvpn-bb2caa8fa93b9272817d22c08ed6f4e5371a35ac.tar.xz
mullvadvpn-bb2caa8fa93b9272817d22c08ed6f4e5371a35ac.zip
Merge branch 'revamp-api-access-methods' into main
-rw-r--r--Cargo.lock13
-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
-rw-r--r--mullvad-cli/src/cmds/api_access.rs599
-rw-r--r--mullvad-cli/src/cmds/mod.rs1
-rw-r--r--mullvad-cli/src/main.rs18
-rw-r--r--mullvad-daemon/src/access_method.rs209
-rw-r--r--mullvad-daemon/src/api.rs185
-rw-r--r--mullvad-daemon/src/lib.rs126
-rw-r--r--mullvad-daemon/src/management_interface.rs92
-rw-r--r--mullvad-management-interface/proto/management_interface.proto60
-rw-r--r--mullvad-management-interface/src/client.rs107
-rw-r--r--mullvad-management-interface/src/lib.rs6
-rw-r--r--mullvad-management-interface/src/types/conversions/access_method.rs289
-rw-r--r--mullvad-management-interface/src/types/conversions/mod.rs1
-rw-r--r--mullvad-management-interface/src/types/conversions/net.rs21
-rw-r--r--mullvad-management-interface/src/types/conversions/settings.rs12
-rw-r--r--mullvad-types/src/access_method.rs345
-rw-r--r--mullvad-types/src/lib.rs1
-rw-r--r--mullvad-types/src/settings/mod.rs5
22 files changed, 2275 insertions, 113 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..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.
diff --git a/mullvad-cli/src/cmds/api_access.rs b/mullvad-cli/src/cmds/api_access.rs
new file mode 100644
index 0000000000..a03d3ba11f
--- /dev/null
+++ b/mullvad-cli/src/cmds/api_access.rs
@@ -0,0 +1,599 @@
+use anyhow::{anyhow, Result};
+use mullvad_management_interface::MullvadProxyClient;
+use mullvad_types::access_method::{AccessMethod, AccessMethodSetting, CustomAccessMethod};
+use std::net::IpAddr;
+
+use clap::{Args, Subcommand};
+use talpid_types::net::openvpn::SHADOWSOCKS_CIPHERS;
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum ApiAccess {
+ /// Display the current API access method.
+ Get,
+ /// Add a custom API access method
+ #[clap(subcommand)]
+ Add(AddCustomCommands),
+ /// Lists all API access methods
+ ///
+ /// * = Enabled
+ List,
+ /// Edit a custom API access method
+ Edit(EditCustomCommands),
+ /// Remove a custom API access method
+ Remove(SelectItem),
+ /// Enable an API access method
+ Enable(SelectItem),
+ /// Disable an API access method
+ Disable(SelectItem),
+ /// Try to use a specific API access method (If the API is unreachable, reverts back to the previous access method)
+ ///
+ /// Selecting "Direct" will connect to the Mullvad API without going through any proxy. This connection use https and is therefore encrypted.
+ ///
+ /// Selecting "Mullvad Bridges" respects your current bridge settings
+ Use(SelectItem),
+ /// Try to reach the Mullvad API using a specific access method
+ Test(SelectItem),
+}
+
+impl ApiAccess {
+ pub async fn handle(self) -> Result<()> {
+ match self {
+ ApiAccess::List => {
+ Self::list().await?;
+ }
+ ApiAccess::Add(cmd) => {
+ Self::add(cmd).await?;
+ }
+ ApiAccess::Edit(cmd) => Self::edit(cmd).await?,
+ ApiAccess::Remove(cmd) => Self::remove(cmd).await?,
+ ApiAccess::Enable(cmd) => {
+ Self::enable(cmd).await?;
+ }
+ ApiAccess::Disable(cmd) => {
+ Self::disable(cmd).await?;
+ }
+ ApiAccess::Test(cmd) => {
+ Self::test(cmd).await?;
+ }
+ ApiAccess::Use(cmd) => {
+ Self::set(cmd).await?;
+ }
+ ApiAccess::Get => {
+ Self::get().await?;
+ }
+ };
+ Ok(())
+ }
+
+ /// Show all API access methods.
+ async fn list() -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ for (index, api_access_method) in rpc.get_api_access_methods().await?.iter().enumerate() {
+ println!(
+ "{}. {}",
+ index + 1,
+ pp::ApiAccessMethodFormatter::new(api_access_method)
+ );
+ }
+ Ok(())
+ }
+
+ /// Add a custom API access method.
+ async fn add(cmd: AddCustomCommands) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let name = cmd.name().to_string();
+ let enabled = cmd.enabled();
+ let access_method = AccessMethod::try_from(cmd)?;
+ rpc.add_access_method(name, enabled, access_method).await?;
+ Ok(())
+ }
+
+ /// Remove an API access method.
+ async fn remove(cmd: SelectItem) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let access_method = Self::get_access_method(&mut rpc, &cmd).await?;
+ rpc.remove_access_method(access_method.get_id())
+ .await
+ .map_err(Into::<anyhow::Error>::into)
+ }
+
+ /// Edit the data of an API access method.
+ async fn edit(cmd: EditCustomCommands) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let mut api_access_method = Self::get_access_method(&mut rpc, &cmd.item).await?;
+
+ // Create a new access method combining the new params with the previous values
+ let access_method = match api_access_method.as_custom() {
+ None => return Err(anyhow!("Can not edit built-in access method")),
+ Some(x) => match x.clone() {
+ CustomAccessMethod::Shadowsocks(shadowsocks) => {
+ let ip = cmd.params.ip.unwrap_or(shadowsocks.peer.ip()).to_string();
+ let port = cmd.params.port.unwrap_or(shadowsocks.peer.port());
+ let password = cmd.params.password.unwrap_or(shadowsocks.password);
+ let cipher = cmd.params.cipher.unwrap_or(shadowsocks.cipher);
+ mullvad_types::access_method::Shadowsocks::from_args(ip, port, cipher, password)
+ .map(AccessMethod::from)
+ }
+ CustomAccessMethod::Socks5(socks) => match socks {
+ mullvad_types::access_method::Socks5::Local(local) => {
+ let ip = cmd.params.ip.unwrap_or(local.peer.ip()).to_string();
+ let port = cmd.params.port.unwrap_or(local.peer.port());
+ let local_port = cmd.params.local_port.unwrap_or(local.port);
+ mullvad_types::access_method::Socks5Local::from_args(ip, port, local_port)
+ .map(AccessMethod::from)
+ }
+ mullvad_types::access_method::Socks5::Remote(remote) => {
+ let ip = cmd.params.ip.unwrap_or(remote.peer.ip()).to_string();
+ let port = cmd.params.port.unwrap_or(remote.peer.port());
+ match remote.authentication {
+ None => mullvad_types::access_method::Socks5Remote::from_args(ip, port),
+ Some(mullvad_types::access_method::SocksAuth {
+ username,
+ password,
+ }) => {
+ let username = cmd.params.username.unwrap_or(username);
+ let password = cmd.params.password.unwrap_or(password);
+ mullvad_types::access_method::Socks5Remote::from_args_with_password(
+ ip, port, username, password,
+ )
+ }
+ }
+ .map(AccessMethod::from)
+ }
+ },
+ },
+ };
+
+ if let Some(name) = cmd.params.name {
+ api_access_method.name = name;
+ };
+ if let Some(access_method) = access_method {
+ api_access_method.access_method = access_method;
+ }
+
+ rpc.update_access_method(api_access_method).await?;
+
+ Ok(())
+ }
+
+ /// Enable a custom API access method.
+ async fn enable(item: SelectItem) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let mut access_method = Self::get_access_method(&mut rpc, &item).await?;
+ access_method.enable();
+ rpc.update_access_method(access_method).await?;
+ Ok(())
+ }
+
+ /// Disable a custom API access method.
+ async fn disable(item: SelectItem) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let mut access_method = Self::get_access_method(&mut rpc, &item).await?;
+ access_method.disable();
+ rpc.update_access_method(access_method).await?;
+ Ok(())
+ }
+
+ /// Test an access method to see if it successfully reaches the Mullvad API.
+ async fn test(item: SelectItem) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ // Retrieve the currently used access method. We will reset to this
+ // after we are done testing.
+ let previous_access_method = rpc.get_current_api_access_method().await?;
+ let access_method = Self::get_access_method(&mut rpc, &item).await?;
+
+ println!("Testing access method \"{}\"", access_method.name);
+ rpc.set_access_method(access_method.get_id()).await?;
+ // Make the daemon perform an network request which involves talking to the Mullvad API.
+ let result = match rpc.get_api_addresses().await {
+ Ok(_) => {
+ println!("Success!");
+ Ok(())
+ }
+ Err(_) => Err(anyhow!("Could not reach the Mullvad API")),
+ };
+ // In any case, switch back to the previous access method.
+ rpc.set_access_method(previous_access_method.get_id())
+ .await?;
+ result
+ }
+
+ /// Try to use of a specific [`AccessMethodSetting`] for subsequent calls to
+ /// the Mullvad API.
+ ///
+ /// First, a test will be performed to check that the new
+ /// [`AccessMethodSetting`] is able to reach the API. If it can, the daemon
+ /// will set this [`AccessMethodSetting`] to be used by the API runtime.
+ ///
+ /// If the new [`AccessMethodSetting`] fails, the daemon will perform a
+ /// roll-back to the previously used [`AccessMethodSetting`]. If that never
+ /// worked, or has recently stopped working, the daemon will start to
+ /// automatically try to find a working [`AccessMethodSetting`] among the
+ /// configured ones.
+ async fn set(item: SelectItem) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let previous_access_method = rpc.get_current_api_access_method().await?;
+ let mut new_access_method = Self::get_access_method(&mut rpc, &item).await?;
+ // Try to reach the API with the newly selected access method.
+ rpc.set_access_method(new_access_method.get_id()).await?;
+ match rpc.get_api_addresses().await {
+ Ok(_) => (),
+ Err(_) => {
+ // Roll-back to the previous access method
+ rpc.set_access_method(previous_access_method.get_id())
+ .await?;
+ return Err(anyhow!(
+ "Could not reach the Mullvad API using access method \"{}\". Rolling back to \"{}\"",
+ new_access_method.get_name(),
+ previous_access_method.get_name()
+ ));
+ }
+ };
+ // It worked! Let the daemon keep using this access method.
+ let display_name = new_access_method.get_name();
+ // Toggle the enabled status if needed
+ if !new_access_method.enabled() {
+ new_access_method.enable();
+ rpc.update_access_method(new_access_method).await?;
+ }
+ println!("Using access method \"{}\"", display_name);
+ Ok(())
+ }
+
+ async fn get() -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let current = rpc.get_current_api_access_method().await?;
+ let mut access_method_formatter = pp::ApiAccessMethodFormatter::new(&current);
+ access_method_formatter.settings.write_enabled = false;
+ println!("{}", access_method_formatter);
+ Ok(())
+ }
+
+ async fn get_access_method(
+ rpc: &mut MullvadProxyClient,
+ item: &SelectItem,
+ ) -> Result<AccessMethodSetting> {
+ rpc.get_api_access_methods()
+ .await?
+ .get(item.as_array_index()?)
+ .cloned()
+ .ok_or(anyhow!(format!("Access method {} does not exist", item)))
+ }
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum AddCustomCommands {
+ /// Configure a SOCKS5 proxy
+ #[clap(subcommand)]
+ Socks5(AddSocks5Commands),
+ /// Configure a custom Shadowsocks proxy to use as an API access method
+ Shadowsocks {
+ /// An easy to remember name for this custom proxy
+ name: String,
+ /// The IP of the remote Shadowsocks-proxy
+ remote_ip: IpAddr,
+ /// Port on which the remote Shadowsocks-proxy listens for traffic
+ #[arg(default_value = "443")]
+ remote_port: u16,
+ /// Password for authentication
+ #[arg(default_value = "mullvad")]
+ password: String,
+ /// Cipher to use
+ #[arg(value_parser = SHADOWSOCKS_CIPHERS, default_value = "aes-256-gcm")]
+ cipher: String,
+ /// Disable the use of this custom access method. It has to be manually
+ /// enabled at a later stage to be used when accessing the Mullvad API.
+ #[arg(default_value_t = false, short, long)]
+ disabled: bool,
+ },
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum AddSocks5Commands {
+ /// Configure a remote SOCKS5 proxy
+ Remote {
+ /// An easy to remember name for this custom proxy
+ name: String,
+ /// IP of the remote SOCKS5-proxy
+ remote_ip: IpAddr,
+ /// Port on which the remote SOCKS5-proxy listens for traffic
+ remote_port: u16,
+ #[clap(flatten)]
+ authentication: Option<SocksAuthentication>,
+ /// Disable the use of this custom access method. It has to be manually
+ /// enabled at a later stage to be used when accessing the Mullvad API.
+ #[arg(default_value_t = false, short, long)]
+ disabled: bool,
+ },
+ /// Configure a local SOCKS5 proxy
+ Local {
+ /// An easy to remember name for this custom proxy
+ name: String,
+ /// The port that the server on localhost is listening on
+ local_port: u16,
+ /// The IP of the remote peer
+ remote_ip: IpAddr,
+ /// The port of the remote peer
+ remote_port: u16,
+ /// Disable the use of this custom access method. It has to be manually
+ /// enabled at a later stage to be used when accessing the Mullvad API.
+ #[arg(default_value_t = false, short, long)]
+ disabled: bool,
+ },
+}
+
+#[derive(Args, Debug, Clone)]
+pub struct SocksAuthentication {
+ /// Username for authentication against a remote SOCKS5 proxy
+ #[arg(short, long)]
+ username: String,
+ /// Password for authentication against a remote SOCKS5 proxy
+ #[arg(short, long)]
+ password: String,
+}
+
+impl AddCustomCommands {
+ fn name(&self) -> &str {
+ match self {
+ AddCustomCommands::Shadowsocks { name, .. }
+ | AddCustomCommands::Socks5(AddSocks5Commands::Remote { name, .. })
+ | AddCustomCommands::Socks5(AddSocks5Commands::Local { name, .. }) => name,
+ }
+ }
+
+ fn enabled(&self) -> bool {
+ match self {
+ AddCustomCommands::Shadowsocks { disabled, .. }
+ | AddCustomCommands::Socks5(AddSocks5Commands::Remote { disabled, .. })
+ | AddCustomCommands::Socks5(AddSocks5Commands::Local { disabled, .. }) => !disabled,
+ }
+ }
+}
+
+/// A minimal wrapper type allowing the user to supply a list index to some
+/// Access Method.
+#[derive(Args, Debug, Clone)]
+pub struct SelectItem {
+ /// Which access method to pick
+ index: usize,
+}
+
+impl SelectItem {
+ /// Transform human-readable (1-based) index to 0-based indexing.
+ pub fn as_array_index(&self) -> Result<usize> {
+ self.index
+ .checked_sub(1)
+ .ok_or(anyhow!("Access method 0 does not exist"))
+ }
+}
+
+impl std::fmt::Display for SelectItem {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.index)
+ }
+}
+
+#[derive(Args, Debug, Clone)]
+pub struct EditCustomCommands {
+ /// Which API access method to edit
+ #[clap(flatten)]
+ item: SelectItem,
+ /// Editing parameters
+ #[clap(flatten)]
+ params: EditParams,
+}
+
+#[derive(Args, Debug, Clone)]
+pub struct EditParams {
+ /// Name of the API access method in the Mullvad client [All]
+ #[arg(long)]
+ name: Option<String>,
+ /// Username for authentication [Socks5 (Remote proxy)]
+ #[arg(long)]
+ username: Option<String>,
+ /// Password for authentication [Socks5 (Remote proxy), Shadowsocks]
+ #[arg(long)]
+ password: Option<String>,
+ /// Cipher to use [Shadowsocks]
+ #[arg(value_parser = SHADOWSOCKS_CIPHERS, long)]
+ cipher: Option<String>,
+ /// The IP of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks]
+ #[arg(long)]
+ ip: Option<IpAddr>,
+ /// The port of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks]
+ #[arg(long)]
+ port: Option<u16>,
+ /// The port that the server on localhost is listening on [Socks5 (Local proxy)]
+ #[arg(long)]
+ local_port: Option<u16>,
+}
+
+/// Implement conversions from CLI types to Daemon types.
+///
+/// Since these are not supposed to be used outside of the CLI,
+/// we define them in a hidden-away module.
+mod conversions {
+ use anyhow::{anyhow, Error};
+ use mullvad_types::access_method as daemon_types;
+
+ use super::{AddCustomCommands, AddSocks5Commands, SocksAuthentication};
+
+ impl TryFrom<AddCustomCommands> for daemon_types::AccessMethod {
+ type Error = Error;
+
+ fn try_from(value: AddCustomCommands) -> Result<Self, Self::Error> {
+ Ok(match value {
+ AddCustomCommands::Socks5(socks) => match socks {
+ AddSocks5Commands::Local {
+ local_port,
+ remote_ip,
+ remote_port,
+ name: _,
+ disabled: _,
+ } => {
+ println!("Adding SOCKS5-proxy: localhost:{local_port} => {remote_ip}:{remote_port}");
+ daemon_types::Socks5Local::from_args(
+ remote_ip.to_string(),
+ remote_port,
+ local_port,
+ )
+ .map(daemon_types::Socks5::Local)
+ .map(daemon_types::AccessMethod::from)
+ .ok_or(anyhow!("Could not create a local Socks5 access method"))?
+ }
+ AddSocks5Commands::Remote {
+ remote_ip,
+ remote_port,
+ authentication,
+ name: _,
+ disabled: _,
+ } => {
+ match authentication {
+ Some(SocksAuthentication { username, password }) => {
+ println!("Adding SOCKS5-proxy: {username}:{password}@{remote_ip}:{remote_port}");
+ daemon_types::Socks5Remote::from_args_with_password(
+ remote_ip.to_string(),
+ remote_port,
+ username,
+ password
+ )
+ }
+ None => {
+ println!("Adding SOCKS5-proxy: {remote_ip}:{remote_port}");
+ daemon_types::Socks5Remote::from_args(
+ remote_ip.to_string(),
+ remote_port,
+ )
+ }
+ }
+ .map(daemon_types::Socks5::Remote)
+ .map(daemon_types::AccessMethod::from)
+ .ok_or(anyhow!("Could not create a remote Socks5 access method"))?
+ }
+ },
+ AddCustomCommands::Shadowsocks {
+ remote_ip,
+ remote_port,
+ password,
+ cipher,
+ name: _,
+ disabled: _,
+ } => {
+ println!(
+ "Adding Shadowsocks-proxy: {password} @ {remote_ip}:{remote_port} using {cipher}"
+ );
+ daemon_types::Shadowsocks::from_args(
+ remote_ip.to_string(),
+ remote_port,
+ cipher,
+ password,
+ )
+ .map(daemon_types::AccessMethod::from)
+ .ok_or(anyhow!("Could not create a Shadowsocks access method"))?
+ }
+ })
+ }
+ }
+}
+
+/// Pretty printing of [`ApiAccessMethod`]s
+mod pp {
+ use mullvad_types::access_method::{
+ AccessMethod, AccessMethodSetting, CustomAccessMethod, Socks5, SocksAuth,
+ };
+
+ pub struct ApiAccessMethodFormatter<'a> {
+ api_access_method: &'a AccessMethodSetting,
+ pub settings: FormatterSettings,
+ }
+
+ pub struct FormatterSettings {
+ /// If the formatter should print the enabled status of an
+ /// [`AcessMethodSetting`] (*) next to its name.
+ pub write_enabled: bool,
+ }
+
+ impl Default for FormatterSettings {
+ fn default() -> Self {
+ Self {
+ write_enabled: true,
+ }
+ }
+ }
+
+ impl<'a> ApiAccessMethodFormatter<'a> {
+ pub fn new(api_access_method: &'a AccessMethodSetting) -> ApiAccessMethodFormatter<'a> {
+ ApiAccessMethodFormatter {
+ api_access_method,
+ settings: Default::default(),
+ }
+ }
+ }
+
+ impl<'a> std::fmt::Display for ApiAccessMethodFormatter<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use crate::print_option;
+
+ let write_status = |f: &mut std::fmt::Formatter<'_>, enabled: bool| {
+ if enabled {
+ write!(f, " *")
+ } else {
+ write!(f, "")
+ }
+ };
+
+ match &self.api_access_method.access_method {
+ AccessMethod::BuiltIn(method) => {
+ write!(f, "{}", method.canonical_name())?;
+ if self.settings.write_enabled {
+ write_status(f, self.api_access_method.enabled())?;
+ }
+ Ok(())
+ }
+ AccessMethod::Custom(method) => match &method {
+ CustomAccessMethod::Shadowsocks(shadowsocks) => {
+ write!(f, "{}", self.api_access_method.get_name())?;
+ if self.settings.write_enabled {
+ write_status(f, self.api_access_method.enabled())?;
+ }
+ writeln!(f)?;
+ print_option!("Protocol", format!("Shadowsocks [{}]", shadowsocks.cipher));
+ print_option!("Peer", shadowsocks.peer);
+ print_option!("Password", shadowsocks.password);
+ Ok(())
+ }
+ CustomAccessMethod::Socks5(socks) => match socks {
+ Socks5::Remote(remote) => {
+ write!(f, "{}", self.api_access_method.get_name())?;
+ if self.settings.write_enabled {
+ write_status(f, self.api_access_method.enabled())?;
+ }
+ writeln!(f)?;
+ print_option!("Protocol", "Socks5");
+ print_option!("Peer", remote.peer);
+ match &remote.authentication {
+ Some(SocksAuth { username, password }) => {
+ print_option!("Username", username);
+ print_option!("Password", password);
+ }
+ None => (),
+ }
+ Ok(())
+ }
+ Socks5::Local(local) => {
+ write!(f, "{}", self.api_access_method.get_name())?;
+ if self.settings.write_enabled {
+ write_status(f, self.api_access_method.enabled())?;
+ }
+ writeln!(f)?;
+ print_option!("Protocol", "Socks5 (local)");
+ print_option!("Peer", local.peer);
+ print_option!("Local port", local.port);
+ Ok(())
+ }
+ },
+ },
+ }
+ }
+ }
+}
diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs
index c63a981133..88e4184f07 100644
--- a/mullvad-cli/src/cmds/mod.rs
+++ b/mullvad-cli/src/cmds/mod.rs
@@ -2,6 +2,7 @@ use clap::builder::{PossibleValuesParser, TypedValueParser, ValueParser};
use std::ops::Deref;
pub mod account;
+pub mod api_access;
pub mod auto_connect;
pub mod beta_program;
pub mod bridge;
diff --git a/mullvad-cli/src/main.rs b/mullvad-cli/src/main.rs
index 41f1643970..7a09a4eebd 100644
--- a/mullvad-cli/src/main.rs
+++ b/mullvad-cli/src/main.rs
@@ -71,6 +71,23 @@ enum Cli {
#[clap(subcommand)]
Relay(relay::Relay),
+ /// Manage Mullvad API access methods.
+ ///
+ /// Access methods are used to connect to the the Mullvad API via one of
+ /// Mullvad's bridge servers or a custom proxy (SOCKS5 & Shadowsocks) when
+ /// and where establishing a direct connection does not work.
+ ///
+ /// If the Mullvad daemon is unable to connect to the Mullvad API, it will
+ /// automatically try to use any other configured access method and re-try
+ /// the API call. If it succeeds, all subsequent API calls are made using
+ /// the new access method. Otherwise it will re-try using yet another access
+ /// method.
+ ///
+ /// The Mullvad API is used for logging in, accessing the relay list,
+ /// rotating Wireguard keys and more.
+ #[clap(subcommand)]
+ ApiAccess(api_access::ApiAccess),
+
/// Manage use of obfuscation protocols for WireGuard.
/// Can make WireGuard traffic look like something else on the network.
/// Helps circumvent censorship and to establish a tunnel when on restricted networks
@@ -134,6 +151,7 @@ async fn main() -> Result<()> {
Cli::Dns(cmd) => cmd.handle().await,
Cli::Lan(cmd) => cmd.handle().await,
Cli::Obfuscation(cmd) => cmd.handle().await,
+ Cli::ApiAccess(cmd) => cmd.handle().await,
Cli::Version => version::print().await,
Cli::FactoryReset => reset::handle().await,
Cli::Relay(cmd) => cmd.handle().await,
diff --git a/mullvad-daemon/src/access_method.rs b/mullvad-daemon/src/access_method.rs
new file mode 100644
index 0000000000..013cf9d7ce
--- /dev/null
+++ b/mullvad-daemon/src/access_method.rs
@@ -0,0 +1,209 @@
+use crate::{
+ settings::{self, MadeChanges},
+ Daemon, EventListener,
+};
+use mullvad_types::{
+ access_method::{self, AccessMethod, AccessMethodSetting},
+ settings::Settings,
+};
+
+#[derive(err_derive::Error, Debug)]
+pub enum Error {
+ /// Can not add access method
+ #[error(display = "Cannot add custom access method")]
+ Add,
+ /// Can not remove built-in access method
+ #[error(display = "Cannot remove built-in access method")]
+ RemoveBuiltIn,
+ /// Can not find access method
+ #[error(display = "Cannot find custom access method {}", _0)]
+ NoSuchMethod(access_method::Id),
+ /// Can not find *any* access method. This should never happen. If it does,
+ /// the user should do a factory reset.
+ #[error(display = "No access methods are configured")]
+ NoMethodsExist,
+ /// Access method could not be rotate
+ #[error(display = "Access method could not be rotated")]
+ RotationError,
+ /// Access methods settings error
+ #[error(display = "Settings error")]
+ Settings(#[error(source)] settings::Error),
+}
+
+/// A tiny datastructure used for signaling whether the daemon should force a
+/// rotation of the currently used [`AccessMethodSetting`] or not, and if so:
+/// how it should do it.
+pub enum Command {
+ /// There is no need to force a rotation of [`AccessMethodSetting`]
+ Nothing,
+ /// Select the next available [`AccessMethodSetting`], whichever that is
+ Rotate,
+ /// Select the [`AccessMethodSetting`] with a certain [`access_method::Id`]
+ Set(access_method::Id),
+}
+
+impl<L> Daemon<L>
+where
+ L: EventListener + Clone + Send + 'static,
+{
+ /// Add a [`AccessMethod`] to the daemon's settings.
+ ///
+ /// If the daemon settings are successfully updated, the
+ /// [`access_method::Id`] of the newly created [`AccessMethodSetting`]
+ /// (which has been derived from the [`AccessMethod`]) is returned.
+ pub async fn add_access_method(
+ &mut self,
+ name: String,
+ enabled: bool,
+ access_method: AccessMethod,
+ ) -> Result<access_method::Id, Error> {
+ let access_method_setting = AccessMethodSetting::new(name, enabled, access_method);
+ let id = access_method_setting.get_id();
+ self.settings
+ .update(|settings| settings.api_access_methods.append(access_method_setting))
+ .await
+ .map(|did_change| self.notify_on_change(did_change))
+ .map(|_| id)
+ .map_err(Error::Settings)
+ }
+
+ /// Remove a [`AccessMethodSetting`] from the daemon's saved settings.
+ ///
+ /// If the [`AccessMethodSetting`] which is currently in use happens to be
+ /// removed, the daemon should force a rotation of the active API endpoint.
+ pub async fn remove_access_method(
+ &mut self,
+ access_method: access_method::Id,
+ ) -> Result<(), Error> {
+ // Make sure that we are not trying to remove a built-in API access
+ // method
+ let command = match self.settings.api_access_methods.find(&access_method) {
+ Some(api_access_method) => {
+ if api_access_method.is_builtin() {
+ Err(Error::RemoveBuiltIn)
+ } else if api_access_method.get_id() == self.get_current_access_method()?.get_id() {
+ Ok(Command::Rotate)
+ } else {
+ Ok(Command::Nothing)
+ }
+ }
+ None => Ok(Command::Nothing),
+ }?;
+
+ self.settings
+ .update(|settings| settings.api_access_methods.remove(&access_method))
+ .await
+ .map(|did_change| self.notify_on_change(did_change))
+ .map_err(Error::Settings)?
+ .process_command(command)
+ .await
+ }
+
+ /// Set a [`AccessMethodSetting`] as the current API access method.
+ ///
+ /// If successful, the daemon will force a rotation of the active API access
+ /// method, which means that subsequent API calls will use the new
+ /// [`AccessMethodSetting`] to figure out the API endpoint.
+ pub async fn set_api_access_method(
+ &mut self,
+ access_method: access_method::Id,
+ ) -> Result<(), Error> {
+ let access_method = self
+ .settings
+ .api_access_methods
+ .find(&access_method)
+ .ok_or(Error::NoSuchMethod(access_method))?;
+ {
+ let mut connection_modes = self.connection_modes.lock().unwrap();
+ connection_modes.set_access_method(access_method.clone());
+ }
+ // Force a rotation of Access Methods.
+ //
+ // This is not a call to `process_command` due to the restrictions on
+ // recursively calling async functions.
+ self.force_api_endpoint_rotation().await
+ }
+
+ /// "Updates" an [`AccessMethodSetting`] by replacing the existing entry
+ /// with the argument `access_method_update` if an existing entry with
+ /// matching [`access_method::Id`] is found.
+ ///
+ /// If the currently active [`AccessMethodSetting`] is updated, the daemon
+ /// will automatically use this updated [`AccessMethodSetting`] when
+ /// performing subsequent API calls.
+ pub async fn update_access_method(
+ &mut self,
+ access_method_update: AccessMethodSetting,
+ ) -> Result<(), Error> {
+ let current = self.get_current_access_method()?;
+ let mut command = Command::Nothing;
+ let settings_update = |settings: &mut Settings| {
+ if let Some(access_method) = settings
+ .api_access_methods
+ .find_mut(&access_method_update.get_id())
+ {
+ *access_method = access_method_update;
+ if access_method.get_id() == current.get_id() {
+ command = Command::Set(access_method.get_id())
+ }
+ }
+ };
+
+ self.settings
+ .update(settings_update)
+ .await
+ .map(|did_change| self.notify_on_change(did_change))
+ .map_err(Error::Settings)?
+ .process_command(command)
+ .await
+ }
+
+ /// Return the [`AccessMethodSetting`] which is currently used to access the
+ /// Mullvad API.
+ pub fn get_current_access_method(&self) -> Result<AccessMethodSetting, Error> {
+ let connections_modes = self.connection_modes.lock().unwrap();
+ Ok(connections_modes.peek())
+ }
+
+ /// Change which [`AccessMethodSetting`] which will be used to figure out
+ /// the Mullvad API endpoint.
+ async fn force_api_endpoint_rotation(&self) -> Result<(), Error> {
+ self.api_handle
+ .service()
+ .next_api_endpoint()
+ .await
+ .map_err(|error| {
+ log::error!("Failed to rotate API endpoint: {}", error);
+ Error::RotationError
+ })
+ }
+
+ /// If settings were changed due to an update, notify all listeners.
+ fn notify_on_change(&mut self, settings_changed: MadeChanges) -> &mut Self {
+ if settings_changed {
+ self.event_listener
+ .notify_settings(self.settings.to_settings());
+
+ let mut connection_modes = self.connection_modes.lock().unwrap();
+ connection_modes.update_access_methods(
+ self.settings
+ .api_access_methods
+ .access_method_settings
+ .iter()
+ .filter(|api_access_method| api_access_method.enabled())
+ .cloned()
+ .collect(),
+ )
+ };
+ self
+ }
+
+ /// The semantics of the [`Command`] datastructure.
+ async fn process_command(&mut self, command: Command) -> Result<(), Error> {
+ match command {
+ Command::Nothing => Ok(()),
+ Command::Rotate => self.force_api_endpoint_rotation().await,
+ Command::Set(id) => self.set_api_access_method(id).await,
+ }
+ }
+}
diff --git a/mullvad-daemon/src/api.rs b/mullvad-daemon/src/api.rs
index 67f80ca235..c548f0a293 100644
--- a/mullvad-daemon/src/api.rs
+++ b/mullvad-daemon/src/api.rs
@@ -10,6 +10,7 @@ use mullvad_api::{
ApiEndpointUpdateCallback,
};
use mullvad_relay_selector::RelaySelector;
+use mullvad_types::access_method::{AccessMethod, AccessMethodSetting, BuiltInAccessMethod};
use std::{
net::SocketAddr,
path::PathBuf,
@@ -27,22 +28,19 @@ use talpid_types::{
/// A stream that returns the next API connection mode to use for reaching the API.
///
-/// When `mullvad-api` fails to contact the API, it requests a new connection mode.
-/// The API can be connected to either directly (i.e., [`ApiConnectionMode::Direct`])
-/// or from a bridge ([`ApiConnectionMode::Proxied`]).
+/// When `mullvad-api` fails to contact the API, it requests a new connection
+/// mode. The API can be connected to either directly (i.e.,
+/// [`ApiConnectionMode::Direct`]) via a bridge ([`ApiConnectionMode::Proxied`])
+/// or via any supported custom proxy protocol ([`api_access_methods::ObfuscationProtocol`]).
///
-/// * Every 3rd attempt returns [`ApiConnectionMode::Direct`].
-/// * Any other attempt returns a configuration for the bridge that is closest to the selected relay
-/// location and matches all bridge constraints.
-/// * When no matching bridge is found, e.g. if the selected hosting providers don't match any
-/// bridge, [`ApiConnectionMode::Direct`] is returned.
+/// The strategy for determining the next [`ApiConnectionMode`] is handled by
+/// [`ConnectionModesIterator`].
pub struct ApiConnectionModeProvider {
cache_dir: PathBuf,
-
+ /// Used for selecting a Bridge when the `Mullvad Bridges` access method is used.
relay_selector: RelaySelector,
- retry_attempt: u32,
-
current_task: Option<Pin<Box<dyn Future<Output = ApiConnectionMode> + Send>>>,
+ connection_modes: Arc<Mutex<ConnectionModesIterator>>,
}
impl Stream for ApiConnectionModeProvider {
@@ -63,35 +61,17 @@ impl Stream for ApiConnectionModeProvider {
};
}
- // Create a new task.
- let config = if Self::should_use_bridge(self.retry_attempt) {
- self.relay_selector
- .get_bridge_forced()
- .map(|settings| match settings {
- ProxySettings::Shadowsocks(ss_settings) => {
- ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks(ss_settings))
- }
- _ => {
- log::error!("Received unexpected proxy settings type");
- ApiConnectionMode::Direct
- }
- })
- .unwrap_or(ApiConnectionMode::Direct)
- } else {
- ApiConnectionMode::Direct
- };
-
- self.retry_attempt = self.retry_attempt.wrapping_add(1);
+ let connection_mode = self.new_connection_mode();
let cache_dir = self.cache_dir.clone();
self.current_task = Some(Box::pin(async move {
- if let Err(error) = config.save(&cache_dir).await {
+ if let Err(error) = connection_mode.save(&cache_dir).await {
log::debug!(
"{}",
error.display_chain_with_msg("Failed to save API endpoint")
);
}
- config
+ connection_mode
}));
self.poll_next(cx)
@@ -99,19 +79,144 @@ impl Stream for ApiConnectionModeProvider {
}
impl ApiConnectionModeProvider {
- pub(crate) fn new(cache_dir: PathBuf, relay_selector: RelaySelector) -> Self {
+ pub(crate) fn new(
+ cache_dir: PathBuf,
+ relay_selector: RelaySelector,
+ connection_modes: Vec<AccessMethodSetting>,
+ ) -> Self {
+ let connection_modes_iterator = ConnectionModesIterator::new(connection_modes);
Self {
cache_dir,
-
relay_selector,
- retry_attempt: 0,
-
current_task: None,
+ connection_modes: Arc::new(Mutex::new(connection_modes_iterator)),
+ }
+ }
+
+ /// Return a pointer to the underlying iterator over [`AccessMethod`].
+ /// Having access to this iterator allow you to influence , e.g. by calling
+ /// [`ConnectionModesIterator::set_access_method()`] or
+ /// [`ConnectionModesIterator::update_access_methods()`].
+ pub(crate) fn handle(&self) -> Arc<Mutex<ConnectionModesIterator>> {
+ self.connection_modes.clone()
+ }
+
+ /// Return a new connection mode to be used for the API connection.
+ fn new_connection_mode(&mut self) -> ApiConnectionMode {
+ log::debug!("Rotating Access mode!");
+ let access_method = {
+ let mut access_methods_picker = self.connection_modes.lock().unwrap();
+ access_methods_picker
+ .next()
+ .map(|access_method_setting| access_method_setting.access_method)
+ .unwrap_or(AccessMethod::from(BuiltInAccessMethod::Direct))
+ };
+
+ let connection_mode = self.from(access_method);
+ log::info!("New API connection mode selected: {}", connection_mode);
+ connection_mode
+ }
+
+ /// Ad-hoc version of [`std::convert::From::from`], but since some
+ /// [`ApiConnectionMode`]s require extra logic/data from
+ /// [`ApiConnectionModeProvider`] the standard [`std::convert::From`] trait
+ /// can not be implemented.
+ fn from(&mut self, access_method: AccessMethod) -> ApiConnectionMode {
+ use mullvad_types::access_method;
+ match access_method {
+ AccessMethod::BuiltIn(access_method) => match access_method {
+ BuiltInAccessMethod::Direct => ApiConnectionMode::Direct,
+ BuiltInAccessMethod::Bridge => self
+ .relay_selector
+ .get_bridge_forced()
+ .and_then(|settings| match settings {
+ ProxySettings::Shadowsocks(ss_settings) => {
+ let ss_settings: access_method::Shadowsocks =
+ access_method::Shadowsocks::new(
+ ss_settings.peer,
+ ss_settings.cipher,
+ ss_settings.password,
+ );
+ Some(ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks(
+ ss_settings,
+ )))
+ }
+ _ => {
+ log::error!("Received unexpected proxy settings type");
+ None
+ }
+ })
+ .unwrap_or(ApiConnectionMode::Direct),
+ },
+ AccessMethod::Custom(access_method) => match access_method {
+ access_method::CustomAccessMethod::Shadowsocks(shadowsocks_config) => {
+ ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks(shadowsocks_config))
+ }
+ access_method::CustomAccessMethod::Socks5(socks_config) => {
+ ApiConnectionMode::Proxied(ProxyConfig::Socks(socks_config))
+ }
+ },
}
}
+}
+
+/// An iterator which will always produce an [`AccessMethod`].
+///
+/// Safety: It is always safe to [`unwrap`] after calling [`next`] on a
+/// [`std::iter::Cycle`], so thereby it is safe to always call [`unwrap`] on a
+/// [`ConnectionModesIterator`].
+///
+/// [`unwrap`]: Option::unwrap
+/// [`next`]: std::iter::Iterator::next
+pub struct ConnectionModesIterator {
+ available_modes: Box<dyn Iterator<Item = AccessMethodSetting> + Send>,
+ next: Option<AccessMethodSetting>,
+ current: AccessMethodSetting,
+}
+
+impl ConnectionModesIterator {
+ pub fn new(access_methods: Vec<AccessMethodSetting>) -> ConnectionModesIterator {
+ let mut iterator = Self::cycle(access_methods);
+ Self {
+ next: None,
+ current: iterator.next().unwrap(),
+ available_modes: iterator,
+ }
+ }
+
+ /// Set the next [`AccessMethod`] to be returned from this iterator.
+ pub fn set_access_method(&mut self, next: AccessMethodSetting) {
+ self.next = Some(next);
+ }
+ /// Update the collection of [`AccessMethod`] which this iterator will
+ /// return.
+ pub fn update_access_methods(&mut self, access_methods: Vec<AccessMethodSetting>) {
+ self.available_modes = Self::cycle(access_methods)
+ }
+
+ fn cycle(
+ access_methods: Vec<AccessMethodSetting>,
+ ) -> Box<dyn Iterator<Item = AccessMethodSetting> + Send> {
+ Box::new(access_methods.into_iter().cycle())
+ }
+
+ /// Look at the currently active [`AccessMethod`]
+ pub fn peek(&self) -> AccessMethodSetting {
+ self.current.clone()
+ }
+}
+
+impl Iterator for ConnectionModesIterator {
+ type Item = AccessMethodSetting;
- fn should_use_bridge(retry_attempt: u32) -> bool {
- retry_attempt % 3 > 0
+ fn next(&mut self) -> Option<Self::Item> {
+ let next = self
+ .next
+ .take()
+ .or_else(|| self.available_modes.next())
+ .unwrap();
+ self.current = next.clone();
+ Some(next)
}
}
@@ -138,8 +243,8 @@ impl ApiEndpointUpdaterHandle {
move |address: SocketAddr| {
let inner_tx = tunnel_tx.clone();
async move {
- let tunnel_tx = if let Some(Some(tunnel_tx)) = { inner_tx.lock().unwrap().as_ref() }
- .map(|tx: &Weak<mpsc::UnboundedSender<TunnelCommand>>| tx.upgrade())
+ let tunnel_tx = if let Some(tunnel_tx) = { inner_tx.lock().unwrap().as_ref() }
+ .and_then(|tx: &Weak<mpsc::UnboundedSender<TunnelCommand>>| tx.upgrade())
{
tunnel_tx
} else {
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index 81ebe1050c..f7343acc87 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -1,6 +1,7 @@
#![deny(rust_2018_idioms)]
#![recursion_limit = "512"]
+mod access_method;
pub mod account_history;
mod api;
#[cfg(not(target_os = "android"))]
@@ -38,6 +39,7 @@ use mullvad_relay_selector::{
RelaySelector, SelectorConfig,
};
use mullvad_types::{
+ access_method::{AccessMethod, AccessMethodSetting},
account::{AccountData, AccountToken, VoucherSubmission},
auth_failed::AuthFailed,
custom_list::CustomList,
@@ -60,7 +62,7 @@ use std::{
mem,
path::PathBuf,
pin::Pin,
- sync::{Arc, Weak},
+ sync::{Arc, Mutex, Weak},
time::Duration,
};
#[cfg(any(target_os = "linux", windows))]
@@ -170,6 +172,9 @@ pub enum Error {
#[error(display = "A list with that name does not exist")]
CustomListNotFound,
+ #[error(display = "Access method error")]
+ AccessMethodError(#[error(source)] access_method::Error),
+
#[cfg(target_os = "macos")]
#[error(display = "Failed to set exclusion group")]
GroupIdError(#[error(source)] io::Error),
@@ -255,6 +260,25 @@ pub enum DaemonCommand {
DeleteCustomList(ResponseTx<(), Error>, mullvad_types::custom_list::Id),
/// Update a custom list with a given id
UpdateCustomList(ResponseTx<(), Error>, CustomList),
+ /// Get API access methods
+ GetApiAccessMethods(ResponseTx<Vec<AccessMethodSetting>, Error>),
+ /// Add API access methods
+ AddApiAccessMethod(
+ ResponseTx<mullvad_types::access_method::Id, Error>,
+ String,
+ bool,
+ AccessMethod,
+ ),
+ /// Remove an API access method
+ RemoveApiAccessMethod(ResponseTx<(), Error>, mullvad_types::access_method::Id),
+ /// Set the API access method to use
+ SetApiAccessMethod(ResponseTx<(), Error>, mullvad_types::access_method::Id),
+ /// Edit an API access method
+ UpdateApiAccessMethod(ResponseTx<(), Error>, AccessMethodSetting),
+ /// Get the currently used API access method
+ GetCurrentAccessMethod(ResponseTx<AccessMethodSetting, Error>),
+ /// Get the addresses of all known API endpoints
+ GetApiAddresses(ResponseTx<Vec<std::net::SocketAddr>, Error>),
/// Get information about the currently running and latest app versions
GetVersionInfo(oneshot::Sender<Option<AppVersionInfo>>),
/// Return whether the daemon is performing post-upgrade tasks
@@ -554,6 +578,7 @@ pub struct Daemon<L: EventListener> {
account_history: account_history::AccountHistory,
device_checker: device::TunnelStateChangeHandler,
account_manager: device::AccountManagerHandle,
+ connection_modes: Arc<Mutex<api::ConnectionModesIterator>>,
api_runtime: mullvad_api::Runtime,
api_handle: mullvad_api::rest::MullvadRestHandle,
version_updater_handle: version_check::VersionUpdaterHandle,
@@ -616,8 +641,21 @@ where
let initial_selector_config = new_selector_config(&settings);
let relay_selector = RelaySelector::new(initial_selector_config, &resource_dir, &cache_dir);
- let proxy_provider =
- api::ApiConnectionModeProvider::new(cache_dir.clone(), relay_selector.clone());
+ let proxy_provider = api::ApiConnectionModeProvider::new(
+ cache_dir.clone(),
+ relay_selector.clone(),
+ settings
+ .api_access_methods
+ .access_method_settings
+ .iter()
+ // We only care about the access methods which are set to 'enabled' by the user.
+ .filter(|api_access_method| api_access_method.enabled())
+ .cloned()
+ .collect(),
+ );
+
+ let connection_modes = proxy_provider.handle();
+
let api_handle = api_runtime
.mullvad_rest_handle(proxy_provider, endpoint_updater.callback())
.await;
@@ -754,6 +792,7 @@ where
account_history,
device_checker: device::TunnelStateChangeHandler::new(account_manager.clone()),
account_manager,
+ connection_modes,
api_runtime,
api_handle,
version_updater_handle,
@@ -1030,6 +1069,16 @@ where
DeleteCustomList(tx, id) => self.on_delete_custom_list(tx, id).await,
UpdateCustomList(tx, update) => self.on_update_custom_list(tx, update).await,
GetVersionInfo(tx) => self.on_get_version_info(tx),
+ GetApiAccessMethods(tx) => self.on_get_api_access_methods(tx),
+ AddApiAccessMethod(tx, name, enabled, access_method) => {
+ self.on_add_access_method(tx, name, enabled, access_method)
+ .await
+ }
+ RemoveApiAccessMethod(tx, method) => self.on_remove_api_access_method(tx, method).await,
+ UpdateApiAccessMethod(tx, method) => self.on_update_api_access_method(tx, method).await,
+ GetCurrentAccessMethod(tx) => self.on_get_current_api_access_method(tx),
+ SetApiAccessMethod(tx, method) => self.on_set_api_access_method(tx, method).await,
+ GetApiAddresses(tx) => self.on_get_api_addresses(tx).await,
IsPerformingPostUpgrade(tx) => self.on_is_performing_post_upgrade(tx),
GetCurrentVersion(tx) => self.on_get_current_version(tx),
#[cfg(not(target_os = "android"))]
@@ -1921,7 +1970,7 @@ where
.notify_settings(self.settings.to_settings());
self.relay_selector
.set_config(new_selector_config(&self.settings));
- if let Err(error) = self.api_handle.service().next_api_endpoint() {
+ if let Err(error) = self.api_handle.service().next_api_endpoint().await {
log::error!("Failed to rotate API endpoint: {}", error);
}
self.reconnect_tunnel();
@@ -2204,6 +2253,75 @@ where
Self::oneshot_send(tx, result, "update_custom_list response");
}
+ fn on_get_api_access_methods(&mut self, tx: ResponseTx<Vec<AccessMethodSetting>, Error>) {
+ let result = Ok(self.settings.api_access_methods.cloned());
+ Self::oneshot_send(tx, result, "get_api_access_methods response");
+ }
+
+ async fn on_add_access_method(
+ &mut self,
+ tx: ResponseTx<mullvad_types::access_method::Id, Error>,
+ name: String,
+ enabled: bool,
+ access_method: AccessMethod,
+ ) {
+ let result = self
+ .add_access_method(name, enabled, access_method)
+ .await
+ .map_err(Error::AccessMethodError);
+ Self::oneshot_send(tx, result, "add_api_access_method response");
+ }
+
+ async fn on_remove_api_access_method(
+ &mut self,
+ tx: ResponseTx<(), Error>,
+ api_access_method: mullvad_types::access_method::Id,
+ ) {
+ let result = self
+ .remove_access_method(api_access_method)
+ .await
+ .map_err(Error::AccessMethodError);
+ Self::oneshot_send(tx, result, "remove_api_access_method response");
+ }
+
+ async fn on_set_api_access_method(
+ &mut self,
+ tx: ResponseTx<(), Error>,
+ access_method: mullvad_types::access_method::Id,
+ ) {
+ let result = self
+ .set_api_access_method(access_method)
+ .await
+ .map_err(Error::AccessMethodError);
+ Self::oneshot_send(tx, result, "set_api_access_method response");
+ }
+
+ async fn on_update_api_access_method(
+ &mut self,
+ tx: ResponseTx<(), Error>,
+ method: AccessMethodSetting,
+ ) {
+ let result = self
+ .update_access_method(method)
+ .await
+ .map_err(Error::AccessMethodError);
+ Self::oneshot_send(tx, result, "update_api_access_method response");
+ }
+
+ fn on_get_current_api_access_method(&mut self, tx: ResponseTx<AccessMethodSetting, Error>) {
+ let result = self
+ .get_current_access_method()
+ .map_err(Error::AccessMethodError);
+ Self::oneshot_send(tx, result, "get_current_api_access_method response");
+ }
+
+ async fn on_get_api_addresses(&mut self, tx: ResponseTx<Vec<std::net::SocketAddr>, Error>) {
+ let api_proxy = mullvad_api::ApiProxy::new(self.api_handle.clone());
+ let result = api_proxy.get_api_addrs().await.map_err(Error::RestError);
+
+ Self::oneshot_send(tx, result, "on_get_api_adressess response");
+ }
+
fn on_get_settings(&self, tx: oneshot::Sender<Settings>) {
Self::oneshot_send(tx, self.settings.to_settings(), "get_settings response");
}
diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs
index 5358628640..993f0f9ece 100644
--- a/mullvad-daemon/src/management_interface.rs
+++ b/mullvad-daemon/src/management_interface.rs
@@ -619,6 +619,98 @@ impl ManagementService for ManagementServiceImpl {
.map_err(map_daemon_error)
}
+ // Access Methods
+
+ async fn add_api_access_method(
+ &self,
+ request: Request<types::NewAccessMethodSetting>,
+ ) -> ServiceResult<types::Uuid> {
+ log::debug!("add_api_access_method");
+ let request = request.into_inner();
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::AddApiAccessMethod(
+ tx,
+ request.name,
+ request.enabled,
+ request
+ .access_method
+ .ok_or(Status::invalid_argument("Could not find access method"))
+ .map(mullvad_types::access_method::AccessMethod::try_from)??,
+ ))?;
+ self.wait_for_result(rx)
+ .await?
+ .map(types::Uuid::from)
+ .map(Response::new)
+ .map_err(map_daemon_error)
+ }
+
+ async fn remove_api_access_method(&self, request: Request<types::Uuid>) -> ServiceResult<()> {
+ log::debug!("remove_api_access_method");
+ let api_access_method = mullvad_types::access_method::Id::try_from(request.into_inner())?;
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::RemoveApiAccessMethod(tx, api_access_method))?;
+ self.wait_for_result(rx)
+ .await?
+ .map(Response::new)
+ .map_err(map_daemon_error)
+ }
+
+ async fn set_api_access_method(&self, request: Request<types::Uuid>) -> ServiceResult<()> {
+ log::debug!("set_api_access_method");
+ let api_access_method = mullvad_types::access_method::Id::try_from(request.into_inner())?;
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::SetApiAccessMethod(tx, api_access_method))?;
+ self.wait_for_result(rx)
+ .await?
+ .map(Response::new)
+ .map_err(map_daemon_error)
+ }
+
+ async fn update_api_access_method(
+ &self,
+ request: Request<types::AccessMethodSetting>,
+ ) -> ServiceResult<()> {
+ log::debug!("update_api_access_method");
+ let access_method_update =
+ mullvad_types::access_method::AccessMethodSetting::try_from(request.into_inner())?;
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::UpdateApiAccessMethod(
+ tx,
+ access_method_update,
+ ))?;
+ self.wait_for_result(rx)
+ .await?
+ .map(Response::new)
+ .map_err(map_daemon_error)
+ }
+
+ /// Return the [`types::AccessMethodSetting`] which the daemon is using to
+ /// connect to the Mullvad API.
+ async fn get_current_api_access_method(
+ &self,
+ _: Request<()>,
+ ) -> ServiceResult<types::AccessMethodSetting> {
+ log::debug!("get_current_api_access_method");
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::GetCurrentAccessMethod(tx))?;
+ self.wait_for_result(rx)
+ .await?
+ .map(types::AccessMethodSetting::from)
+ .map(Response::new)
+ .map_err(map_daemon_error)
+ }
+
+ async fn get_api_addresses(&self, _: Request<()>) -> ServiceResult<types::ApiAddresses> {
+ log::debug!("get_api_addresses");
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::GetApiAddresses(tx))?;
+ self.wait_for_result(rx)
+ .await?
+ .map(types::ApiAddresses::from)
+ .map(Response::new)
+ .map_err(map_daemon_error)
+ }
+
// Split tunneling
//
diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto
index f4b3eb961d..24bfe2284b 100644
--- a/mullvad-management-interface/proto/management_interface.proto
+++ b/mullvad-management-interface/proto/management_interface.proto
@@ -22,6 +22,7 @@ service ManagementService {
rpc GetCurrentVersion(google.protobuf.Empty) returns (google.protobuf.StringValue) {}
rpc GetVersionInfo(google.protobuf.Empty) returns (AppVersionInfo) {}
+ rpc GetApiAddresses(google.protobuf.Empty) returns (ApiAddresses) {}
rpc IsPerformingPostUpgrade(google.protobuf.Empty) returns (google.protobuf.BoolValue) {}
@@ -73,6 +74,13 @@ service ManagementService {
rpc DeleteCustomList(google.protobuf.StringValue) returns (google.protobuf.Empty) {}
rpc UpdateCustomList(CustomList) returns (google.protobuf.Empty) {}
+ // Access methods
+ rpc AddApiAccessMethod(NewAccessMethodSetting) returns (UUID) {}
+ rpc RemoveApiAccessMethod(UUID) returns (google.protobuf.Empty) {}
+ rpc SetApiAccessMethod(UUID) returns (google.protobuf.Empty) {}
+ rpc UpdateApiAccessMethod(AccessMethodSetting) returns (google.protobuf.Empty) {}
+ rpc GetCurrentApiAccessMethod(google.protobuf.Empty) returns (AccessMethodSetting) {}
+
// Split tunneling (Linux)
rpc GetSplitTunnelProcesses(google.protobuf.Empty) returns (stream google.protobuf.Int32Value) {}
rpc AddSplitTunnelProcess(google.protobuf.Int32Value) returns (google.protobuf.Empty) {}
@@ -91,6 +99,8 @@ service ManagementService {
rpc CheckVolumes(google.protobuf.Empty) returns (google.protobuf.Empty) {}
}
+message UUID { string value = 1; }
+
message RelaySettingsUpdate {
oneof type {
CustomRelaySettings custom = 1;
@@ -102,6 +112,8 @@ message AccountData { google.protobuf.Timestamp expiry = 1; }
message AccountHistory { google.protobuf.StringValue token = 1; }
+message ApiAddresses { repeated google.protobuf.StringValue api_addresses = 1; }
+
message VoucherSubmission {
uint64 seconds_added = 1;
google.protobuf.Timestamp new_expiry = 2;
@@ -324,6 +336,53 @@ message CustomList {
message CustomListSettings { repeated CustomList custom_lists = 1; }
+message AccessMethod {
+ message Direct {}
+ message Bridges {}
+ message Socks5Local {
+ string ip = 1;
+ uint32 port = 2;
+ uint32 local_port = 3;
+ }
+ message SocksAuth {
+ string username = 1;
+ string password = 2;
+ }
+ message Socks5Remote {
+ string ip = 1;
+ uint32 port = 2;
+ SocksAuth authentication = 3;
+ }
+ message Shadowsocks {
+ string ip = 1;
+ uint32 port = 2;
+ string password = 3;
+ string cipher = 4;
+ }
+ oneof access_method {
+ Direct direct = 1;
+ Bridges bridges = 2;
+ Socks5Local socks5local = 3;
+ Socks5Remote socks5remote = 4;
+ Shadowsocks shadowsocks = 5;
+ }
+}
+
+message AccessMethodSetting {
+ UUID id = 1;
+ string name = 2;
+ bool enabled = 3;
+ AccessMethod access_method = 4;
+}
+
+message NewAccessMethodSetting {
+ string name = 1;
+ bool enabled = 2;
+ AccessMethod access_method = 3;
+}
+
+message ApiAccessMethodSettings { repeated AccessMethodSetting access_method_settings = 1; }
+
message Settings {
RelaySettings relay_settings = 1;
BridgeSettings bridge_settings = 2;
@@ -336,6 +395,7 @@ message Settings {
SplitTunnelSettings split_tunnel = 9;
ObfuscationSettings obfuscation_settings = 10;
CustomListSettings custom_lists = 11;
+ ApiAccessMethodSettings api_access_methods = 12;
}
message SplitTunnelSettings {
diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs
index a1ddc5e39a..417083e161 100644
--- a/mullvad-management-interface/src/client.rs
+++ b/mullvad-management-interface/src/client.rs
@@ -3,6 +3,7 @@
use crate::types;
use futures::{Stream, StreamExt};
use mullvad_types::{
+ access_method::{self, AccessMethod, AccessMethodSetting},
account::{AccountData, AccountToken, VoucherSubmission},
custom_list::{CustomList, Id},
device::{Device, DeviceEvent, DeviceId, DeviceState, RemoveDeviceEvent},
@@ -163,6 +164,55 @@ impl MullvadProxyClient {
mullvad_types::relay_list::RelayList::try_from(list).map_err(Error::InvalidResponse)
}
+ pub async fn get_api_access_methods(&mut self) -> Result<Vec<AccessMethodSetting>> {
+ self.0
+ .get_settings(())
+ .await
+ .map_err(Error::Rpc)?
+ .into_inner()
+ .api_access_methods
+ .ok_or(Error::ApiAccessMethodSettingsNotFound)?
+ .access_method_settings
+ .into_iter()
+ .map(|api_access_method| {
+ AccessMethodSetting::try_from(api_access_method).map_err(Error::InvalidResponse)
+ })
+ .collect()
+ }
+
+ pub async fn get_api_access_method(
+ &mut self,
+ id: &access_method::Id,
+ ) -> Result<AccessMethodSetting> {
+ self.get_api_access_methods()
+ .await?
+ .into_iter()
+ .find(|api_access_method| api_access_method.get_id() == *id)
+ .ok_or(Error::ApiAccessMethodNotFound)
+ }
+
+ pub async fn get_current_api_access_method(&mut self) -> Result<AccessMethodSetting> {
+ self.0
+ .get_current_api_access_method(())
+ .await
+ .map_err(Error::Rpc)
+ .map(tonic::Response::into_inner)
+ .and_then(|access_method| {
+ AccessMethodSetting::try_from(access_method).map_err(Error::InvalidResponse)
+ })
+ }
+
+ pub async fn get_api_addresses(&mut self) -> Result<Vec<std::net::SocketAddr>> {
+ self.0
+ .get_api_addresses(())
+ .await
+ .map_err(Error::Rpc)
+ .map(tonic::Response::into_inner)
+ .and_then(|api_addresses| {
+ Vec::<std::net::SocketAddr>::try_from(api_addresses).map_err(Error::InvalidResponse)
+ })
+ }
+
pub async fn update_relay_locations(&mut self) -> Result<()> {
self.0
.update_relay_locations(())
@@ -457,6 +507,63 @@ impl MullvadProxyClient {
Ok(())
}
+ pub async fn add_access_method(
+ &mut self,
+ name: String,
+ enabled: bool,
+ access_method: AccessMethod,
+ ) -> Result<()> {
+ let request = types::NewAccessMethodSetting {
+ name,
+ enabled,
+ access_method: Some(types::AccessMethod::from(access_method)),
+ };
+ self.0
+ .add_api_access_method(request)
+ .await
+ .map_err(Error::Rpc)
+ .map(drop)
+ }
+
+ pub async fn remove_access_method(
+ &mut self,
+ api_access_method: access_method::Id,
+ ) -> Result<()> {
+ self.0
+ .remove_api_access_method(types::Uuid::from(api_access_method))
+ .await
+ .map_err(Error::Rpc)
+ .map(drop)
+ }
+
+ pub async fn update_access_method(
+ &mut self,
+ access_method_update: AccessMethodSetting,
+ ) -> Result<()> {
+ self.0
+ .update_api_access_method(types::AccessMethodSetting::from(access_method_update))
+ .await
+ .map_err(Error::Rpc)
+ .map(drop)
+ }
+
+ /// Set the [`AccessMethod`] which [`ApiConnectionModeProvider`] should
+ /// pick.
+ ///
+ /// - `access_method`: If `Some(access_method)`, [`ApiConnectionModeProvider`] will skip
+ /// ahead and return `access_method` when asked for a new access method.
+ /// If `None`, [`ApiConnectionModeProvider`] will pick the next access
+ /// method "randomly"
+ ///
+ /// [`ApiConnectionModeProvider`]: mullvad_daemon::api::ApiConnectionModeProvider
+ pub async fn set_access_method(&mut self, api_access_method: access_method::Id) -> Result<()> {
+ self.0
+ .set_api_access_method(types::Uuid::from(api_access_method))
+ .await
+ .map_err(Error::Rpc)
+ .map(drop)
+ }
+
#[cfg(target_os = "linux")]
pub async fn get_split_tunnel_processes(&mut self) -> Result<Vec<i32>> {
use futures::TryStreamExt;
diff --git a/mullvad-management-interface/src/lib.rs b/mullvad-management-interface/src/lib.rs
index cf1a798878..c9414d03bf 100644
--- a/mullvad-management-interface/src/lib.rs
+++ b/mullvad-management-interface/src/lib.rs
@@ -103,6 +103,12 @@ pub enum Error {
#[error(display = "Location was not found in the custom list")]
LocationNotFoundInCustomlist,
+
+ #[error(display = "Could not retrieve API access methods from settings")]
+ ApiAccessMethodSettingsNotFound,
+
+ #[error(display = "An access method with that id does not exist")]
+ ApiAccessMethodNotFound,
}
#[deprecated(note = "Prefer MullvadProxyClient")]
diff --git a/mullvad-management-interface/src/types/conversions/access_method.rs b/mullvad-management-interface/src/types/conversions/access_method.rs
new file mode 100644
index 0000000000..8907c4da29
--- /dev/null
+++ b/mullvad-management-interface/src/types/conversions/access_method.rs
@@ -0,0 +1,289 @@
+/// Implements conversions for the auxilliary
+/// [`crate::types::proto::ApiAccessMethodSettings`] type to the internal
+/// [`mullvad_types::access_method::Settings`] data type.
+mod settings {
+ use crate::types::{proto, FromProtobufTypeError};
+ use mullvad_types::access_method;
+
+ impl From<&access_method::Settings> for proto::ApiAccessMethodSettings {
+ fn from(settings: &access_method::Settings) -> Self {
+ Self {
+ access_method_settings: settings
+ .access_method_settings
+ .iter()
+ .map(|method| method.clone().into())
+ .collect(),
+ }
+ }
+ }
+
+ impl From<access_method::Settings> for proto::ApiAccessMethodSettings {
+ fn from(settings: access_method::Settings) -> Self {
+ proto::ApiAccessMethodSettings::from(&settings)
+ }
+ }
+
+ impl TryFrom<proto::ApiAccessMethodSettings> for access_method::Settings {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(settings: proto::ApiAccessMethodSettings) -> Result<Self, Self::Error> {
+ Ok(Self {
+ access_method_settings: settings
+ .access_method_settings
+ .iter()
+ .map(access_method::AccessMethodSetting::try_from)
+ .collect::<Result<Vec<access_method::AccessMethodSetting>, _>>()?,
+ })
+ }
+ }
+}
+
+/// Implements conversions for the auxilliary
+/// [`crate::types::proto::ApiAccessMethod`] type to the internal
+/// [`mullvad_types::access_method::AccessMethodSetting`] data type.
+mod data {
+ use crate::types::{proto, FromProtobufTypeError};
+ use mullvad_types::access_method::{
+ AccessMethod, AccessMethodSetting, BuiltInAccessMethod, CustomAccessMethod, Id,
+ Shadowsocks, Socks5, Socks5Local, Socks5Remote, SocksAuth,
+ };
+
+ impl TryFrom<proto::AccessMethodSetting> for AccessMethodSetting {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: proto::AccessMethodSetting) -> Result<Self, Self::Error> {
+ let id = value
+ .id
+ .ok_or(FromProtobufTypeError::InvalidArgument(
+ "Could not deserialize Access Method from protobuf",
+ ))
+ .and_then(Id::try_from)?;
+ let name = value.name;
+ let enabled = value.enabled;
+ let access_method = value
+ .access_method
+ .ok_or(FromProtobufTypeError::InvalidArgument(
+ "Could not deserialize Access Method from protobuf",
+ ))
+ .and_then(AccessMethod::try_from)?;
+
+ Ok(AccessMethodSetting::with_id(
+ id,
+ name,
+ enabled,
+ access_method,
+ ))
+ }
+ }
+
+ impl From<AccessMethodSetting> for proto::AccessMethodSetting {
+ fn from(value: AccessMethodSetting) -> Self {
+ let id = proto::Uuid::from(value.get_id());
+ let name = value.get_name();
+ let enabled = value.enabled();
+ proto::AccessMethodSetting {
+ id: Some(id),
+ name,
+ enabled,
+ access_method: Some(proto::AccessMethod::from(value.access_method)),
+ }
+ }
+ }
+
+ impl TryFrom<proto::AccessMethod> for AccessMethod {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: proto::AccessMethod) -> Result<Self, Self::Error> {
+ let access_method =
+ value
+ .access_method
+ .ok_or(FromProtobufTypeError::InvalidArgument(
+ "Could not deserialize Access Method from protobuf",
+ ))?;
+
+ Ok(match access_method {
+ proto::access_method::AccessMethod::Direct(direct) => AccessMethod::from(direct),
+ proto::access_method::AccessMethod::Bridges(bridge) => AccessMethod::from(bridge),
+ proto::access_method::AccessMethod::Socks5local(sockslocal) => {
+ AccessMethod::try_from(sockslocal)?
+ }
+ proto::access_method::AccessMethod::Socks5remote(socksremote) => {
+ AccessMethod::try_from(socksremote)?
+ }
+ proto::access_method::AccessMethod::Shadowsocks(shadowsocks) => {
+ AccessMethod::try_from(shadowsocks)?
+ }
+ })
+ }
+ }
+
+ impl From<AccessMethod> for proto::AccessMethod {
+ fn from(value: AccessMethod) -> Self {
+ match value {
+ AccessMethod::Custom(value) => proto::AccessMethod::from(value),
+ AccessMethod::BuiltIn(value) => proto::AccessMethod::from(value),
+ }
+ }
+ }
+
+ impl From<proto::access_method::Direct> for AccessMethod {
+ fn from(_value: proto::access_method::Direct) -> Self {
+ AccessMethod::from(BuiltInAccessMethod::Direct)
+ }
+ }
+
+ impl From<proto::access_method::Bridges> for AccessMethod {
+ fn from(_value: proto::access_method::Bridges) -> Self {
+ AccessMethod::from(BuiltInAccessMethod::Bridge)
+ }
+ }
+
+ impl TryFrom<proto::access_method::Socks5Local> for AccessMethod {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: proto::access_method::Socks5Local) -> Result<Self, Self::Error> {
+ Socks5Local::from_args(value.ip, value.port as u16, value.local_port as u16)
+ .ok_or(FromProtobufTypeError::InvalidArgument(
+ "Could not parse Socks5 (local) message from protobuf",
+ ))
+ .map(AccessMethod::from)
+ }
+ }
+
+ impl TryFrom<proto::access_method::Socks5Remote> for AccessMethod {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: proto::access_method::Socks5Remote) -> Result<Self, Self::Error> {
+ let proto::access_method::Socks5Remote {
+ ip,
+ port,
+ authentication,
+ } = value;
+ let port = port as u16;
+ match authentication.map(SocksAuth::from) {
+ Some(SocksAuth { username, password }) => {
+ Socks5Remote::from_args_with_password(ip, port, username, password)
+ }
+ None => Socks5Remote::from_args(ip, port),
+ }
+ .ok_or({
+ FromProtobufTypeError::InvalidArgument(
+ "Could not parse Socks5 (remote) message from protobuf",
+ )
+ })
+ .map(AccessMethod::from)
+ }
+ }
+
+ impl TryFrom<proto::access_method::Shadowsocks> for AccessMethod {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: proto::access_method::Shadowsocks) -> Result<Self, Self::Error> {
+ Shadowsocks::from_args(value.ip, value.port as u16, value.cipher, value.password)
+ .ok_or(FromProtobufTypeError::InvalidArgument(
+ "Could not parse Shadowsocks message from protobuf",
+ ))
+ .map(AccessMethod::from)
+ }
+ }
+
+ impl From<BuiltInAccessMethod> for proto::AccessMethod {
+ fn from(value: BuiltInAccessMethod) -> Self {
+ let access_method = match value {
+ mullvad_types::access_method::BuiltInAccessMethod::Direct => {
+ proto::access_method::AccessMethod::Direct(proto::access_method::Direct {})
+ }
+ mullvad_types::access_method::BuiltInAccessMethod::Bridge => {
+ proto::access_method::AccessMethod::Bridges(proto::access_method::Bridges {})
+ }
+ };
+ proto::AccessMethod {
+ access_method: Some(access_method),
+ }
+ }
+ }
+
+ impl From<CustomAccessMethod> for proto::AccessMethod {
+ fn from(value: CustomAccessMethod) -> Self {
+ let access_method = match value {
+ CustomAccessMethod::Shadowsocks(ss) => {
+ proto::access_method::AccessMethod::Shadowsocks(
+ proto::access_method::Shadowsocks {
+ ip: ss.peer.ip().to_string(),
+ port: ss.peer.port() as u32,
+ password: ss.password,
+ cipher: ss.cipher,
+ },
+ )
+ }
+ CustomAccessMethod::Socks5(Socks5::Local(Socks5Local { peer, port })) => {
+ proto::access_method::AccessMethod::Socks5local(
+ proto::access_method::Socks5Local {
+ ip: peer.ip().to_string(),
+ port: peer.port() as u32,
+ local_port: port as u32,
+ },
+ )
+ }
+ CustomAccessMethod::Socks5(Socks5::Remote(Socks5Remote {
+ peer,
+ authentication,
+ })) => proto::access_method::AccessMethod::Socks5remote(
+ proto::access_method::Socks5Remote {
+ ip: peer.ip().to_string(),
+ port: peer.port() as u32,
+ authentication: authentication.map(proto::access_method::SocksAuth::from),
+ },
+ ),
+ };
+
+ proto::AccessMethod {
+ access_method: Some(access_method),
+ }
+ }
+ }
+
+ impl From<Id> for proto::Uuid {
+ fn from(value: Id) -> Self {
+ proto::Uuid {
+ value: value.to_string(),
+ }
+ }
+ }
+
+ impl TryFrom<proto::Uuid> for Id {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: proto::Uuid) -> Result<Self, Self::Error> {
+ Self::from_string(value.value).ok_or(FromProtobufTypeError::InvalidArgument(
+ "Could not parse UUID message from protobuf",
+ ))
+ }
+ }
+
+ impl From<SocksAuth> for proto::access_method::SocksAuth {
+ fn from(value: SocksAuth) -> Self {
+ proto::access_method::SocksAuth {
+ username: value.username,
+ password: value.password,
+ }
+ }
+ }
+
+ impl From<proto::access_method::SocksAuth> for SocksAuth {
+ fn from(value: proto::access_method::SocksAuth) -> Self {
+ Self {
+ username: value.username,
+ password: value.password,
+ }
+ }
+ }
+
+ impl TryFrom<&proto::AccessMethodSetting> for AccessMethodSetting {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: &proto::AccessMethodSetting) -> Result<Self, Self::Error> {
+ AccessMethodSetting::try_from(value.clone())
+ }
+ }
+}
diff --git a/mullvad-management-interface/src/types/conversions/mod.rs b/mullvad-management-interface/src/types/conversions/mod.rs
index d2e8b60265..dd6fcd4501 100644
--- a/mullvad-management-interface/src/types/conversions/mod.rs
+++ b/mullvad-management-interface/src/types/conversions/mod.rs
@@ -1,5 +1,6 @@
use std::str::FromStr;
+mod access_method;
mod account;
mod custom_list;
mod custom_tunnel;
diff --git a/mullvad-management-interface/src/types/conversions/net.rs b/mullvad-management-interface/src/types/conversions/net.rs
index d0dcc975d0..ea5dcf99a5 100644
--- a/mullvad-management-interface/src/types/conversions/net.rs
+++ b/mullvad-management-interface/src/types/conversions/net.rs
@@ -174,6 +174,27 @@ impl From<proto::IpVersion> for proto::IpVersionConstraint {
}
}
+impl TryFrom<proto::ApiAddresses> for Vec<SocketAddr> {
+ type Error = FromProtobufTypeError;
+
+ fn try_from(value: proto::ApiAddresses) -> Result<Self, Self::Error> {
+ value
+ .api_addresses
+ .iter()
+ .map(|api_address| api_address.parse::<SocketAddr>())
+ .collect::<Result<_, _>>()
+ .map_err(|_| FromProtobufTypeError::InvalidArgument("Invalid socket address"))
+ }
+}
+
+impl From<Vec<SocketAddr>> for proto::ApiAddresses {
+ fn from(value: Vec<SocketAddr>) -> Self {
+ Self {
+ api_addresses: value.iter().map(SocketAddr::to_string).collect(),
+ }
+ }
+}
+
pub fn try_tunnel_type_from_i32(
tunnel_type: i32,
) -> Result<talpid_types::net::TunnelType, FromProtobufTypeError> {
diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs
index f123b41755..98c195d935 100644
--- a/mullvad-management-interface/src/types/conversions/settings.rs
+++ b/mullvad-management-interface/src/types/conversions/settings.rs
@@ -42,6 +42,9 @@ impl From<&mullvad_types::settings::Settings> for proto::Settings {
custom_lists: Some(proto::CustomListSettings::from(
settings.custom_lists.clone(),
)),
+ api_access_methods: Some(proto::ApiAccessMethodSettings::from(
+ &settings.api_access_methods,
+ )),
}
}
}
@@ -140,6 +143,12 @@ impl TryFrom<proto::Settings> for mullvad_types::settings::Settings {
.ok_or(FromProtobufTypeError::InvalidArgument(
"missing custom lists settings",
))?;
+ let api_access_methods_settings =
+ settings
+ .api_access_methods
+ .ok_or(FromProtobufTypeError::InvalidArgument(
+ "missing api access methods settings",
+ ))?;
#[cfg(windows)]
let split_tunnel = settings
.split_tunnel
@@ -171,6 +180,9 @@ impl TryFrom<proto::Settings> for mullvad_types::settings::Settings {
custom_lists: mullvad_types::custom_list::CustomListsSettings::try_from(
custom_lists_settings,
)?,
+ api_access_methods: mullvad_types::access_method::Settings::try_from(
+ api_access_methods_settings,
+ )?,
})
}
}
diff --git a/mullvad-types/src/access_method.rs b/mullvad-types/src/access_method.rs
new file mode 100644
index 0000000000..30c1f25192
--- /dev/null
+++ b/mullvad-types/src/access_method.rs
@@ -0,0 +1,345 @@
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
+
+/// Daemon settings for API access methods.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct Settings {
+ pub access_method_settings: Vec<AccessMethodSetting>,
+}
+
+impl Settings {
+ /// Append an [`AccessMethod`] to the end of `api_access_methods`.
+ pub fn append(&mut self, api_access_method: AccessMethodSetting) {
+ self.access_method_settings.push(api_access_method)
+ }
+
+ /// Remove an [`ApiAccessMethod`] from `api_access_methods`.
+ pub fn remove(&mut self, api_access_method: &Id) {
+ self.retain(|method| method.get_id() != *api_access_method)
+ }
+
+ /// Search for a particular [`AccessMethod`] in `api_access_methods`.
+ pub fn find(&self, element: &Id) -> Option<&AccessMethodSetting> {
+ self.access_method_settings
+ .iter()
+ .find(|api_access_method| *element == api_access_method.get_id())
+ }
+
+ /// Search for a particular [`AccessMethod`] in `api_access_methods`.
+ ///
+ /// If the [`AccessMethod`] is found to be part of `api_access_methods`, a
+ /// mutable reference to that inner element is returned. Otherwise, `None`
+ /// is returned.
+ pub fn find_mut(&mut self, element: &Id) -> Option<&mut AccessMethodSetting> {
+ self.access_method_settings
+ .iter_mut()
+ .find(|api_access_method| *element == api_access_method.get_id())
+ }
+
+ /// Equivalent to [`Vec::retain`].
+ pub fn retain<F>(&mut self, f: F)
+ where
+ F: FnMut(&AccessMethodSetting) -> bool,
+ {
+ self.access_method_settings.retain(f)
+ }
+
+ /// Clone the content of `api_access_methods`.
+ pub fn cloned(&self) -> Vec<AccessMethodSetting> {
+ self.access_method_settings.clone()
+ }
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ access_method_settings: vec![BuiltInAccessMethod::Direct, BuiltInAccessMethod::Bridge]
+ .into_iter()
+ .map(|built_in| {
+ AccessMethodSetting::new(
+ built_in.canonical_name(),
+ true,
+ AccessMethod::from(built_in),
+ )
+ })
+ .collect(),
+ }
+ }
+}
+
+/// API Access Method datastructure
+///
+/// Mirrors the protobuf definition
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct AccessMethodSetting {
+ /// Some unique id (distinct for each `AccessMethod`).
+ id: Id,
+ pub name: String,
+ pub enabled: bool,
+ pub access_method: AccessMethod,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Id(uuid::Uuid);
+
+impl Id {
+ #[allow(clippy::new_without_default)]
+ pub fn new() -> Self {
+ Self(uuid::Uuid::new_v4())
+ }
+ /// Tries to parse a UUID from a raw String. If it is successful, an
+ /// [`Id`] is instantiated.
+ pub fn from_string(id: String) -> Option<Self> {
+ uuid::Uuid::from_str(&id).ok().map(Self)
+ }
+}
+
+impl std::fmt::Display for Id {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+/// Access Method datastructure.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Hash)]
+pub enum AccessMethod {
+ BuiltIn(BuiltInAccessMethod),
+ Custom(CustomAccessMethod),
+}
+
+impl AccessMethodSetting {
+ pub fn new(name: String, enabled: bool, access_method: AccessMethod) -> Self {
+ Self {
+ id: Id::new(),
+ name,
+ enabled,
+ access_method,
+ }
+ }
+
+ /// Just like [`new`], [`with_id`] will create a new [`ApiAccessMethod`].
+ /// But instead of automatically generating a new UUID, the id is instead
+ /// passed as an argument.
+ ///
+ /// This is useful when converting to [`ApiAccessMethod`] from other data
+ /// representations, such as protobuf.
+ ///
+ /// [`new`]: ApiAccessMethod::new
+ /// [`with_id`]: ApiAccessMethod::with_id
+ pub fn with_id(id: Id, name: String, enabled: bool, access_method: AccessMethod) -> Self {
+ Self {
+ id,
+ name,
+ enabled,
+ access_method,
+ }
+ }
+
+ pub fn get_id(&self) -> Id {
+ self.id.clone()
+ }
+
+ pub fn get_name(&self) -> String {
+ self.name.clone()
+ }
+
+ pub fn enabled(&self) -> bool {
+ self.enabled
+ }
+
+ pub fn as_custom(&self) -> Option<&CustomAccessMethod> {
+ self.access_method.as_custom()
+ }
+
+ pub fn is_builtin(&self) -> bool {
+ self.as_custom().is_none()
+ }
+
+ /// Set an API access method to be enabled.
+ pub fn enable(&mut self) {
+ self.enabled = true;
+ }
+
+ /// Set an API access method to be disabled.
+ pub fn disable(&mut self) {
+ self.enabled = false;
+ }
+}
+
+/// Built-In access method datastructure.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Hash)]
+pub enum BuiltInAccessMethod {
+ Direct,
+ Bridge,
+}
+
+/// Custom access method datastructure.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub enum CustomAccessMethod {
+ Shadowsocks(Shadowsocks),
+ Socks5(Socks5),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub enum Socks5 {
+ Local(Socks5Local),
+ Remote(Socks5Remote),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub struct Shadowsocks {
+ pub peer: SocketAddr,
+ pub password: String,
+ /// One of [`shadowsocks_ciphers`].
+ /// Gets validated at a later stage. Is assumed to be valid.
+ ///
+ /// shadowsocks_ciphers: talpid_types::net::openvpn::SHADOWSOCKS_CIPHERS
+ pub cipher: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub struct Socks5Local {
+ pub peer: SocketAddr,
+ /// Port on localhost where the SOCKS5-proxy listens to.
+ pub port: u16,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub struct Socks5Remote {
+ pub peer: SocketAddr,
+ pub authentication: Option<SocksAuth>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub struct SocksAuth {
+ pub username: String,
+ pub password: String,
+}
+
+impl AccessMethod {
+ pub fn as_custom(&self) -> Option<&CustomAccessMethod> {
+ match self {
+ AccessMethod::BuiltIn(_) => None,
+ AccessMethod::Custom(access_method) => Some(access_method),
+ }
+ }
+}
+
+impl BuiltInAccessMethod {
+ pub fn canonical_name(&self) -> String {
+ match self {
+ BuiltInAccessMethod::Direct => "Direct".to_string(),
+ BuiltInAccessMethod::Bridge => "Mullvad Bridges".to_string(),
+ }
+ }
+}
+
+impl Shadowsocks {
+ pub fn new(peer: SocketAddr, cipher: String, password: String) -> Self {
+ Shadowsocks {
+ peer,
+ password,
+ cipher,
+ }
+ }
+
+ /// Like [new()], but tries to parse `ip` and `port` into a [`std::net::SocketAddr`] for you.
+ /// If `ip` or `port` are valid [`Some(Socks5Local)`] is returned, otherwise [`None`].
+ pub fn from_args(ip: String, port: u16, cipher: String, password: String) -> Option<Self> {
+ let peer = SocketAddrV4::new(Ipv4Addr::from_str(&ip).ok()?, port).into();
+ Some(Self::new(peer, cipher, password))
+ }
+}
+
+impl Socks5Local {
+ pub fn new(peer: SocketAddr, port: u16) -> Self {
+ Self { peer, port }
+ }
+
+ /// Like [new()], but tries to parse `ip` and `port` into a [`std::net::SocketAddr`] for you.
+ /// If `ip` or `port` are valid [`Some(Socks5Local)`] is returned, otherwise [`None`].
+ pub fn from_args(ip: String, port: u16, localport: u16) -> Option<Self> {
+ let peer_ip = IpAddr::V4(Ipv4Addr::from_str(&ip).ok()?);
+ let peer = SocketAddr::new(peer_ip, port);
+ Some(Self::new(peer, localport))
+ }
+}
+
+impl Socks5Remote {
+ pub fn new(peer: SocketAddr) -> Self {
+ Self {
+ peer,
+ authentication: None,
+ }
+ }
+
+ /// Like [new()], but tries to parse `ip` and `port` into a [`std::net::SocketAddr`] for you.
+ /// If `ip` or `port` are valid [`Some(Socks5Remote)`] is returned, otherwise [`None`].
+ pub fn from_args(ip: String, port: u16) -> Option<Self> {
+ let peer_ip = IpAddr::V4(Ipv4Addr::from_str(&ip).ok()?);
+ let peer = SocketAddr::new(peer_ip, port);
+ Some(Self::new(peer))
+ }
+
+ /// Like [from_args()], but with authentication.
+ pub fn from_args_with_password(
+ ip: String,
+ port: u16,
+ username: String,
+ password: String,
+ ) -> Option<Self> {
+ let mut socks = Self::from_args(ip, port)?;
+ socks.authentication = Some(SocksAuth { username, password });
+ Some(socks)
+ }
+}
+
+impl From<BuiltInAccessMethod> for AccessMethod {
+ fn from(value: BuiltInAccessMethod) -> Self {
+ AccessMethod::BuiltIn(value)
+ }
+}
+
+impl From<CustomAccessMethod> for AccessMethod {
+ fn from(value: CustomAccessMethod) -> Self {
+ AccessMethod::Custom(value)
+ }
+}
+
+impl From<Shadowsocks> for AccessMethod {
+ fn from(value: Shadowsocks) -> Self {
+ CustomAccessMethod::Shadowsocks(value).into()
+ }
+}
+
+impl From<Socks5> for AccessMethod {
+ fn from(value: Socks5) -> Self {
+ AccessMethod::from(CustomAccessMethod::Socks5(value))
+ }
+}
+
+impl From<Socks5Remote> for AccessMethod {
+ fn from(value: Socks5Remote) -> Self {
+ Socks5::Remote(value).into()
+ }
+}
+
+impl From<Socks5Local> for AccessMethod {
+ fn from(value: Socks5Local) -> Self {
+ Socks5::Local(value).into()
+ }
+}
+
+impl From<Socks5Remote> for Socks5 {
+ fn from(value: Socks5Remote) -> Self {
+ Socks5::Remote(value)
+ }
+}
+
+impl From<Socks5Local> for Socks5 {
+ fn from(value: Socks5Local) -> Self {
+ Socks5::Local(value)
+ }
+}
diff --git a/mullvad-types/src/lib.rs b/mullvad-types/src/lib.rs
index bfac631f82..8aefaeb400 100644
--- a/mullvad-types/src/lib.rs
+++ b/mullvad-types/src/lib.rs
@@ -1,5 +1,6 @@
#![deny(rust_2018_idioms)]
+pub mod access_method;
pub mod account;
pub mod auth_failed;
pub mod custom_list;
diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs
index 3b3ca15014..6ade7dea32 100644
--- a/mullvad-types/src/settings/mod.rs
+++ b/mullvad-types/src/settings/mod.rs
@@ -1,4 +1,5 @@
use crate::{
+ access_method,
custom_list::CustomListsSettings,
relay_constraints::{
BridgeConstraints, BridgeSettings, BridgeState, Constraint, GeographicLocationConstraint,
@@ -76,6 +77,9 @@ pub struct Settings {
/// All of the custom relay lists
#[cfg_attr(target_os = "android", jnix(skip))]
pub custom_lists: CustomListsSettings,
+ /// API access methods.
+ #[cfg_attr(target_os = "android", jnix(skip))]
+ pub api_access_methods: access_method::Settings,
/// If the daemon should allow communication with private (LAN) networks.
pub allow_lan: bool,
/// Extra level of kill switch. When this setting is on, the disconnected state will block
@@ -136,6 +140,7 @@ impl Default for Settings {
split_tunnel: SplitTunnelSettings::default(),
settings_version: CURRENT_SETTINGS_VERSION,
custom_lists: CustomListsSettings::default(),
+ api_access_methods: access_method::Settings::default(),
}
}
}