diff --git a/Cargo.lock b/Cargo.lock index 6ba669fe5e8c..b3b830e0ce20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1805,6 +1805,7 @@ dependencies = [ "talpid-types", "tokio", "tokio-rustls", + "tokio-socks", ] [[package]] @@ -3798,6 +3799,18 @@ dependencies = [ "tokio", ] +[[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" diff --git a/mullvad-api/Cargo.toml b/mullvad-api/Cargo.toml index 32d725a2c6fa..83d8bf0723e9 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 e8f7fb889cab..17d9f7f0d850 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>, + ) -> Result { + 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( + first_hop: SocketAddr, + hostname: &str, + make_proxy_stream: ProxyFactory, + #[cfg(target_os = "android")] socket_bypass_tx: Option>, + ) -> Result + where + ProxyFactory: FnOnce(TcpStream) -> ProxyFuture, + ProxyFuture: Future>, + 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 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 for InnerConnectionMode { type Error = ProxyConfigError; fn try_from(config: ApiConnectionMode) -> Result { + 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, address_cache: AddressCache, abort_notify: Arc, - proxy_context: SharedContext, #[cfg(target_os = "android")] socket_bypass_tx: Option>, } @@ -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>, @@ -281,7 +445,6 @@ impl Service 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 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 1e6ab41f8097..44a2309587e5 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 { 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 0fc31353a7fa..674bcf8c4eb0 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>, ), Reset, - NextApiConfig, + NextApiConfig(oneshot::Sender>), } /// 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 000000000000..a03d3ba11fbb --- /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::::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(¤t); + access_method_formatter.settings.write_enabled = false; + println!("{}", access_method_formatter); + Ok(()) + } + + async fn get_access_method( + rpc: &mut MullvadProxyClient, + item: &SelectItem, + ) -> Result { + 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, + /// 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 { + 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, + /// Username for authentication [Socks5 (Remote proxy)] + #[arg(long)] + username: Option, + /// Password for authentication [Socks5 (Remote proxy), Shadowsocks] + #[arg(long)] + password: Option, + /// Cipher to use [Shadowsocks] + #[arg(value_parser = SHADOWSOCKS_CIPHERS, long)] + cipher: Option, + /// The IP of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks] + #[arg(long)] + ip: Option, + /// The port of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks] + #[arg(long)] + port: Option, + /// The port that the server on localhost is listening on [Socks5 (Local proxy)] + #[arg(long)] + local_port: Option, +} + +/// 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 for daemon_types::AccessMethod { + type Error = Error; + + fn try_from(value: AddCustomCommands) -> Result { + 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 c63a98113310..88e4184f0729 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 41f164397093..7a09a4eebdf7 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 000000000000..013cf9d7ce51 --- /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 Daemon +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 { + 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 { + 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 67f80ca2356d..c548f0a293c4 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 + Send>>>, + connection_modes: Arc>, } 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, + ) -> 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> { + 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 + Send>, + next: Option, + current: AccessMethodSetting, +} + +impl ConnectionModesIterator { + pub fn new(access_methods: Vec) -> 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) { + self.available_modes = Self::cycle(access_methods) + } + + fn cycle( + access_methods: Vec, + ) -> Box + 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 { + 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>| tx.upgrade()) + let tunnel_tx = if let Some(tunnel_tx) = { inner_tx.lock().unwrap().as_ref() } + .and_then(|tx: &Weak>| tx.upgrade()) { tunnel_tx } else { diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 81ebe1050c54..f7343acc877a 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, Error>), + /// Add API access methods + AddApiAccessMethod( + ResponseTx, + 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), + /// Get the addresses of all known API endpoints + GetApiAddresses(ResponseTx, Error>), /// Get information about the currently running and latest app versions GetVersionInfo(oneshot::Sender>), /// Return whether the daemon is performing post-upgrade tasks @@ -554,6 +578,7 @@ pub struct Daemon { account_history: account_history::AccountHistory, device_checker: device::TunnelStateChangeHandler, account_manager: device::AccountManagerHandle, + connection_modes: Arc>, 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, 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, + 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) { + 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, 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) { 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 53586286403c..993f0f9ece0b 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, + ) -> ServiceResult { + 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) -> 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) -> 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, + ) -> 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 { + 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 { + 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 f4b3eb961d11..24bfe2284b7f 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 a1ddc5e39afa..417083e16123 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> { + 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 { + 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 { + 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> { + self.0 + .get_api_addresses(()) + .await + .map_err(Error::Rpc) + .map(tonic::Response::into_inner) + .and_then(|api_addresses| { + Vec::::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> { use futures::TryStreamExt; diff --git a/mullvad-management-interface/src/lib.rs b/mullvad-management-interface/src/lib.rs index cf1a7988786e..c9414d03bf67 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 000000000000..8907c4da2947 --- /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 for proto::ApiAccessMethodSettings { + fn from(settings: access_method::Settings) -> Self { + proto::ApiAccessMethodSettings::from(&settings) + } + } + + impl TryFrom for access_method::Settings { + type Error = FromProtobufTypeError; + + fn try_from(settings: proto::ApiAccessMethodSettings) -> Result { + Ok(Self { + access_method_settings: settings + .access_method_settings + .iter() + .map(access_method::AccessMethodSetting::try_from) + .collect::, _>>()?, + }) + } + } +} + +/// 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 for AccessMethodSetting { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::AccessMethodSetting) -> Result { + 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 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 for AccessMethod { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::AccessMethod) -> Result { + 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 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 for AccessMethod { + fn from(_value: proto::access_method::Direct) -> Self { + AccessMethod::from(BuiltInAccessMethod::Direct) + } + } + + impl From for AccessMethod { + fn from(_value: proto::access_method::Bridges) -> Self { + AccessMethod::from(BuiltInAccessMethod::Bridge) + } + } + + impl TryFrom for AccessMethod { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::access_method::Socks5Local) -> Result { + 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 for AccessMethod { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::access_method::Socks5Remote) -> Result { + 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 for AccessMethod { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::access_method::Shadowsocks) -> Result { + 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 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 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 for proto::Uuid { + fn from(value: Id) -> Self { + proto::Uuid { + value: value.to_string(), + } + } + } + + impl TryFrom for Id { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::Uuid) -> Result { + Self::from_string(value.value).ok_or(FromProtobufTypeError::InvalidArgument( + "Could not parse UUID message from protobuf", + )) + } + } + + impl From for proto::access_method::SocksAuth { + fn from(value: SocksAuth) -> Self { + proto::access_method::SocksAuth { + username: value.username, + password: value.password, + } + } + } + + impl From 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 { + 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 d2e8b60265a3..dd6fcd450167 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 d0dcc975d0fc..ea5dcf99a53d 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 for proto::IpVersionConstraint { } } +impl TryFrom for Vec { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::ApiAddresses) -> Result { + value + .api_addresses + .iter() + .map(|api_address| api_address.parse::()) + .collect::>() + .map_err(|_| FromProtobufTypeError::InvalidArgument("Invalid socket address")) + } +} + +impl From> for proto::ApiAddresses { + fn from(value: Vec) -> Self { + Self { + api_addresses: value.iter().map(SocketAddr::to_string).collect(), + } + } +} + pub fn try_tunnel_type_from_i32( tunnel_type: i32, ) -> Result { diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs index f123b417553b..98c195d93562 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 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 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 000000000000..30c1f25192ae --- /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, +} + +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(&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 { + 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 { + 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, +} + +#[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 { + 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 { + 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 { + 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 { + let mut socks = Self::from_args(ip, port)?; + socks.authentication = Some(SocksAuth { username, password }); + Some(socks) + } +} + +impl From for AccessMethod { + fn from(value: BuiltInAccessMethod) -> Self { + AccessMethod::BuiltIn(value) + } +} + +impl From for AccessMethod { + fn from(value: CustomAccessMethod) -> Self { + AccessMethod::Custom(value) + } +} + +impl From for AccessMethod { + fn from(value: Shadowsocks) -> Self { + CustomAccessMethod::Shadowsocks(value).into() + } +} + +impl From for AccessMethod { + fn from(value: Socks5) -> Self { + AccessMethod::from(CustomAccessMethod::Socks5(value)) + } +} + +impl From for AccessMethod { + fn from(value: Socks5Remote) -> Self { + Socks5::Remote(value).into() + } +} + +impl From for AccessMethod { + fn from(value: Socks5Local) -> Self { + Socks5::Local(value).into() + } +} + +impl From for Socks5 { + fn from(value: Socks5Remote) -> Self { + Socks5::Remote(value) + } +} + +impl From 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 bfac631f82ab..8aefaeb4000b 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 3b3ca15014ef..6ade7dea3271 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(), } } }