diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2023-09-20 15:36:01 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2023-10-09 14:40:08 +0200 |
| commit | 43cb757d2cbb396a627fb6b970394a7b73d37dc5 (patch) | |
| tree | 084fcd0a9152d50242ed1934fc5623efe8a0052d /mullvad-cli/src/cmds/api_access.rs | |
| parent | 7fcaad31643ca324c5dd4a17ddf0f445ac679384 (diff) | |
| download | mullvadvpn-43cb757d2cbb396a627fb6b970394a7b73d37dc5.tar.xz mullvadvpn-43cb757d2cbb396a627fb6b970394a7b73d37dc5.zip | |
Cleanup
- General code cleanup
- Fix some typos
- Add some doc comments
- Address several `TODO` comments
- Fix `clippy` warnings
- Removed unused dependency `mullvad-api` from `mullvad-cli`
- Removed unused dependency `rand` from `mullvad-daemon`
- Rename `mullvad proxy` to `mullvad api-access`
- Rename `mullvad_types::api_access_method` -> `mullvad_types::access_method`
- Remove unused `mullvad api-access edit` arguments
- Fix `Display` for `ProxyConfig` printing arguments in the wrong order
- Re-phrase `mullvad api-access test`
- If the API call failed, point out which tested access method that
did not work.
- Fix missing `socket_bypass_tx` value for Android
- Refactor `ApiAccessMethod` proto definition
- Simplify protobuf definitions of `SOCKS5` api access methods
- Remove the `Socks5` enum in favor of implementing `Socks5Local` and
`Socks5Remote` directly.
- Move `enabled` and `name` out of the individual messages and put them
next to the `oneof access_method` in `ApiAccessMethod` proto definition
- Use derived `PartialEq` and `Hash` from `AccessMethod`
- Instead of hand-rolling `Hash` and implementing an ad-hoc version of
half of `PartialEq`, these can now be derived and used as one would
imaging due to the refactoring wherer `name` and `enabled` was moved
out of `AccessMethod` into `ApiAccessMethod`.
Diffstat (limited to 'mullvad-cli/src/cmds/api_access.rs')
| -rw-r--r-- | mullvad-cli/src/cmds/api_access.rs | 475 |
1 files changed, 475 insertions, 0 deletions
diff --git a/mullvad-cli/src/cmds/api_access.rs b/mullvad-cli/src/cmds/api_access.rs new file mode 100644 index 0000000000..921a5da811 --- /dev/null +++ b/mullvad-cli/src/cmds/api_access.rs @@ -0,0 +1,475 @@ +use anyhow::{anyhow, Result}; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::access_method::{ + daemon::{ApiAccessMethodReplace, ApiAccessMethodToggle}, + AccessMethod, ApiAccessMethod, ObfuscationProtocol, +}; +use std::net::IpAddr; + +use clap::{Args, Subcommand}; +use talpid_types::net::openvpn::SHADOWSOCKS_CIPHERS; + +#[derive(Subcommand, Debug, Clone)] +pub enum ApiAccess { + /// List the configured API proxies + List, + /// Add a custom API proxy + #[clap(subcommand)] + Add(AddCustomCommands), + /// Edit an API proxy + Edit(EditCustomCommands), + /// Remove an API proxy + Remove(SelectItem), + /// Enable an API proxy + Enable(SelectItem), + /// Disable an API proxy + Disable(SelectItem), + /// Test an API proxy + Test(SelectItem), + /// Force the use of a specific API proxy. + /// + /// Selecting "Mullvad Bridges" respects your current bridge settings. + Use(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) => { + let enabled = true; + Self::toggle(cmd, enabled).await?; + } + ApiAccess::Disable(cmd) => { + let enabled = false; + Self::toggle(cmd, enabled).await?; + } + ApiAccess::Test(cmd) => { + Self::test(cmd).await?; + } + ApiAccess::Use(cmd) => { + Self::set(cmd).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 proxy = ApiAccessMethod::try_from(cmd)?; + rpc.add_access_method(proxy).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) + .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 api_access_method = Self::get_access_method(&mut rpc, &cmd.item).await?; + let access_method = api_access_method + .as_custom() + .cloned() + .ok_or(anyhow!("Can not edit built-in access method"))?; + + // Create a new access method combining the new params with the previous values + let edited_access_method: ApiAccessMethod = match access_method.access_method { + ObfuscationProtocol::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); + let name = cmd.params.name.unwrap_or(api_access_method.name); + let enabled = api_access_method.enabled; + mullvad_types::access_method::Shadowsocks::from_args(ip, port, cipher, password) + .map(|x| ApiAccessMethod { + name, + enabled, + access_method: AccessMethod::from(x), + }) + } + ObfuscationProtocol::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); + let name = cmd.params.name.unwrap_or(api_access_method.get_name()); + let enabled = api_access_method.enabled(); + mullvad_types::access_method::Socks5Local::from_args(ip, port, local_port).map( + |x| ApiAccessMethod { + name, + enabled, + access_method: AccessMethod::from(x), + }, + ) + } + 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()); + let name = cmd.params.name.unwrap_or(api_access_method.get_name()); + let enabled = api_access_method.enabled(); + mullvad_types::access_method::Socks5Remote::from_args(ip, port).map(|x| { + ApiAccessMethod { + name, + enabled, + access_method: AccessMethod::from(x), + } + }) + } + }, + } + .ok_or(anyhow!( + "Could not edit access method {}, reverting changes.", + cmd.item + ))?; + + rpc.replace_access_method(ApiAccessMethodReplace { + index: cmd.item.as_array_index()?, + access_method: edited_access_method, + }) + .await?; + + Ok(()) + } + + /// Toggle a custom API access method to be enabled or disabled. + async fn toggle(item: SelectItem, enabled: bool) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let access_method = Self::get_access_method(&mut rpc, &item).await?; + rpc.toggle_access_method(ApiAccessMethodToggle { + access_method, + enable: enabled, + }) + .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?; + let access_method = Self::get_access_method(&mut rpc, &item).await?; + let name = access_method.get_name(); + rpc.set_access_method(access_method).await?; + // Make the daemon perform an network request which involves talking to the Mullvad API. + match rpc.get_api_addressess().await { + Ok(_) => println!("Connected to the Mullvad API!"), + Err(_) => println!( + "Could *not* connect to the Mullvad API using access method \"{}\"", + name + ), + } + + Ok(()) + } + + /// Force the use of a specific access method when trying to reach the + /// Mullvad API. If this method fails, the daemon will resume the automatic + /// roll-over behavior (which is the default). + async fn set(item: SelectItem) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let access_method = Self::get_access_method(&mut rpc, &item).await?; + rpc.set_access_method(access_method).await?; + Ok(()) + } + + async fn get_access_method( + rpc: &mut MullvadProxyClient, + item: &SelectItem, + ) -> Result<ApiAccessMethod> { + 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 SOCKS5 proxy + #[clap(subcommand)] + Socks5(Socks5AddCommands), + + /// Configure Shadowsocks proxy + Shadowsocks { + /// An easy to remember name for this custom proxy + name: String, + /// The IP of the remote Shadowsocks server + remote_ip: IpAddr, + /// The port of the remote Shadowsocks server + #[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, + }, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum Socks5AddCommands { + /// 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, + }, + /// Configure a remote SOCKS5 proxy + Remote { + /// An easy to remember name for this custom proxy + name: String, + /// The IP of the remote proxy server + remote_ip: IpAddr, + /// The port of the remote proxy server + remote_port: u16, + }, +} + +/// 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 proxy to edit + #[clap(flatten)] + item: SelectItem, + /// Editing parameters + #[clap(flatten)] + params: EditParams, +} + +#[derive(Args, Debug, Clone)] +pub struct EditParams { + /// Name of the API proxy in the Mullvad client [All] + #[arg(long)] + name: Option<String>, + /// Password for authentication [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, Socks5AddCommands}; + + impl TryFrom<AddCustomCommands> for daemon_types::ApiAccessMethod { + type Error = Error; + + fn try_from(value: AddCustomCommands) -> Result<Self, Self::Error> { + // All api proxies are automatically enabled from the start. + let enabled = true; + Ok(match value { + AddCustomCommands::Socks5(variant) => match variant { + Socks5AddCommands::Local { + local_port, + remote_ip, + remote_port, + name, + } => { + println!("Adding LOCAL SOCKS5-proxy: localhost:{local_port} => {remote_ip}:{remote_port}"); + let socks_proxy = daemon_types::Socks5::Local( + daemon_types::Socks5Local::from_args( + remote_ip.to_string(), + remote_port, + local_port, + ) + .ok_or(anyhow!("Could not create a local Socks5 api proxy"))?, + ); + daemon_types::ApiAccessMethod { + name, + enabled, + access_method: daemon_types::AccessMethod::from(socks_proxy), + } + } + Socks5AddCommands::Remote { + remote_ip, + remote_port, + name, + } => { + println!("Adding REMOTE SOCKS5-proxy: {remote_ip}:{remote_port}"); + let socks_proxy = daemon_types::Socks5::Remote( + daemon_types::Socks5Remote::from_args( + remote_ip.to_string(), + remote_port, + ) + .ok_or(anyhow!("Could not create a remote Socks5 api proxy"))?, + ); + daemon_types::ApiAccessMethod { + name, + enabled, + access_method: socks_proxy.into(), + } + } + }, + AddCustomCommands::Shadowsocks { + remote_ip, + remote_port, + password, + cipher, + name, + } => { + println!( + "Adding Shadowsocks-proxy: {password} @ {remote_ip}:{remote_port} using {cipher}" + ); + let shadowsocks_proxy = daemon_types::Shadowsocks::from_args( + remote_ip.to_string(), + remote_port, + cipher, + password, + ) + .ok_or(anyhow!("Could not create a Shadowsocks api proxy"))?; + + daemon_types::ApiAccessMethod { + name, + enabled, + access_method: shadowsocks_proxy.into(), + } + } + }) + } + } +} + +/// Pretty printing of [`ApiAccessMethod`]s +mod pp { + use mullvad_types::access_method::{ + AccessMethod, ApiAccessMethod, ObfuscationProtocol, Socks5, + }; + + pub struct ApiAccessMethodFormatter<'a> { + api_access_method: &'a ApiAccessMethod, + } + + impl<'a> ApiAccessMethodFormatter<'a> { + pub fn new(api_access_method: &'a ApiAccessMethod) -> ApiAccessMethodFormatter<'a> { + ApiAccessMethodFormatter { api_access_method } + } + } + + 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, "") + } + }; + + // TODO: For debugging purposes only, remove later + writeln!(f, "{:?}", self.api_access_method)?; + + match &self.api_access_method.access_method { + AccessMethod::BuiltIn(method) => { + write!(f, "{}", method.canonical_name())?; + write_status(f, self.api_access_method.enabled()) + } + AccessMethod::Custom(method) => match &method.access_method { + ObfuscationProtocol::Shadowsocks(shadowsocks) => { + write!(f, "{}", self.api_access_method.get_name())?; + 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(()) + } + ObfuscationProtocol::Socks5(socks) => match socks { + Socks5::Remote(remote) => { + write!(f, "{}", self.api_access_method.get_name())?; + write_status(f, self.api_access_method.enabled())?; + writeln!(f)?; + print_option!("Protocol", "Socks5"); + print_option!("Peer", remote.peer); + Ok(()) + } + Socks5::Local(local) => { + write!(f, "{}", self.api_access_method.get_name())?; + 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(()) + } + }, + }, + } + } + } +} |
