diff options
| author | David Lönnhager <david.l@mullvad.net> | 2023-05-03 11:20:31 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2023-05-03 11:20:31 +0200 |
| commit | 49ea114adddba1a1db6ffc6c440e743c01797a47 (patch) | |
| tree | 66f1bf1e3e1d208e233e5622045503abe85a3a89 /mullvad-cli | |
| parent | beaa6d3b80d9c9dfed99c710c793830db3ddc7ec (diff) | |
| parent | aade46c9c73c874e4153caa450e713d8f8b37760 (diff) | |
| download | mullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.tar.xz mullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.zip | |
Merge branch 'update-clap'
Diffstat (limited to 'mullvad-cli')
26 files changed, 1882 insertions, 3078 deletions
diff --git a/mullvad-cli/Cargo.toml b/mullvad-cli/Cargo.toml index 2ba23a65ca..ab3e0ac03f 100644 --- a/mullvad-cli/Cargo.toml +++ b/mullvad-cli/Cargo.toml @@ -12,17 +12,17 @@ name = "mullvad" path = "src/main.rs" [dependencies] +anyhow = "1.0" base64 = "0.13" chrono = { version = "0.4.19", features = ["serde"] } -clap = { version = "3.0", features = ["cargo"] } -err-derive = "0.3.1" +clap = { version = "4.2.7", features = ["cargo", "derive"] } env_logger = "0.10.0" futures = "0.3" natord = "1.0.9" serde = "1.0" itertools = "0.10" -mullvad-types = { path = "../mullvad-types" } +mullvad-types = { path = "../mullvad-types", features = ["clap"] } mullvad-paths = { path = "../mullvad-paths" } mullvad-version = { path = "../mullvad-version" } talpid-types = { path = "../talpid-types" } @@ -31,7 +31,7 @@ mullvad-management-interface = { path = "../mullvad-management-interface" } tokio = { version = "1.8", features = [ "rt-multi-thread" ] } [target.'cfg(all(unix, not(target_os = "android")))'.dependencies] -clap_complete = { version = "3.0" } +clap_complete = { version = "4.2.1" } [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/mullvad-cli/src/cmds/account.rs b/mullvad-cli/src/cmds/account.rs index 77859990ea..d46469484f 100644 --- a/mullvad-cli/src/cmds/account.rs +++ b/mullvad-cli/src/cmds/account.rs @@ -1,183 +1,131 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::{anyhow, Result}; +use clap::Subcommand; use itertools::Itertools; -use mullvad_management_interface::{ - types::{self, Timestamp}, - Code, ManagementServiceClient, Status, -}; -use mullvad_types::{account::AccountToken, device::Device}; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{account::AccountToken, device::DeviceState}; use std::io::{self, Write}; const NOT_LOGGED_IN_MESSAGE: &str = "Not logged in on any account"; const REVOKED_MESSAGE: &str = "The current device has been revoked"; -const DEVICE_NOT_FOUND_ERROR: &str = "There is no such device"; -const INVALID_ACCOUNT_ERROR: &str = "The account does not exist"; -const TOO_MANY_DEVICES_ERROR: &str = - "There are too many devices on this account. Revoke one to log in"; -const ALREADY_LOGGED_IN_ERROR: &str = - "You are already logged in. Please log out before creating a new account"; -pub struct Account; +#[derive(Subcommand, Debug)] +pub enum Account { + /// Create and log in on a new account + Create, -#[mullvad_management_interface::async_trait] -impl Command for Account { - fn name(&self) -> &'static str { - "account" - } + /// Log in on an account + Login { + /// The Mullvad account token to configure the client with + account: Option<String>, + }, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control and display information about your Mullvad account") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("create").about("Create and log in to a new account")) - .subcommand( - clap::App::new("login").about("Log in to an account").arg( - clap::Arg::new("account") - .help("The Mullvad account token to configure the client with") - .required(false), - ), - ) - .subcommand(clap::App::new("logout").about("Log out of the current account")) - .subcommand( - clap::App::new("get") - .about("Display information about the current account") - .arg( - clap::Arg::new("verbose") - .long("verbose") - .short('v') - .help("Enables verbose output"), - ), - ) - .subcommand( - clap::App::new("list-devices") - .about("List devices associated with an account") - .arg( - clap::Arg::new("account") - .help("Mullvad account number") - .long("account") - .takes_value(true), - ) - .arg( - clap::Arg::new("verbose") - .long("verbose") - .short('v') - .help("Enables verbose output"), - ), - ) - .subcommand( - clap::App::new("revoke-device") - .about("Revoke a device associated with an account") - .arg( - clap::Arg::new("account") - .help("Mullvad account number") - .long("account") - .takes_value(true), - ) - .arg( - clap::Arg::new("device") - .help("Name or ID of the device to revoke") - .required(true), - ), - ) - .subcommand( - clap::App::new("redeem").about("Redeems a voucher").arg( - clap::Arg::new("voucher") - .help("The Mullvad voucher code to be submitted") - .required(true), - ), - ) - } + /// Log out of the current account + Logout, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(_matches) = matches.subcommand_matches("create") { - self.create().await - } else if let Some(set_matches) = matches.subcommand_matches("login") { - self.login(parse_token_else_stdin(set_matches)).await - } else if let Some(_matches) = matches.subcommand_matches("logout") { - self.logout().await - } else if let Some(set_matches) = matches.subcommand_matches("get") { - let verbose = set_matches.is_present("verbose"); - self.get(verbose).await - } else if let Some(set_matches) = matches.subcommand_matches("list-devices") { - self.list_devices(set_matches).await - } else if let Some(set_matches) = matches.subcommand_matches("revoke-device") { - self.revoke_device(set_matches).await - } else if let Some(matches) = matches.subcommand_matches("redeem") { - let voucher = matches.value_of_t_or_exit("voucher"); - self.redeem_voucher(voucher).await - } else { - unreachable!("No account command given"); - } - } + /// Display information about the current account + Get { + /// Enable verbose output + #[arg(long, short = 'v')] + verbose: bool, + }, + + /// List devices associated with an account + ListDevices { + /// Mullvad account number (current account if not specified) + #[arg(long, short = 'a')] + account: Option<String>, + + /// Enable verbose output + #[arg(long, short = 'v')] + verbose: bool, + }, + + /// Revoke a device associated with an account + RevokeDevice { + /// Name or UID of the device to revoke + device: String, + + /// Mullvad account number (current account if not specified) + #[arg(long, short = 'a')] + account: Option<String>, + }, + + /// Redeem a voucher + Redeem { + /// Voucher code to submit + voucher: String, + }, } impl Account { - async fn create(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.create_new_account(()).await.map_err(map_device_error)?; + pub async fn handle(self) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + match self { + Account::Create => Self::create(&mut rpc).await, + Account::Login { account } => { + Self::login( + &mut rpc, + account.unwrap_or_else(|| from_stdin("Enter an account number: ")), + ) + .await + } + Account::Logout => Self::logout(&mut rpc).await, + Account::Get { verbose } => Self::get(&mut rpc, verbose).await, + Account::ListDevices { account, verbose } => { + Self::list_devices(&mut rpc, account, verbose).await + } + Account::RevokeDevice { device, account } => { + Self::revoke_device(&mut rpc, device, account).await + } + Account::Redeem { voucher } => Self::redeem_voucher(&mut rpc, voucher).await, + } + } + + async fn create(rpc: &mut MullvadProxyClient) -> Result<()> { + rpc.create_new_account().await?; println!("New account created!"); - self.get(false).await + Self::get(rpc, false).await } - async fn login(&self, token: AccountToken) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.login_account(token.clone()) - .await - .map_err(map_device_error)?; + async fn login(rpc: &mut MullvadProxyClient, token: AccountToken) -> Result<()> { + rpc.login_account(token.clone()).await?; println!("Mullvad account \"{token}\" set"); Ok(()) } - async fn logout(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.logout_account(()).await?; + async fn logout(rpc: &mut MullvadProxyClient) -> Result<()> { + rpc.logout_account().await?; println!("Removed device from Mullvad account"); Ok(()) } - async fn get(&self, verbose: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; + async fn get(rpc: &mut MullvadProxyClient, verbose: bool) -> Result<()> { + let _ = rpc.update_device().await; - let _ = rpc.update_device(()).await; + let state = rpc.get_device().await?; - let state = rpc - .get_device(()) - .await - .map_err(map_device_error)? - .into_inner(); - - use types::device_state::State; - - match State::from_i32(state.state).unwrap() { - State::LoggedIn => { - let device = state.device.expect("Device must be provided if logged in"); + match state { + DeviceState::LoggedIn(device) => { println!("Mullvad account: {}", device.account_token); - let inner_device = Device::try_from(device.device.unwrap()).unwrap(); - println!("Device name : {}", inner_device.pretty_name()); + println!("Device name : {}", device.device.pretty_name()); if verbose { - println!("Device id : {}", inner_device.id); - println!("Device pubkey : {}", inner_device.pubkey); - println!( - "Device created : {}", - inner_device.created.with_timezone(&chrono::Local) - ); - for port in inner_device.ports { + println!("Device id : {}", device.device.id); + println!("Device pubkey : {}", device.device.pubkey); + println!("Device created : {}", device.device.created,); + for port in device.device.ports { println!("Device port : {port}"); } } - let expiry = rpc - .get_account_data(device.account_token) - .await - .map_err(|error| Error::RpcFailedExt("Failed to fetch account data", error))? - .into_inner(); + let expiry = rpc.get_account_data(device.account_token).await?; println!( "Expires at : {}", - Self::format_expiry(&expiry.expiry.unwrap()) + expiry.expiry.with_timezone(&chrono::Local), ); } - State::LoggedOut => { + DeviceState::LoggedOut => { println!("{NOT_LOGGED_IN_MESSAGE}"); } - State::Revoked => { + DeviceState::Revoked => { println!("{REVOKED_MESSAGE}"); } } @@ -185,23 +133,17 @@ impl Account { Ok(()) } - async fn list_devices(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let token = self.parse_account_else_current(&mut rpc, matches).await?; - let mut device_list = rpc - .list_devices(token) - .await - .map_err(map_device_error)? - .into_inner(); - - let verbose = matches.is_present("verbose"); + async fn list_devices( + rpc: &mut MullvadProxyClient, + account: Option<String>, + verbose: bool, + ) -> Result<()> { + let token = account_else_current(rpc, account).await?; + let mut device_list = rpc.list_devices(token).await?; println!("Devices on the account:"); - device_list - .devices - .sort_unstable_by_key(|dev| dev.created.as_ref().map(|dt| dt.seconds).unwrap_or(0)); - for device in device_list.devices { - let device = Device::try_from(device.clone()).unwrap(); + device_list.sort_unstable_by_key(|dev| dev.created.timestamp()); + for device in device_list { if verbose { println!(); println!("Name : {}", device.pretty_name()); @@ -222,146 +164,80 @@ impl Account { Ok(()) } - async fn revoke_device(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; + async fn revoke_device( + rpc: &mut MullvadProxyClient, + device: String, + account: Option<String>, + ) -> Result<()> { + let token = account_else_current(rpc, account).await?; - let token = self.parse_account_else_current(&mut rpc, matches).await?; - let device_to_revoke = parse_device_name(matches); - - let device_list = rpc - .list_devices(token.clone()) - .await - .map_err(map_device_error)? - .into_inner(); + let device_list = rpc.list_devices(token.clone()).await?; let device_id = device_list - .devices .into_iter() .find(|dev| { - dev.name.eq_ignore_ascii_case(&device_to_revoke) - || dev.id.eq_ignore_ascii_case(&device_to_revoke) + dev.name.eq_ignore_ascii_case(&device) || dev.id.eq_ignore_ascii_case(&device) }) .map(|dev| dev.id) - .ok_or(Error::Other(DEVICE_NOT_FOUND_ERROR))?; + .ok_or(mullvad_management_interface::Error::DeviceNotFound)?; - rpc.remove_device(types::DeviceRemoval { - account_token: token, - device_id, - }) - .await - .map_err(map_device_error)?; + rpc.remove_device(token, device_id).await?; println!("Removed device"); Ok(()) } - async fn parse_account_else_current( - &self, - rpc: &mut ManagementServiceClient, - matches: &clap::ArgMatches, - ) -> Result<String> { - match matches.value_of("account").map(str::to_string) { - Some(token) => Ok(token), - None => { - let state = rpc - .get_device(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain device", error))? - .into_inner(); - if state.state != types::device_state::State::LoggedIn as i32 { - return Err(Error::Other("Log in or specify an account")); - } - Ok(state.device.unwrap().account_token) - } - } - } - - async fn redeem_voucher(&self, mut voucher: String) -> Result<()> { - let mut rpc = new_rpc_client().await?; + async fn redeem_voucher(rpc: &mut MullvadProxyClient, mut voucher: String) -> Result<()> { voucher.retain(|c| c.is_alphanumeric()); - match rpc.submit_voucher(voucher).await { - Ok(submission) => { - let submission = submission.into_inner(); - println!( - "Added {} to the account", - Self::format_duration(submission.seconds_added) - ); - println!( - "New expiry date: {}", - Self::format_expiry(&submission.new_expiry.unwrap()) - ); - Ok(()) - } - Err(err) => { - match err.code() { - Code::NotFound | Code::ResourceExhausted => { - eprintln!("Failed to submit voucher: {}", err.message()); - } - _ => return Err(Error::RpcFailed(err)), - } - std::process::exit(1); - } - } - } - - fn format_duration(seconds: u64) -> String { - let dur = chrono::Duration::seconds(seconds as i64); - if dur.num_days() > 0 { - format!("{} days", dur.num_days()) - } else if dur.num_hours() > 0 { - format!("{} hours", dur.num_hours()) - } else if dur.num_minutes() > 0 { - format!("{} minutes", dur.num_minutes()) - } else { - format!("{} seconds", dur.num_seconds()) - } - } - - fn format_expiry(expiry: &Timestamp) -> String { - let ndt = chrono::NaiveDateTime::from_timestamp(expiry.seconds, expiry.nanos as u32); - let utc = chrono::DateTime::<chrono::Utc>::from_utc(ndt, chrono::Utc); - utc.with_timezone(&chrono::Local).to_string() + let submission = rpc.submit_voucher(voucher).await?; + println!( + "Added {} to the account", + format_duration(submission.time_added) + ); + println!( + "New expiry date: {}", + submission.new_expiry.with_timezone(&chrono::Local), + ); + Ok(()) } } -fn map_device_error(error: Status) -> Error { - match error.code() { - Code::ResourceExhausted => Error::Other(TOO_MANY_DEVICES_ERROR), - Code::Unauthenticated => Error::Other(INVALID_ACCOUNT_ERROR), - Code::AlreadyExists => Error::Other(ALREADY_LOGGED_IN_ERROR), - Code::NotFound => Error::Other(DEVICE_NOT_FOUND_ERROR), - _other => Error::RpcFailed(error), +async fn account_else_current( + rpc: &mut MullvadProxyClient, + token: Option<String>, +) -> Result<String> { + match token { + Some(account) => Ok(account), + None => { + let state = rpc.get_device().await?; + match state { + DeviceState::LoggedIn(account) => Ok(account.account_token), + _ => Err(anyhow!("Log in or specify an account")), + } + } } } -fn parse_token_else_stdin(matches: &clap::ArgMatches) -> String { - parse_from_match_else_stdin("Enter account number: ", "account", matches) - .split_whitespace() - .join("") -} - -fn parse_device_name(matches: &clap::ArgMatches) -> String { - parse_from_match_else_stdin("Enter device name: ", "device", matches) - .trim() - .to_string() +fn from_stdin(prompt_str: &'static str) -> String { + let mut val = String::new(); + io::stdout() + .write_all(prompt_str.as_bytes()) + .expect("Failed to write to STDOUT"); + let _ = io::stdout().flush(); + io::stdin() + .read_line(&mut val) + .expect("Failed to read from STDIN"); + val.split_whitespace().join("") } -fn parse_from_match_else_stdin( - prompt_str: &'static str, - key: &'static str, - matches: &clap::ArgMatches, -) -> String { - match matches.value_of(key) { - Some(device) => device.to_string(), - None => { - let mut val = String::new(); - io::stdout() - .write_all(prompt_str.as_bytes()) - .expect("Failed to write to STDOUT"); - let _ = io::stdout().flush(); - io::stdin() - .read_line(&mut val) - .expect("Failed to read from STDIN"); - val - } +fn format_duration(seconds: u64) -> String { + let dur = chrono::Duration::seconds(seconds as i64); + if dur.num_days() > 0 { + format!("{} days", dur.num_days()) + } else if dur.num_hours() > 0 { + format!("{} hours", dur.num_hours()) + } else if dur.num_minutes() > 0 { + format!("{} minutes", dur.num_minutes()) + } else { + format!("{} seconds", dur.num_seconds()) } } diff --git a/mullvad-cli/src/cmds/auto_connect.rs b/mullvad-cli/src/cmds/auto_connect.rs index 067dff9a6b..4fe578f820 100644 --- a/mullvad-cli/src/cmds/auto_connect.rs +++ b/mullvad-cli/src/cmds/auto_connect.rs @@ -1,53 +1,36 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct AutoConnect; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for AutoConnect { - fn name(&self) -> &'static str { - "auto-connect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control the daemon auto-connect setting") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about("Change auto-connect setting") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand(clap::App::new("get").about("Display the current auto-connect setting")) - } +#[derive(Subcommand, Debug)] +pub enum AutoConnect { + /// Display the current auto-connect setting + Get, + /// Change auto-connect setting + Set { policy: BooleanOption }, +} - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - let auto_connect = set_matches.value_of("policy").expect("missing policy"); - self.set(auto_connect == "on").await - } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get().await - } else { - unreachable!("No auto-connect command given"); +impl AutoConnect { + pub async fn handle(self) -> Result<()> { + match self { + AutoConnect::Get => Self::get().await, + AutoConnect::Set { policy } => Self::set(policy).await, } } -} -impl AutoConnect { - async fn set(&self, auto_connect: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_auto_connect(auto_connect).await?; + async fn set(policy: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_auto_connect(*policy).await?; println!("Changed auto-connect setting"); Ok(()) } - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let auto_connect = rpc.get_settings(()).await?.into_inner().auto_connect; - println!("Autoconnect: {}", if auto_connect { "on" } else { "off" }); + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let auto_connect = BooleanOption::from(rpc.get_settings().await?.auto_connect); + println!("Autoconnect: {auto_connect}"); Ok(()) } } diff --git a/mullvad-cli/src/cmds/beta_program.rs b/mullvad-cli/src/cmds/beta_program.rs index 77aa73ace3..ba9c72616e 100644 --- a/mullvad-cli/src/cmds/beta_program.rs +++ b/mullvad-cli/src/cmds/beta_program.rs @@ -1,61 +1,43 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::{anyhow, Result}; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct BetaProgram; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for BetaProgram { - fn name(&self) -> &'static str { - "beta-program" - } +#[derive(Subcommand, Debug)] +pub enum BetaProgram { + /// Get beta notifications setting + Get, + /// Change beta notifications setting + Set { policy: BooleanOption }, +} - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Receive notifications about beta updates") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about("Change beta notifications setting") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand(clap::App::new("get").about("Get beta notifications setting")) +impl BetaProgram { + pub async fn handle(self) -> Result<()> { + match self { + BetaProgram::Get => Self::get().await, + BetaProgram::Set { policy } => Self::set(policy).await, + } } - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("get", _)) => { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - let enabled_str = if settings.show_beta_releases { - "on" - } else { - "off" - }; - println!("Beta program: {enabled_str}"); - Ok(()) - } - Some(("set", matches)) => { - let enable_str = matches.value_of("policy").expect("missing policy"); - let enable = enable_str == "on"; + async fn set(state: BooleanOption) -> Result<()> { + if !*state && mullvad_version::VERSION.contains("beta") { + return Err(anyhow!( + "The beta program must be enabled while running a beta version", + )); + } - if !enable && mullvad_version::VERSION.contains("beta") { - return Err(Error::InvalidCommand( - "The beta program must be enabled while running a beta version", - )); - } + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_show_beta_releases(*state).await?; - let mut rpc = new_rpc_client().await?; - rpc.set_show_beta_releases(enable).await?; + println!("Beta program: {state}"); + Ok(()) + } - println!("Beta program: {enable_str}"); - Ok(()) - } - _ => { - unreachable!("unhandled command"); - } - } + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let opt = BooleanOption::from(rpc.get_settings().await?.show_beta_releases); + println!("Beta program: {opt}"); + Ok(()) } } diff --git a/mullvad-cli/src/cmds/block_when_disconnected.rs b/mullvad-cli/src/cmds/block_when_disconnected.rs deleted file mode 100644 index 36b9a20e37..0000000000 --- a/mullvad-cli/src/cmds/block_when_disconnected.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::{new_rpc_client, Command, Result}; - -pub struct BlockWhenDisconnected; - -#[mullvad_management_interface::async_trait] -impl Command for BlockWhenDisconnected { - fn name(&self) -> &'static str { - "lockdown-mode" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control if the system service should block network access when disconnected from VPN") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about("Change the lockdown mode setting") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand( - clap::App::new("get") - .about("Display the current lockdown mode setting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - let block_when_disconnected = set_matches.value_of("policy").expect("missing policy"); - self.set(block_when_disconnected == "on").await - } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get().await - } else { - unreachable!("No block-when-disconnected command given"); - } - } -} - -impl BlockWhenDisconnected { - async fn set(&self, block_when_disconnected: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_block_when_disconnected(block_when_disconnected) - .await?; - println!("Changed lockdown mode setting"); - Ok(()) - } - - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let block_when_disconnected = rpc - .get_settings(()) - .await? - .into_inner() - .block_when_disconnected; - println!( - "Network traffic will be {} when the VPN is disconnected", - if block_when_disconnected { - "blocked" - } else { - "allowed" - } - ); - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/bridge.rs b/mullvad-cli/src/cmds/bridge.rs index d7291a810d..1652a0cfb2 100644 --- a/mullvad-cli/src/cmds/bridge.rs +++ b/mullvad-cli/src/cmds/bridge.rs @@ -1,224 +1,238 @@ -use crate::{location, new_rpc_client, Command, Error, Result}; - -use mullvad_management_interface::types; -use mullvad_types::relay_constraints::{ - BridgeConstraints, BridgeSettings, BridgeState, Constraint, LocationConstraint, +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{ + relay_constraints::{ + BridgeConstraints, BridgeSettings, BridgeState, Constraint, LocationConstraint, Ownership, + Provider, Providers, + }, + relay_list::RelayEndpointData, }; +use std::net::{IpAddr, SocketAddr}; use talpid_types::net::openvpn::{self, SHADOWSOCKS_CIPHERS}; -use std::{convert::TryFrom, net::SocketAddr}; +use super::relay_constraints::LocationArgs; -pub struct Bridge; +#[derive(Subcommand, Debug)] +pub enum Bridge { + /// Get current bridge settings + Get, + /// Set bridge state and settings, such as provider + #[clap(subcommand)] + Set(SetCommands), + /// List available bridge relays + List, +} -#[mullvad_management_interface::async_trait] -impl Command for Bridge { - fn name(&self) -> &'static str { - "bridge" - } +#[derive(Subcommand, Debug, Clone)] +pub enum SetCommands { + /// Specify whether to use a bridge + State { policy: BridgeState }, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about( - "Manage use of bridges, socks proxies and Shadowsocks for OpenVPN. \ - Can make OpenVPN tunnels use Shadowsocks via one of the Mullvad bridge servers. \ - Can also make OpenVPN connect through any custom SOCKS5 proxy. \ - These settings also affect how the app reaches the API over Shadowsocks.", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_bridge_set_subcommand()) - .subcommand(clap::App::new("get").about("Get current bridge settings and state")) - .subcommand(clap::App::new("list").about("List bridge relays")) - } + /// Set country or city to select relays from. Use the 'list' + /// command to show available alternatives. + Location(LocationArgs), - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("set", set_matches)) => Self::handle_set(set_matches).await, - Some(("get", _)) => Self::handle_get().await, - Some(("list", _)) => Self::list_bridge_relays().await, - _ => unreachable!("unhandled command"), - } - } -} + /// Set hosting provider(s) to select relays from. The 'list' + /// command shows the available relays and their providers. + Provider { + /// Either 'any', or provider to select from. + #[arg(required(true), num_args = 1..)] + providers: Vec<Provider>, + }, -fn create_bridge_set_subcommand() -> clap::App<'static> { - clap::App::new("set") - .about("Set bridge state and settings") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_set_state_subcommand()) - .subcommand(create_set_custom_settings_subcommand()) - .subcommand( - clap::App::new("provider") - .about( - "Set hosting provider(s) to select bridge relays from. The 'list' \ - command shows the available relays and their providers.", - ) - .arg( - clap::Arg::new("provider") - .help("The hosting provider(s) to use, or 'any' for no preference.") - .multiple_values(true) - .required(true), - ), - ) - .subcommand( - clap::App::new("ownership") - .about( - "Filters bridges based on ownership. The 'list' \ - command shows the available relays and whether they're rented.", - ) - .arg( - clap::Arg::new("ownership") - .help("Ownership preference, or 'any' for no preference.") - .possible_values(["any", "owned", "rented"]) - .required(true), - ), - ) - .subcommand(location::get_subcommand().about( - "Set country or city to select bridge relays from. Use the 'list' \ - command to show available alternatives.", - )) + /// Filter relays based on ownership. The 'list' command + /// shows the available relays and whether they're rented. + Ownership { + /// Servers to select from: 'any', 'owned', or 'rented'. + ownership: Constraint<Ownership>, + }, + + /// Configure a SOCKS5 proxy + #[clap(subcommand)] + Custom(SetCustomCommands), } -fn create_set_custom_settings_subcommand() -> clap::App<'static> { - #[allow(unused_mut)] - let mut local_subcommand = clap::App::new("local") - .about("Registers a local SOCKS5 proxy") - .arg( - clap::Arg::new("local-port") - .help("Specifies the port the local proxy server is listening on") - .required(true) - .index(1), +#[derive(Subcommand, Debug, Clone)] +pub enum SetCustomCommands { + /// Configure a local SOCKS5 proxy + #[cfg_attr( + target_os = "linux", + clap( + about = "Registers a local SOCKS5 proxy. The server must be excluded using \ + 'mullvad-exclude', or `SO_MARK` must be set to '0x6d6f6c65', in order \ + to bypass firewall restrictions" ) - .arg( - clap::Arg::new("remote-ip") - .help("Specifies the IP of the proxy server peer") - .required(true) - .index(2), + )] + #[cfg_attr( + target_os = "windows", + clap( + about = "Registers a local SOCKS5 proxy. The server must be excluded using \ + split tunneling in order to bypass firewall restrictions" ) - .arg( - clap::Arg::new("remote-port") - .help("Specifies the port of the proxy server peer") - .required(true) - .index(3), - ); + )] + #[cfg_attr( + target_os = "macos", + clap( + about = "Registers a local SOCKS5 proxy. The server must run as root to bypass \ + firewall restrictions" + ) + )] + Local { + /// 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, + }, - #[cfg(target_os = "linux")] - { - local_subcommand = local_subcommand.about( - "Registers a local SOCKS5 proxy. The server must be excluded using \ - 'mullvad-exclude', or `SO_MARK` must be set to '0x6d6f6c65', in order \ - to bypass firewall restrictions", - ); - } - #[cfg(target_os = "macos")] - { - local_subcommand = local_subcommand.about( - "Registers a local SOCKS5 proxy. The server must run as root to bypass \ - firewall restrictions", - ); - } + /// Configure a remote SOCKS5 proxy + Remote { + /// The IP of the remote proxy server + remote_ip: IpAddr, + /// The port of the remote proxy server + remote_port: u16, - clap::App::new("custom") - .about("Configure a SOCKS5 proxy") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(local_subcommand) - .subcommand( - clap::App::new("remote") - .about("Registers a remote SOCKS5 proxy") - .arg( - clap::Arg::new("remote-ip") - .help("Specifies the IP of the remote proxy server") - .required(true) - .index(1), - ) - .arg( - clap::Arg::new("remote-port") - .help("Specifies the port the remote proxy server is listening on") - .required(true) - .index(2), - ) - .arg( - clap::Arg::new("username") - .help("Specifies the username for remote authentication") - .required(true) - .index(3), - ) - .arg( - clap::Arg::new("password") - .help("Specifies the password for remote authentication") - .required(true) - .index(4), - ), - ) - .subcommand( - clap::App::new("shadowsocks") - .about("Configure bundled Shadowsocks proxy") - .arg( - clap::Arg::new("remote-ip") - .help("Specifies the IP of the remote Shadowsocks server") - .required(true) - .index(1), - ) - .arg( - clap::Arg::new("remote-port") - .help("Specifies the port of the remote Shadowsocks server") - .default_value("443") - .index(2), - ) - .arg( - clap::Arg::new("password") - .help("Specifies the password on the remote Shadowsocks server") - .default_value("mullvad") - .index(3), - ) - .arg( - clap::Arg::new("cipher") - .help("Specifies the cipher to use") - .default_value("aes-256-gcm") - .possible_values(SHADOWSOCKS_CIPHERS) - .index(4), - ), - ) -} + /// Username for authentication + #[arg(requires = "password")] + username: Option<String>, + /// Password for authentication + #[arg(requires = "username")] + password: Option<String>, + }, + + /// Configure bundled Shadowsocks proxy + Shadowsocks { + /// The IP of the remote Shadowsocks server + remote_ip: IpAddr, + /// The port of the remote Shadowsocks server + #[arg(default_value = "443")] + remote_port: u16, -fn create_set_state_subcommand() -> clap::App<'static> { - clap::App::new("state").about("Set bridge state").arg( - clap::Arg::new("policy") - .help("Specifies whether a bridge should be used") - .required(true) - .index(1) - .possible_values(["auto", "on", "off"]), - ) + /// Password for authentication + #[arg(default_value = "mullvad")] + password: String, + + /// Cipher to use + #[arg(value_parser = SHADOWSOCKS_CIPHERS, default_value = "aes-256-gcm")] + cipher: String, + }, } impl Bridge { - async fn handle_set(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("location", location_matches)) => { - Self::handle_set_bridge_location(location_matches).await + pub async fn handle(self) -> Result<()> { + match self { + Bridge::Get => Self::get().await, + Bridge::List => Self::list().await, + Bridge::Set(subcmd) => Self::set(subcmd).await, + } + } + + async fn set(subcmd: SetCommands) -> Result<()> { + match subcmd { + SetCommands::State { policy } => { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_state(policy).await?; + println!("Updated bridge state"); + Ok(()) + } + SetCommands::Location(location) => { + Self::update_bridge_settings(Some(Constraint::from(location)), None, None).await } - Some(("provider", provider_matches)) => { - Self::handle_set_bridge_provider(provider_matches).await + SetCommands::Ownership { ownership } => { + Self::update_bridge_settings(None, None, Some(ownership)).await + } + SetCommands::Provider { providers } => { + let providers = if providers[0].eq_ignore_ascii_case("any") { + Constraint::Any + } else { + Constraint::Only(Providers::new(providers.into_iter()).unwrap()) + }; + Self::update_bridge_settings(None, Some(providers), None).await + } + SetCommands::Custom(subcmd) => Self::set_custom(subcmd).await, + } + } + + async fn set_custom(subcmd: SetCustomCommands) -> Result<()> { + match subcmd { + SetCustomCommands::Local { + local_port, + remote_ip, + remote_port, + } => { + let local_proxy = openvpn::LocalProxySettings { + port: local_port, + peer: SocketAddr::new(remote_ip, remote_port), + }; + let packed_proxy = openvpn::ProxySettings::Local(local_proxy); + if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { + panic!("{}", error); + } + + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy)) + .await?; } - Some(("ownership", ownership_matches)) => { - Self::handle_set_bridge_ownership(ownership_matches).await + SetCustomCommands::Remote { + remote_ip, + remote_port, + username, + password, + } => { + let auth = match (username, password) { + (Some(username), Some(password)) => { + Some(openvpn::ProxyAuth { username, password }) + } + _ => None, + }; + let proxy = openvpn::RemoteProxySettings { + address: SocketAddr::new(remote_ip, remote_port), + auth, + }; + let packed_proxy = openvpn::ProxySettings::Remote(proxy); + if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { + panic!("{}", error); + } + + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy)) + .await?; } - Some(("custom", custom_matches)) => { - Self::handle_bridge_set_custom_settings(custom_matches).await + SetCustomCommands::Shadowsocks { + remote_ip, + remote_port, + password, + cipher, + } => { + let proxy = openvpn::ShadowsocksProxySettings { + peer: SocketAddr::new(remote_ip, remote_port), + password, + cipher, + #[cfg(target_os = "linux")] + fwmark: None, + }; + let packed_proxy = openvpn::ProxySettings::Shadowsocks(proxy); + if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { + panic!("{}", error); + } + + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy)) + .await?; } - Some(("state", set_matches)) => Self::handle_set_bridge_state(set_matches).await, - _ => unreachable!("unhandled command"), } + + println!("Updated bridge settings"); + Ok(()) } - async fn handle_get() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - let bridge_settings = BridgeSettings::try_from(settings.bridge_settings.unwrap()).unwrap(); - println!( - "Bridge state: {}", - BridgeState::try_from(settings.bridge_state.unwrap()).unwrap() - ); - match bridge_settings { + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + println!("Bridge state: {}", settings.bridge_state); + match settings.bridge_settings { BridgeSettings::Custom(proxy) => match proxy { openvpn::ProxySettings::Local(local_proxy) => Self::print_local_proxy(&local_proxy), openvpn::ProxySettings::Remote(remote_proxy) => { @@ -235,175 +249,6 @@ impl Bridge { Ok(()) } - async fn handle_set_bridge_location(matches: &clap::ArgMatches) -> Result<()> { - Self::update_bridge_settings( - Some(location::get_constraint_from_args(matches)), - None, - None, - ) - .await - } - - async fn handle_set_bridge_provider(matches: &clap::ArgMatches) -> Result<()> { - let providers: Vec<String> = matches.values_of_t_or_exit("provider"); - let providers = if providers.get(0).map(String::as_str) == Some("any") { - vec![] - } else { - providers - }; - - Self::update_bridge_settings(None, Some(providers), None).await - } - - async fn handle_set_bridge_ownership(matches: &clap::ArgMatches) -> Result<()> { - let ownership = - super::relay::parse_ownership_constraint(matches.value_of("ownership").unwrap()); - Self::update_bridge_settings(None, None, Some(ownership)).await - } - - async fn update_bridge_settings( - location: Option<types::RelayLocation>, - providers: Option<Vec<String>>, - ownership: Option<types::Ownership>, - ) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - - let bridge_settings = BridgeSettings::try_from(settings.bridge_settings.unwrap()).unwrap(); - let constraints = match bridge_settings { - BridgeSettings::Normal(mut constraints) => { - if let Some(new_location) = location { - constraints.location = Constraint::<LocationConstraint>::from(new_location); - } - if let Some(new_providers) = providers { - constraints.providers = - types::relay_constraints::try_providers_constraint_from_proto( - &new_providers, - ) - .unwrap(); - } - if let Some(new_ownership) = ownership { - constraints.ownership = - types::relay_constraints::ownership_constraint_from_proto(new_ownership); - } - constraints - } - _ => { - let location = Constraint::<LocationConstraint>::from(location.unwrap_or_default()); - let providers = types::relay_constraints::try_providers_constraint_from_proto( - &providers.unwrap_or_default(), - ) - .unwrap(); - let ownership = ownership - .map(types::relay_constraints::ownership_constraint_from_proto) - .unwrap_or_default(); - - BridgeConstraints { - location, - providers, - ownership, - } - } - }; - - rpc.set_bridge_settings( - types::BridgeSettings::try_from(BridgeSettings::Normal(constraints)).unwrap(), - ) - .await?; - Ok(()) - } - - async fn handle_set_bridge_state(matches: &clap::ArgMatches) -> Result<()> { - let state = match matches.value_of("policy").unwrap() { - "auto" => BridgeState::Auto, - "on" => BridgeState::On, - "off" => BridgeState::Off, - _ => unreachable!(), - }; - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_state(types::BridgeState::from(state)) - .await?; - Ok(()) - } - - async fn handle_bridge_set_custom_settings(matches: &clap::ArgMatches) -> Result<()> { - if let Some(args) = matches.subcommand_matches("local") { - let local_port = args.value_of_t_or_exit("local-port"); - let remote_ip = args.value_of_t_or_exit("remote-ip"); - let remote_port = args.value_of_t_or_exit("remote-port"); - - let local_proxy = openvpn::LocalProxySettings { - port: local_port, - peer: SocketAddr::new(remote_ip, remote_port), - }; - let packed_proxy = openvpn::ProxySettings::Local(local_proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { - panic!("{}", error); - } - - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_settings(types::BridgeSettings::from(BridgeSettings::Custom( - packed_proxy, - ))) - .await?; - } else if let Some(args) = matches.subcommand_matches("remote") { - let remote_ip = args.value_of_t_or_exit("remote-ip"); - let remote_port = args.value_of_t_or_exit("remote-port"); - let username = args.value_of("username"); - let password = args.value_of("password"); - - let auth = match (username, password) { - (Some(username), Some(password)) => Some(openvpn::ProxyAuth { - username: username.to_string(), - password: password.to_string(), - }), - _ => None, - }; - let proxy = openvpn::RemoteProxySettings { - address: SocketAddr::new(remote_ip, remote_port), - auth, - }; - let packed_proxy = openvpn::ProxySettings::Remote(proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { - panic!("{}", error); - } - - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_settings(types::BridgeSettings::from(BridgeSettings::Custom( - packed_proxy, - ))) - .await?; - } else if let Some(args) = matches.subcommand_matches("shadowsocks") { - let remote_ip = args.value_of_t_or_exit("remote-ip"); - let remote_port = args.value_of_t_or_exit("remote-port"); - let password = args.value_of_t_or_exit("password"); - let cipher = args.value_of_t_or_exit("cipher"); - - let proxy = openvpn::ShadowsocksProxySettings { - peer: SocketAddr::new(remote_ip, remote_port), - password, - cipher, - #[cfg(target_os = "linux")] - fwmark: None, - }; - let packed_proxy = openvpn::ProxySettings::Shadowsocks(proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { - panic!("{}", error); - } - - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_settings(types::BridgeSettings::from(BridgeSettings::Custom( - packed_proxy, - ))) - .await?; - } else { - unreachable!("unhandled proxy type"); - } - - println!("proxy details have been updated"); - Ok(()) - } - fn print_local_proxy(proxy: &openvpn::LocalProxySettings) { println!("proxy: local"); println!(" local port: {}", proxy.port); @@ -429,13 +274,9 @@ impl Bridge { println!(" cipher: {}", proxy.cipher); } - async fn list_bridge_relays() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let relay_list = rpc - .get_relay_locations(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain relay locations", error))? - .into_inner(); + async fn list() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let relay_list = rpc.get_relay_locations().await?; let mut countries = Vec::new(); @@ -445,8 +286,7 @@ impl Bridge { .into_iter() .filter_map(|mut city| { city.relays.retain(|relay| { - relay.active - && relay.endpoint_type == (types::relay::RelayType::Bridge as i32) + relay.active && matches!(relay.endpoint_data, RelayEndpointData::Bridge) }); if !city.relays.is_empty() { Some(city) @@ -489,4 +329,39 @@ impl Bridge { } Ok(()) } + + async fn update_bridge_settings( + location: Option<Constraint<LocationConstraint>>, + providers: Option<Constraint<Providers>>, + ownership: Option<Constraint<Ownership>>, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let constraints = match rpc.get_settings().await?.bridge_settings { + BridgeSettings::Normal(mut constraints) => { + if let Some(new_location) = location { + constraints.location = new_location; + } + if let Some(new_providers) = providers { + constraints.providers = new_providers; + } + if let Some(new_ownership) = ownership { + constraints.ownership = new_ownership; + } + constraints + } + _ => BridgeConstraints { + location: location.unwrap_or(Constraint::Any), + providers: providers.unwrap_or(Constraint::Any), + ownership: ownership.unwrap_or(Constraint::Any), + }, + }; + + rpc.set_bridge_settings(BridgeSettings::Normal(constraints)) + .await?; + + println!("Updated bridge settings"); + + Ok(()) + } } diff --git a/mullvad-cli/src/cmds/connect.rs b/mullvad-cli/src/cmds/connect.rs deleted file mode 100644 index 0f470d3d2a..0000000000 --- a/mullvad-cli/src/cmds/connect.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{format, new_rpc_client, state, Command, Error, Result}; -use futures::StreamExt; -use mullvad_types::states::TunnelState; - -pub struct Connect; - -#[mullvad_management_interface::async_trait] -impl Command for Connect { - fn name(&self) -> &'static str { - "connect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Command the client to start establishing a VPN tunnel") - .arg( - clap::Arg::new("wait") - .long("wait") - .short('w') - .help("Wait until connected before exiting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - - let receiver_option = if matches.is_present("wait") { - Some(state::state_listen(rpc.clone())) - } else { - None - }; - - if rpc.connect_tunnel(()).await?.into_inner() { - if let Some(mut receiver) = receiver_option { - while let Some(state) = receiver.next().await { - let state = state?; - format::print_state(&state, false); - match state { - TunnelState::Connected { .. } => return Ok(()), - TunnelState::Error(_) => return Err(Error::CommandFailed("connect")), - _ => {} - } - } - return Err(Error::StatusListenerFailed); - } - } - - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/disconnect.rs b/mullvad-cli/src/cmds/disconnect.rs deleted file mode 100644 index 4ea5722fe9..0000000000 --- a/mullvad-cli/src/cmds/disconnect.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{format, new_rpc_client, state, Command, Error, Result}; -use futures::StreamExt; - -pub struct Disconnect; - -#[mullvad_management_interface::async_trait] -impl Command for Disconnect { - fn name(&self) -> &'static str { - "disconnect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Command the client to disconnect the VPN tunnel") - .arg( - clap::Arg::new("wait") - .long("wait") - .short('w') - .help("Wait until disconnected before exiting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - - let receiver_option = if matches.is_present("wait") { - Some(state::state_listen(rpc.clone())) - } else { - None - }; - - if rpc.disconnect_tunnel(()).await?.into_inner() { - if let Some(mut receiver) = receiver_option { - while let Some(state) = receiver.next().await { - let state = state?; - format::print_state(&state, false); - if state.is_disconnected() { - return Ok(()); - } - } - return Err(Error::StatusListenerFailed); - } - } - - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/dns.rs b/mullvad-cli/src/cmds/dns.rs index 28a48a5614..fd2b215936 100644 --- a/mullvad-cli/src/cmds/dns.rs +++ b/mullvad-cli/src/cmds/dns.rs @@ -1,161 +1,87 @@ -use crate::{new_rpc_client, Command, Result}; -use mullvad_management_interface::types; -use mullvad_types::settings::{DnsOptions, DnsState}; -use std::{convert::TryInto, net::IpAddr}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::settings::{CustomDnsOptions, DefaultDnsOptions, DnsOptions, DnsState}; +use std::net::IpAddr; -pub struct Dns; +#[derive(Subcommand, Debug)] +pub enum Dns { + /// Display the current DNS settings + Get, -#[mullvad_management_interface::async_trait] -impl Command for Dns { - fn name(&self) -> &'static str { - "dns" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Configure DNS servers to use when connected") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get").about("Display the current DNS settings")) - .subcommand( - clap::App::new("set") - .about("Set DNS servers to use") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("default") - .about("Use default DNS servers") - .arg( - clap::Arg::new("block ads") - .long("block-ads") - .takes_value(false) - .help("Block domain names used for ads"), - ) - .arg( - clap::Arg::new("block trackers") - .long("block-trackers") - .takes_value(false) - .help("Block domain names used for tracking"), - ) - .arg( - clap::Arg::new("block malware") - .long("block-malware") - .takes_value(false) - .help("Block domains known to be used by malware"), - ) - .arg( - clap::Arg::new("block adult content") - .long("block-adult-content") - .takes_value(false) - .help("Block domains known to be used for adult content"), - ) - .arg( - clap::Arg::new("block gambling") - .long("block-gambling") - .takes_value(false) - .help("Block domains known to be used for gambling"), - ), - ) - .subcommand( - clap::App::new("custom") - .about("Set a list of custom DNS servers") - .arg( - clap::Arg::new("servers") - .multiple_occurrences(true) - .help("One or more IP addresses pointing to DNS resolvers.") - .required(true), - ), - ), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("set", matches)) => match matches.subcommand() { - Some(("default", matches)) => { - self.set_default( - matches.is_present("block ads"), - matches.is_present("block trackers"), - matches.is_present("block malware"), - matches.is_present("block adult content"), - matches.is_present("block gambling"), - ) - .await - } - Some(("custom", matches)) => { - let servers = match matches.values_of_t::<IpAddr>("servers") { - Ok(servers) => Some(servers), - Err(e) => match e.kind { - clap::ErrorKind::ArgumentNotFound => None, - _ => e.exit(), - }, - }; - self.set_custom(servers).await - } - _ => unreachable!("No custom-dns server command given"), - }, - Some(("get", _)) => self.get().await, - _ => unreachable!("No custom-dns command given"), - } - } + /// Set DNS servers to use + Set { + #[clap(subcommand)] + cmd: DnsSet, + }, } -impl Dns { - async fn set_default( - &self, +#[derive(Subcommand, Debug, Clone)] +pub enum DnsSet { + /// Use a default DNS server, with or without content + /// blocking. + Default { + /// Block domains known to be used for ads + #[arg(long)] block_ads: bool, + + /// Block domains known to be used for tracking + #[arg(long)] block_trackers: bool, + + /// Block domains known to be used by malware + #[arg(long)] block_malware: bool, + + /// Block domains known to be used for adult content + #[arg(long)] block_adult_content: bool, + + /// Block domains known to be used for gambling + #[arg(long)] block_gambling: bool, - ) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - rpc.set_dns_options(types::DnsOptions { - state: types::dns_options::DnsState::Default as i32, - default_options: Some(types::DefaultDnsOptions { - block_ads, - block_trackers, - block_malware, - block_adult_content, - block_gambling, - }), - ..settings.tunnel_options.unwrap().dns_options.unwrap() - }) - .await?; - println!("Updated DNS settings"); - Ok(()) - } + }, - async fn set_custom(&self, servers: Option<Vec<IpAddr>>) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - rpc.set_dns_options(types::DnsOptions { - state: types::dns_options::DnsState::Custom as i32, - custom_options: Some(types::CustomDnsOptions { - addresses: servers - .unwrap_or_default() - .into_iter() - .map(|a| a.to_string()) - .collect(), - }), - ..settings.tunnel_options.unwrap().dns_options.unwrap() - }) - .await?; - println!("Updated DNS settings"); - Ok(()) + /// Set a list of custom DNS servers + Custom { + /// One or more IP addresses pointing to DNS resolvers + #[arg(required(true), num_args = 1..)] + servers: Vec<IpAddr>, + }, +} + +impl Dns { + pub async fn handle(self) -> Result<()> { + match self { + Dns::Get => Self::get().await, + Dns::Set { + cmd: + DnsSet::Default { + block_ads, + block_trackers, + block_malware, + block_adult_content, + block_gambling, + }, + } => { + Self::set_default( + block_ads, + block_trackers, + block_malware, + block_adult_content, + block_gambling, + ) + .await + } + Dns::Set { + cmd: DnsSet::Custom { servers }, + } => Self::set_custom(servers).await, + } } - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let options: DnsOptions = rpc - .get_settings(()) - .await? - .into_inner() - .tunnel_options - .unwrap() - .dns_options - .unwrap() - .try_into() - .unwrap(); + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let options = rpc.get_settings().await?.tunnel_options.dns_options; match options.state { DnsState::Default => { @@ -179,4 +105,42 @@ impl Dns { Ok(()) } + + async fn set_default( + block_ads: bool, + block_trackers: bool, + block_malware: bool, + block_adult_content: bool, + block_gambling: bool, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + rpc.set_dns_options(DnsOptions { + state: DnsState::Default, + default_options: DefaultDnsOptions { + block_ads, + block_trackers, + block_malware, + block_adult_content, + block_gambling, + }, + ..settings.tunnel_options.dns_options + }) + .await?; + println!("Updated DNS settings"); + Ok(()) + } + + async fn set_custom(servers: Vec<IpAddr>) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + rpc.set_dns_options(DnsOptions { + state: DnsState::Custom, + custom_options: CustomDnsOptions { addresses: servers }, + ..settings.tunnel_options.dns_options + }) + .await?; + println!("Updated DNS settings"); + Ok(()) + } } diff --git a/mullvad-cli/src/cmds/lan.rs b/mullvad-cli/src/cmds/lan.rs index d1c3635c43..7bf92063ca 100644 --- a/mullvad-cli/src/cmds/lan.rs +++ b/mullvad-cli/src/cmds/lan.rs @@ -1,56 +1,41 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct Lan; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for Lan { - fn name(&self) -> &'static str { - "lan" - } +#[derive(Subcommand, Debug)] +pub enum Lan { + /// Display the current local network sharing setting + Get, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control the allow local network sharing setting") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set").about("Change allow LAN setting").arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["allow", "block"]), - ), - ) - .subcommand( - clap::App::new("get").about("Display the current local network sharing setting"), - ) - } + /// Change allow LAN setting + Set { + #[arg(value_parser = BooleanOption::custom_parser("allow", "block"))] + policy: BooleanOption, + }, +} - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - let allow_lan = set_matches.value_of("policy").expect("missing policy"); - self.set(allow_lan == "allow").await - } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get().await - } else { - unreachable!("No lan command given"); +impl Lan { + pub async fn handle(self) -> Result<()> { + match self { + Lan::Get => Self::get().await, + Lan::Set { policy } => Self::set(policy).await, } } -} -impl Lan { - async fn set(&self, allow_lan: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_allow_lan(allow_lan).await?; + async fn set(policy: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_allow_lan(*policy).await?; println!("Changed local network sharing setting"); Ok(()) } - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let allow_lan = rpc.get_settings(()).await?.into_inner().allow_lan; - println!( - "Local network sharing setting: {}", - if allow_lan { "allow" } else { "block" } - ); + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let allow_lan = + BooleanOption::with_labels(rpc.get_settings().await?.allow_lan, "allow", "block"); + println!("Local network sharing setting: {allow_lan}"); Ok(()) } } diff --git a/mullvad-cli/src/cmds/lockdown.rs b/mullvad-cli/src/cmds/lockdown.rs new file mode 100644 index 0000000000..001f195fda --- /dev/null +++ b/mullvad-cli/src/cmds/lockdown.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; + +use super::BooleanOption; + +#[derive(Subcommand, Debug)] +pub enum LockdownMode { + /// Display the current lockdown mode setting + Get, + /// Change the lockdown mode setting + Set { policy: BooleanOption }, +} + +impl LockdownMode { + pub async fn handle(self) -> Result<()> { + match self { + LockdownMode::Get => Self::get().await, + LockdownMode::Set { policy } => Self::set(policy).await, + } + } + + async fn set(policy: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_block_when_disconnected(*policy).await?; + println!("Changed lockdown mode setting"); + Ok(()) + } + + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let state = BooleanOption::from(rpc.get_settings().await?.block_when_disconnected); + println!("Block traffic when the VPN is disconnected: {state}"); + Ok(()) + } +} diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs index 4c374d64ee..7adcf6c65b 100644 --- a/mullvad-cli/src/cmds/mod.rs +++ b/mullvad-cli/src/cmds/mod.rs @@ -1,86 +1,80 @@ -use crate::Command; -use std::collections::HashMap; +use clap::builder::{PossibleValuesParser, TypedValueParser, ValueParser}; +use std::ops::Deref; -mod account; -pub use self::account::Account; +pub mod account; +pub mod auto_connect; +pub mod beta_program; +pub mod bridge; +pub mod dns; +pub mod lan; +pub mod lockdown; +pub mod obfuscation; +pub mod relay; +pub mod relay_constraints; +pub mod reset; +pub mod split_tunnel; +pub mod status; +pub mod tunnel; +pub mod tunnel_state; +pub mod version; -mod auto_connect; -pub use self::auto_connect::AutoConnect; - -mod beta_program; -pub use self::beta_program::BetaProgram; - -mod block_when_disconnected; -pub use self::block_when_disconnected::BlockWhenDisconnected; - -mod bridge; -pub use self::bridge::Bridge; - -mod connect; -pub use self::connect::Connect; - -mod disconnect; -pub use self::disconnect::Disconnect; - -mod dns; -pub use self::dns::Dns; - -mod lan; -pub use self::lan::Lan; +/// A value parser that parses "on" or "off" into a boolean +#[derive(Debug, Clone, Copy)] +pub struct BooleanOption { + state: bool, + on_label: &'static str, + off_label: &'static str, +} -mod obfuscation; -pub use self::obfuscation::Obfuscation; +impl Deref for BooleanOption { + type Target = bool; -mod reconnect; -pub use self::reconnect::Reconnect; + fn deref(&self) -> &Self::Target { + &self.state + } +} -mod relay; -pub use self::relay::Relay; +impl clap::builder::ValueParserFactory for BooleanOption { + type Parser = ValueParser; -mod reset; -pub use self::reset::Reset; + /// A value parser that parses "on" or "off" into a `BooleanOption` + fn value_parser() -> Self::Parser { + Self::custom_parser("on", "off") + } +} -#[cfg(any(target_os = "linux", windows))] -mod split_tunnel; -#[cfg(any(target_os = "linux", windows))] -pub use self::split_tunnel::SplitTunnel; +impl BooleanOption { + /// A value parser that parses `on_label` and `off_label` into a `BooleanOption` + fn custom_parser(on_label: &'static str, off_label: &'static str) -> ValueParser { + assert!(on_label != off_label); -mod status; -pub use self::status::Status; + ValueParser::new( + PossibleValuesParser::new([on_label, off_label]) + .map(move |val| Self::with_labels(val == on_label, on_label, off_label)), + ) + } -mod tunnel; -pub use self::tunnel::Tunnel; + fn with_labels(state: bool, on_label: &'static str, off_label: &'static str) -> Self { + Self { + state, + on_label, + off_label, + } + } +} -mod version; -pub use self::version::Version; +impl From<bool> for BooleanOption { + fn from(state: bool) -> Self { + Self::with_labels(state, "on", "off") + } +} -/// Returns a map of all available subcommands with their name as key. -pub fn get_commands() -> HashMap<&'static str, Box<dyn Command>> { - let commands: Vec<Box<dyn Command>> = vec![ - Box::new(Account), - Box::new(AutoConnect), - Box::new(BetaProgram), - Box::new(BlockWhenDisconnected), - Box::new(Bridge), - Box::new(Connect), - Box::new(Disconnect), - Box::new(Dns), - Box::new(Reconnect), - Box::new(Lan), - Box::new(Obfuscation), - Box::new(Relay), - Box::new(Reset), - #[cfg(any(target_os = "linux", windows))] - Box::new(SplitTunnel), - Box::new(Status), - Box::new(Tunnel), - Box::new(Version), - ]; - let mut map = HashMap::new(); - for cmd in commands { - if map.insert(cmd.name(), cmd).is_some() { - panic!("Multiple commands with the same name"); +impl std::fmt::Display for BooleanOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.state { + self.on_label.fmt(f) + } else { + self.off_label.fmt(f) } } - map } diff --git a/mullvad-cli/src/cmds/obfuscation.rs b/mullvad-cli/src/cmds/obfuscation.rs index 5a8b908340..b2aaaa1f6e 100644 --- a/mullvad-cli/src/cmds/obfuscation.rs +++ b/mullvad-cli/src/cmds/obfuscation.rs @@ -1,137 +1,74 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::relay_constraints::{ + Constraint, ObfuscationSettings, SelectedObfuscation, Udp2TcpObfuscationSettings, +}; -use mullvad_management_interface::{types as grpc_types, ManagementServiceClient}; +#[derive(Subcommand, Debug)] +pub enum Obfuscation { + /// Get current obfuscation settings + Get, -use mullvad_types::relay_constraints::{ObfuscationSettings, SelectedObfuscation}; - -use std::convert::TryFrom; - -pub struct Obfuscation; - -#[mullvad_management_interface::async_trait] -impl Command for Obfuscation { - fn name(&self) -> &'static str { - "obfuscation" - } + /// Set obfuscation settings + #[clap(subcommand)] + Set(SetCommands), +} - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about( - "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", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_obfuscation_set_subcommand()) - .subcommand(create_obfuscation_get_subcommand()) - } +#[derive(Subcommand, Debug, Clone)] +pub enum SetCommands { + /// Specifies if obfuscation should be used with WireGuard connections. + /// And if so, what obfuscation protocol it should use. + Mode { mode: SelectedObfuscation }, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("set", set_matches)) => Self::handle_set(set_matches).await, - Some(("get", _get_matches)) => Self::handle_get().await, - _ => unreachable!("unhandled command"), - } - } + /// Specifies the config for the udp2tcp obfuscator. + Udp2tcp { + /// Port to use, or 'any' + #[arg(long, short = 'p')] + port: Constraint<u16>, + }, } impl Obfuscation { - async fn handle_set(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("mode", mode_matches)) => { - let obfuscator_type = mode_matches.value_of("mode").unwrap(); - let mut rpc = new_rpc_client().await?; - let mut settings = Self::get_obfuscation_settings(&mut rpc).await?; - settings.selected_obfuscation = match obfuscator_type { - "auto" => SelectedObfuscation::Auto, - "off" => SelectedObfuscation::Off, - "udp2tcp" => SelectedObfuscation::Udp2Tcp, - _ => unreachable!("Unhandled obfuscator mode"), - }; - Self::set_obfuscation_settings(&mut rpc, &settings).await?; + pub async fn handle(self) -> Result<()> { + match self { + Obfuscation::Get => { + let mut rpc = MullvadProxyClient::new().await?; + let obfuscation_settings = rpc.get_settings().await?.obfuscation_settings; + println!( + "Obfuscation mode: {}", + obfuscation_settings.selected_obfuscation + ); + println!("udp2tcp settings: {}", obfuscation_settings.udp2tcp); + Ok(()) } - Some(("udp2tcp", settings_matches)) => { - let port: String = settings_matches.value_of_t_or_exit("port"); - let mut rpc = new_rpc_client().await?; - let mut settings = Self::get_obfuscation_settings(&mut rpc).await?; - settings.udp2tcp.port = if port == "any" { - mullvad_types::relay_constraints::Constraint::Any - } else { - mullvad_types::relay_constraints::Constraint::Only( - port.parse::<u16>().expect("Invalid port number"), - ) - }; - Self::set_obfuscation_settings(&mut rpc, &settings).await?; - } - _ => unreachable!("unhandled command"), + Obfuscation::Set(subcmd) => Self::set(subcmd).await, } - Ok(()) } - async fn handle_get() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let obfuscation_settings = Self::get_obfuscation_settings(&mut rpc).await?; - println!( - "Obfuscation mode: {}", - obfuscation_settings.selected_obfuscation - ); - println!("udp2tcp settings: {}", obfuscation_settings.udp2tcp); - Ok(()) - } + async fn set(subcmd: SetCommands) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let current_settings = rpc.get_settings().await?.obfuscation_settings; - async fn get_obfuscation_settings( - rpc: &mut ManagementServiceClient, - ) -> Result<ObfuscationSettings> { - let settings = rpc.get_settings(()).await?.into_inner(); + match subcmd { + SetCommands::Mode { mode } => { + rpc.set_obfuscation_settings(ObfuscationSettings { + selected_obfuscation: mode, + ..current_settings + }) + .await?; + } + SetCommands::Udp2tcp { port } => { + rpc.set_obfuscation_settings(ObfuscationSettings { + udp2tcp: Udp2TcpObfuscationSettings { port }, + ..current_settings + }) + .await?; + } + } - let obfuscation_settings = ObfuscationSettings::try_from( - settings - .obfuscation_settings - .expect("No obfuscation settings"), - ) - .expect("failed to parse obfuscation settings"); - Ok(obfuscation_settings) - } + println!("Updated obfuscation settings"); - async fn set_obfuscation_settings( - rpc: &mut ManagementServiceClient, - settings: &ObfuscationSettings, - ) -> Result<()> { - let grpc_settings: grpc_types::ObfuscationSettings = settings.into(); - let _ = rpc.set_obfuscation_settings(grpc_settings).await?; Ok(()) } } - -fn create_obfuscation_set_subcommand() -> clap::App<'static> { - clap::App::new("set") - .about("Set obfuscation settings") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("mode").about("Set obfuscation mode").arg( - clap::Arg::new("mode") - .help( - "Specifies if obfuscation should be used with WireGuard connections. \ - And if so, what obfuscation protocol it should use.", - ) - .required(true) - .index(1) - .possible_values(["auto", "off", "udp2tcp"]), - ), - ) - .subcommand( - clap::App::new("udp2tcp") - .about("Specifies the config for the udp2tcp obfuscator") - .setting(clap::AppSettings::ArgRequiredElseHelp) - .arg( - clap::Arg::new("port") - .help("TCP port of remote endpoint. Either 'any' or a specific port") - .long("port") - .takes_value(true), - ), - ) -} - -fn create_obfuscation_get_subcommand() -> clap::App<'static> { - clap::App::new("get").about("Get current obfuscation settings") -} diff --git a/mullvad-cli/src/cmds/reconnect.rs b/mullvad-cli/src/cmds/reconnect.rs deleted file mode 100644 index 0a39d9f33d..0000000000 --- a/mullvad-cli/src/cmds/reconnect.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{format, new_rpc_client, state, Command, Error, Result}; -use futures::StreamExt; -use mullvad_types::states::TunnelState; - -pub struct Reconnect; - -#[mullvad_management_interface::async_trait] -impl Command for Reconnect { - fn name(&self) -> &'static str { - "reconnect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Command the client to reconnect") - .arg( - clap::Arg::new("wait") - .long("wait") - .short('w') - .help("Wait until reconnected before exiting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - - let receiver_option = if matches.is_present("wait") { - Some(state::state_listen(rpc.clone())) - } else { - None - }; - - if rpc.reconnect_tunnel(()).await?.into_inner() { - if let Some(mut receiver) = receiver_option { - while let Some(state) = receiver.next().await { - let state = state?; - format::print_state(&state, false); - match state { - TunnelState::Connected { .. } => return Ok(()), - TunnelState::Error { .. } => return Err(Error::CommandFailed("reconnect")), - _ => {} - } - } - return Err(Error::StatusListenerFailed); - } - } - - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 1497a7da41..37b86537e4 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -1,674 +1,181 @@ -use crate::{location, new_rpc_client, Command, Error, Result}; +use anyhow::{anyhow, Context, Result}; +use clap::Subcommand; use itertools::Itertools; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{ + location::Hostname, + relay_constraints::{ + Constraint, LocationConstraint, Match, OpenVpnConstraints, Ownership, Provider, Providers, + RelayConstraintsUpdate, RelaySettings, RelaySettingsUpdate, TransportPort, + WireguardConstraints, + }, + relay_list::{RelayEndpointData, RelayListCountry}, + ConnectionConfig, CustomTunnelEndpoint, +}; use std::{ - convert::TryFrom, - io::{self, BufRead}, + io::BufRead, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - str::FromStr, +}; +use talpid_types::net::{ + all_of_the_internet, openvpn, wireguard, Endpoint, IpVersion, TransportProtocol, TunnelType, }; -use mullvad_management_interface::{types, ManagementServiceClient}; -use mullvad_types::relay_constraints::{Constraint, RelaySettings}; -use talpid_types::net::all_of_the_internet; +use super::{relay_constraints::LocationArgs, BooleanOption}; -pub struct Relay; +#[derive(Subcommand, Debug)] +pub enum Relay { + /// Display the current relay constraints + Get, -#[mullvad_management_interface::async_trait] -impl Command for Relay { - fn name(&self) -> &'static str { - "relay" - } + /// Set relay constraints, such as location and port + #[clap(subcommand)] + Set(SetCommands), - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Manage relay and tunnel constraints") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about( - "Set relay server selection parameters. Such as location and port/protocol", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("custom") - .about("Set a custom VPN relay") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("wireguard") - .arg( - clap::Arg::new("host") - .help("Hostname or IP") - .required(true), - ) - .arg( - clap::Arg::new("port") - .help("Remote network port") - .required(true), - ) - .arg( - clap::Arg::new("peer-pubkey") - .help("Base64 encoded peer public key") - .required(true), - ) - .arg( - clap::Arg::new("v4-gateway") - .help("IPv4 gateway address") - .required(true), - ) - .arg( - clap::Arg::new("addr") - .help("Local address of wireguard tunnel") - .required(true) - .multiple_values(true), - ) - .arg( - clap::Arg::new("v6-gateway") - .help("IPv6 gateway address") - .long("v6-gateway") - .takes_value(true), - ) - ) - .subcommand(clap::App::new("openvpn") - .arg( - clap::Arg::new("host") - .help("Hostname or IP") - .required(true), - ) - .arg( - clap::Arg::new("port") - .help("Remote network port") - .required(true), - ) - .arg( - clap::Arg::new("username") - .help("Username to be used with the OpenVpn relay") - .required(true), - ) - .arg( - clap::Arg::new("password") - .help("Password to be used with the OpenVpn relay") - .required(true), - ) - .arg( - clap::Arg::new("protocol") - .help("Transport protocol") - .long("protocol") - .default_value("udp") - .possible_values(["udp", "tcp"]), - ) - ) - ) - .subcommand( - location::get_subcommand() - .about("Set country or city to select relays from. Use the 'list' \ - command to show available alternatives.") - ) - .subcommand( - clap::App::new("hostname") - .about("Set the exact relay to use via its hostname. Shortcut for \ - 'location <country> <city> <hostname>'.") - .arg( - clap::Arg::new("hostname") - .help("The hostname") - .required(true), - ), - ) - .subcommand( - clap::App::new("provider") - .about("Set hosting provider(s) to select relays from. The 'list' \ - command shows the available relays and their providers.") - .arg( - clap::Arg::new("provider") - .help("The hosting provider(s) to use, or 'any' for no preference.") - .multiple_values(true) - .required(true) - ) - ) - .subcommand( - clap::App::new("ownership") - .about("Filters relays based on ownership. The 'list' \ - command shows the available relays and whether they're rented.") - .arg( - clap::Arg::new("ownership") - .help("Ownership preference, or 'any' for no preference.") - .possible_values(["any", "owned", "rented"]) - .required(true) - ) - ) - .subcommand( - clap::App::new("tunnel") - .about("Set tunnel protocol-specific constraints.") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("openvpn") - .about("Set OpenVPN-specific constraints") - .setting(clap::AppSettings::ArgRequiredElseHelp) - .arg( - clap::Arg::new("port") - .help("Port to use. Either 'any' or a specific port") - .long("port") - .takes_value(true), - ) - .arg( - clap::Arg::new("transport protocol") - .help("Transport protocol") - .long("protocol") - .possible_values(["any", "udp", "tcp"]) - .takes_value(true), - ) - ) - .subcommand( - clap::App::new("wireguard") - .about("Set WireGuard-specific constraints") - .setting(clap::AppSettings::ArgRequiredElseHelp) - .arg( - clap::Arg::new("port") - .help("Port to use. Either 'any' or a specific port") - .long("port") - .takes_value(true), - ) - .arg( - clap::Arg::new("ip version") - .long("ipv") - .possible_values(["any", "4", "6"]) - .takes_value(true), - ) - .arg( - clap::Arg::new("entry location") - .help("Entry endpoint to use. This can be 'any', 'none', or \ - any location that is valid with 'set location', \ - such as 'se got'.") - .long("entry-location") - .min_values(1) - .max_values(3), - ) - ) - ) - .subcommand(clap::App::new("tunnel-protocol") - .about("Set tunnel protocol") - .arg( - clap::Arg::new("tunnel protocol") - .required(true) - .index(1) - .possible_values(["any", "wireguard", "openvpn", ]), - ) - ), - ) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("list").about("List available countries and cities"), - ) - .subcommand( - clap::App::new("update") - .about("Update the list of available countries and cities"), - ) - } + /// List available relays + List, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - self.set(set_matches).await - } else if matches.subcommand_matches("get").is_some() { - self.get().await - } else if matches.subcommand_matches("list").is_some() { - self.list().await - } else if matches.subcommand_matches("update").is_some() { - self.update().await - } else { - unreachable!("No relay command given"); - } - } + /// Update the relay list + Update, } -impl Relay { - async fn update_constraints(&self, update: types::RelaySettingsUpdate) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.update_relay_settings(update) - .await - .map_err(|error| Error::RpcFailedExt("Failed to update relay settings", error))?; - println!("Relay constraints updated"); - Ok(()) - } - - async fn set(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(custom_matches) = matches.subcommand_matches("custom") { - self.set_custom(custom_matches).await - } else if let Some(location_matches) = matches.subcommand_matches("location") { - self.set_location(location_matches).await - } else if let Some(relay_matches) = matches.subcommand_matches("hostname") { - self.set_hostname(relay_matches).await - } else if let Some(providers_matches) = matches.subcommand_matches("provider") { - self.set_providers(providers_matches).await - } else if let Some(ownership_matches) = matches.subcommand_matches("ownership") { - self.set_ownership(ownership_matches).await - } else if let Some(matches) = matches.subcommand_matches("tunnel") { - if let Some(tunnel_matches) = matches.subcommand_matches("openvpn") { - self.set_openvpn_constraints(tunnel_matches).await - } else if let Some(tunnel_matches) = matches.subcommand_matches("wireguard") { - self.set_wireguard_constraints(tunnel_matches).await - } else { - unreachable!("Invalid tunnel protocol"); - } - } else if let Some(tunnel_matches) = matches.subcommand_matches("tunnel-protocol") { - self.set_tunnel_protocol(tunnel_matches).await - } else { - unreachable!("No set relay command given"); - } - } - - async fn set_custom(&self, matches: &clap::ArgMatches) -> Result<()> { - let custom_endpoint = match matches.subcommand() { - Some(("openvpn", openvpn_matches)) => Self::read_custom_openvpn_relay(openvpn_matches), - Some(("wireguard", wg_matches)) => Self::read_custom_wireguard_relay(wg_matches), - _ => unreachable!("No set relay command given"), - }; - - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Custom(custom_endpoint)), - }) - .await - } - - fn read_custom_openvpn_relay(matches: &clap::ArgMatches) -> types::CustomRelaySettings { - let host = matches.value_of_t_or_exit("host"); - let port = matches.value_of_t_or_exit("port"); - let username = matches.value_of_t_or_exit("username"); - let password = matches.value_of_t_or_exit("password"); - let protocol: String = matches.value_of_t_or_exit("protocol"); - - let protocol = Self::validate_transport_protocol(&protocol); - - types::CustomRelaySettings { - host, - config: Some(types::ConnectionConfig { - config: Some(types::connection_config::Config::Openvpn( - types::connection_config::OpenvpnConfig { - address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port) - .to_string(), - protocol: protocol as i32, - username, - password, - }, - )), - }), - } - } - - fn read_custom_wireguard_relay(matches: &clap::ArgMatches) -> types::CustomRelaySettings { - use types::connection_config::wireguard_config; - - let host = matches.value_of_t_or_exit("host"); - let port = matches.value_of_t_or_exit("port"); - let addresses: Vec<IpAddr> = matches.values_of_t_or_exit("addr"); - let peer_key_str: String = matches.value_of_t_or_exit("peer-pubkey"); - let ipv4_gateway: Ipv4Addr = matches.value_of_t_or_exit("v4-gateway"); - let ipv6_gateway = match matches.value_of_t::<Ipv6Addr>("v6-gateway") { - Ok(gateway) => Some(gateway), - Err(e) => match e.kind { - clap::ErrorKind::ArgumentNotFound => None, - _ => e.exit(), - }, - }; - let mut private_key_str = String::new(); - println!("Reading private key from standard input"); - let _ = io::stdin().lock().read_line(&mut private_key_str); - if private_key_str.trim().is_empty() { - eprintln!("Expected to read private key from standard input"); - } - let private_key = Self::validate_wireguard_key(&private_key_str); - let peer_public_key = Self::validate_wireguard_key(&peer_key_str); - - types::CustomRelaySettings { - host, - config: Some(types::ConnectionConfig { - config: Some(types::connection_config::Config::Wireguard( - types::connection_config::WireguardConfig { - tunnel: Some(wireguard_config::TunnelConfig { - private_key: private_key.to_vec(), - addresses: addresses - .iter() - .map(|address| address.to_string()) - .collect(), - }), - peer: Some(wireguard_config::PeerConfig { - public_key: peer_public_key.to_vec(), - allowed_ips: all_of_the_internet() - .iter() - .map(|address| address.to_string()) - .collect(), - endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port) - .to_string(), - }), - ipv4_gateway: ipv4_gateway.to_string(), - ipv6_gateway: ipv6_gateway - .as_ref() - .map(|addr| addr.to_string()) - .unwrap_or_default(), - }, - )), - }), - } - } - - fn validate_wireguard_key(key_str: &str) -> [u8; 32] { - let key_bytes = base64::decode(key_str.trim()).unwrap_or_else(|e| { - eprintln!("Failed to decode wireguard key: {e}"); - std::process::exit(1); - }); - - let mut key = [0u8; 32]; - if key_bytes.len() != 32 { - eprintln!( - "Expected key length to be 32 bytes, got {}", - key_bytes.len() - ); - std::process::exit(1); - } - - key.copy_from_slice(&key_bytes); - key - } - - fn validate_transport_protocol(protocol: &str) -> types::TransportProtocol { - match protocol { - "udp" => types::TransportProtocol::Udp, - "tcp" => types::TransportProtocol::Tcp, - _ => clap::Error::raw( - clap::ErrorKind::ValueValidation, - "invalid transport protocol", - ) - .exit(), - } - } - - async fn set_hostname(&self, matches: &clap::ArgMatches) -> Result<()> { - let hostname = matches.value_of("hostname").unwrap(); - let countries = Self::get_filtered_relays().await?; - - let find_relay = || { - for country in countries { - for city in country.cities { - for relay in city.relays { - if relay.hostname.to_lowercase() == hostname.to_lowercase() { - return Some(types::RelayLocation { - country: country.code, - city: city.code, - hostname: relay.hostname, - }); - } - } - } - } - None - }; - - if let Some(location) = find_relay() { - println!( - "Setting location constraint to {} in {}, {}", - location.hostname, location.city, location.country - ); +#[derive(Subcommand, Debug, Clone)] +pub enum SetCommands { + /// Set country or city to select relays from. Use the 'list' + /// command to show available alternatives. + Location(LocationArgs), - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - location: Some(location), - ..Default::default() - }, - )), - }) - .await - } else { - clap::Error::raw(clap::ErrorKind::ValueValidation, "No matching server found").exit() - } - } - - async fn set_location(&self, matches: &clap::ArgMatches) -> Result<()> { - let location_constraint = location::get_constraint_from_args(matches); - let mut found = false; - - if !location_constraint.country.is_empty() { - // TODO: `mullvad_types::relay_constraints::LocationConstraint::matches(&relay)` - // could be used to guarantee consistency with the daemon. - let countries = Self::get_filtered_relays().await?; - for country in &countries { - if country.code != location_constraint.country { - continue; - } - - if location_constraint.city.is_empty() { - found = true; - break; - } + /// Set the location using only a hostname + Hostname { + /// A hostname, such as "se3-wireguard". + hostname: Hostname, + }, - for city in &country.cities { - if city.code != location_constraint.city { - continue; - } + /// Set hosting provider(s) to select relays from. The 'list' + /// command shows the available relays and their providers. + Provider { + #[arg(required(true), num_args = 1..)] + providers: Vec<Provider>, + }, - if location_constraint.hostname.is_empty() { - found = true; - break; - } + /// Filter relays based on ownership. The 'list' command + /// shows the available relays and whether they're rented. + Ownership { + /// Servers to select from: 'any', 'owned', or 'rented'. + ownership: Constraint<Ownership>, + }, - for relay in &city.relays { - if relay.hostname != location_constraint.hostname { - continue; - } - found = true; - break; - } + /// Set tunnel protocol specific constraints + #[clap(subcommand)] + Tunnel(SetTunnelCommands), - break; - } - break; - } + /// Set tunnel protocol to use: 'any', 'wireguard', or 'openvpn'. + TunnelProtocol { protocol: Constraint<TunnelType> }, - if !found { - eprintln!("Warning: No matching relay was found."); - } - } - - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - location: Some(location_constraint), - ..Default::default() - }, - )), - }) - .await - } - - async fn set_providers(&self, matches: &clap::ArgMatches) -> Result<()> { - let providers: Vec<String> = matches.values_of_t_or_exit("provider"); - let providers = if providers.get(0).map(String::as_str) == Some("any") { - vec![] - } else { - providers - }; - - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - providers: Some(types::ProviderUpdate { providers }), - ..Default::default() - }, - )), - }) - .await - } - - async fn set_ownership(&self, matches: &clap::ArgMatches) -> Result<()> { - let ownership = parse_ownership_constraint(matches.value_of("ownership").unwrap()); - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - ownership: Some(types::OwnershipUpdate { - ownership: ownership as i32, - }), - ..Default::default() - }, - )), - }) - .await - } + /// Set a custom VPN relay to use + #[clap(subcommand)] + Custom(SetCustomCommands), +} - async fn set_openvpn_constraints(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut openvpn_constraints = { - let mut rpc = new_rpc_client().await?; - self.get_openvpn_constraints(&mut rpc).await? - }; - openvpn_constraints.port = parse_transport_port(matches, &mut openvpn_constraints.port)?; +#[derive(Subcommand, Debug, Clone)] +pub enum SetTunnelCommands { + /// Set OpenVPN-specific constraints + #[clap(arg_required_else_help = true)] + Openvpn { + /// Port to use, or 'any' + #[arg(long, short = 'p', requires = "transport_protocol")] + port: Option<Constraint<u16>>, - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - openvpn_constraints: Some(openvpn_constraints), - ..Default::default() - }, - )), - }) - .await - } + /// Transport protocol to use, or 'any' + #[arg(long, short = 't')] + transport_protocol: Option<Constraint<TransportProtocol>>, + }, - async fn get_openvpn_constraints( - &self, - rpc: &mut ManagementServiceClient, - ) -> Result<types::OpenvpnConstraints> { - match rpc - .get_settings(()) - .await? - .into_inner() - .relay_settings - .unwrap() - .endpoint - .unwrap() - { - types::relay_settings::Endpoint::Normal(settings) => { - Ok(settings.openvpn_constraints.unwrap()) - } - types::relay_settings::Endpoint::Custom(_settings) => { - println!("Clearing custom tunnel constraints"); - Ok(types::OpenvpnConstraints::default()) - } - } - } + /// Set WireGuard-specific constraints + #[clap(arg_required_else_help = true)] + Wireguard { + /// Port to use, or 'any' + #[arg(long, short = 'p')] + port: Option<Constraint<u16>>, - async fn set_wireguard_constraints(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let relay_list = rpc - .get_relay_locations(()) - .await? - .into_inner() - .wireguard - .unwrap(); - let mut wireguard_constraints = self.get_wireguard_constraints(&mut rpc).await?; + /// IP protocol to use, or 'any' + #[arg(long, short = 'i')] + ip_version: Option<Constraint<IpVersion>>, - if let Some(port) = matches.value_of("port") { - wireguard_constraints.port = match parse_port_constraint(port)? { - Constraint::Any => 0, - Constraint::Only(specific_port) => { - let specific_port = u32::from(specific_port); + /// Whether to enable multihop. The location constraints are specified with + /// 'entry-location'. + #[arg(long, short = 'm')] + use_multihop: Option<BooleanOption>, - let is_valid_port = relay_list - .port_ranges - .iter() - .any(|range| range.first <= specific_port && specific_port <= range.last); - if !is_valid_port { - return Err(Error::CommandFailed("The specified port is invalid")); - } + #[clap(subcommand)] + entry_location: Option<EntryLocation>, + }, +} - specific_port - } - } - } +#[derive(Subcommand, Debug, Clone)] +pub enum EntryLocation { + /// Entry endpoint to use. This can be 'any' or any location that is valid with 'set location', + /// such as 'se got'. + EntryLocation(LocationArgs), +} - if let Some(ipv) = matches.value_of("ip version") { - wireguard_constraints.ip_version = - parse_ip_version_constraint(ipv).option().map(|protocol| { - types::IpVersionConstraint { - protocol: protocol as i32, - } - }); - } - if let Some(entry) = matches.values_of("entry location") { - wireguard_constraints.entry_location = parse_entry_location_constraint(entry); - let use_multihop = wireguard_constraints.entry_location.is_some(); - wireguard_constraints.use_multihop = use_multihop; - } +#[derive(Subcommand, Debug, Clone)] +pub enum SetCustomCommands { + /// Use a custom OpenVPN relay + #[clap(arg_required_else_help = true)] + Openvpn { + /// Hostname or IP + host: String, + /// Remote port + port: u16, + /// Username for authentication + username: String, + /// Password for authentication + password: String, + /// Transport protocol to use + #[arg(default_value_t = TransportProtocol::Udp)] + transport_protocol: TransportProtocol, + }, - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - wireguard_constraints: Some(wireguard_constraints), - ..Default::default() - }, - )), - }) - .await - } + /// Use a custom WireGuard relay + #[clap(arg_required_else_help = true)] + Wireguard { + /// Hostname or IP + host: String, + /// Remote port + port: u16, + /// Base64 encoded public key of remote peer + #[arg(value_parser = wireguard::PublicKey::from_base64)] + peer_pubkey: wireguard::PublicKey, + /// IP addresses of local tunnel interface + #[arg(required = true, num_args = 1..)] + tunnel_ip: Vec<IpAddr>, + /// IPv4 gateway address + #[arg(long)] + v4_gateway: Ipv4Addr, + /// IPv6 gateway address + #[arg(long)] + v6_gateway: Option<Ipv6Addr>, + }, +} - async fn get_wireguard_constraints( - &self, - rpc: &mut ManagementServiceClient, - ) -> Result<types::WireguardConstraints> { - match rpc - .get_settings(()) - .await? - .into_inner() - .relay_settings - .unwrap() - .endpoint - .unwrap() - { - types::relay_settings::Endpoint::Normal(settings) => { - Ok(settings.wireguard_constraints.unwrap()) - } - types::relay_settings::Endpoint::Custom(_settings) => { - println!("Clearing custom tunnel constraints"); - Ok(types::WireguardConstraints::default()) - } +impl Relay { + pub async fn handle(self) -> Result<()> { + match self { + Relay::Get => Self::get().await, + Relay::List => Self::list().await, + Relay::Update => Self::update().await, + Relay::Set(subcmd) => Self::set(subcmd).await, } } - async fn set_tunnel_protocol(&self, matches: &clap::ArgMatches) -> Result<()> { - let tunnel_type = match matches.value_of("tunnel protocol").unwrap() { - "wireguard" => Some(types::TunnelType::Wireguard), - "openvpn" => Some(types::TunnelType::Openvpn), - "any" => None, - _ => unreachable!(), - }; - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - tunnel_type: Some(types::TunnelTypeUpdate { - tunnel_type: tunnel_type.map(|tunnel_type| types::TunnelTypeConstraint { - tunnel_type: tunnel_type as i32, - }), - }), - ..Default::default() - }, - )), - }) - .await - } - - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let relay_settings = rpc - .get_settings(()) - .await? - .into_inner() - .relay_settings - .unwrap(); - - println!( - "Current constraints: {}", - RelaySettings::try_from(relay_settings).unwrap() - ); - + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let relay_settings = rpc.get_settings().await?.relay_settings; + println!("Current constraints: {relay_settings}"); Ok(()) } - async fn list(&self) -> Result<()> { + async fn list() -> Result<()> { let mut countries = Self::get_filtered_relays().await?; countries.sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); for mut country in countries { @@ -684,9 +191,9 @@ impl Relay { city.name, city.code, city.latitude, city.longitude ); for relay in &city.relays { - let support_msg = match relay.endpoint_type { - i if i == i32::from(types::relay::RelayType::Openvpn) => "OpenVPN", - i if i == i32::from(types::relay::RelayType::Wireguard) => "WireGuard", + let support_msg = match relay.endpoint_data { + RelayEndpointData::Openvpn => "OpenVPN", + RelayEndpointData::Wireguard(_) => "WireGuard", _ => unreachable!("Bug in relay filtering earlier on"), }; let ownership = if relay.owned { @@ -694,9 +201,9 @@ impl Relay { } else { "rented" }; - let mut addresses = vec![&relay.ipv4_addr_in]; - if !relay.ipv6_addr_in.is_empty() { - addresses.push(&relay.ipv6_addr_in); + let mut addresses: Vec<IpAddr> = vec![relay.ipv4_addr_in.into()]; + if let Some(ipv6_addr) = relay.ipv6_addr_in { + addresses.push(ipv6_addr.into()); } println!( "\t\t{} ({}) - {}, hosted by {} ({ownership})", @@ -712,21 +219,20 @@ impl Relay { Ok(()) } - async fn update(&self) -> Result<()> { - new_rpc_client().await?.update_relay_locations(()).await?; + async fn update() -> Result<()> { + MullvadProxyClient::new() + .await? + .update_relay_locations() + .await?; println!("Updating relay list in the background..."); Ok(()) } - async fn get_filtered_relays() -> Result<Vec<types::RelayListCountry>> { - let mut rpc = new_rpc_client().await?; - let relay_list = rpc - .get_relay_locations(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain relay locations", error))? - .into_inner(); + async fn get_filtered_relays() -> Result<Vec<RelayListCountry>> { + let mut rpc = MullvadProxyClient::new().await?; + let relay_list = rpc.get_relay_locations().await?; - let mut countries = Vec::new(); + let mut countries = vec![]; for mut country in relay_list.countries { country.cities = country @@ -734,8 +240,7 @@ impl Relay { .into_iter() .filter_map(|mut city| { city.relays.retain(|relay| { - relay.active - && relay.endpoint_type != (types::relay::RelayType::Bridge as i32) + relay.active && relay.endpoint_data != RelayEndpointData::Bridge }); if !city.relays.is_empty() { Some(city) @@ -751,108 +256,334 @@ impl Relay { Ok(countries) } -} -fn parse_port_constraint(raw_port: &str) -> Result<Constraint<u16>> { - match raw_port.to_lowercase().as_str() { - "any" => Ok(Constraint::Any), - port => Ok(Constraint::Only(u16::from_str(port).map_err(|_| { - Error::InvalidCommand("Invalid port. Must be \"any\" or 0-65535.") - })?)), + async fn update_constraints(update: RelaySettingsUpdate) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.update_relay_settings(update).await?; + println!("Relay constraints updated"); + Ok(()) } -} -fn parse_protocol(raw_protocol: &str) -> Constraint<types::TransportProtocol> { - match raw_protocol { - "any" => Constraint::Any, - "udp" => Constraint::Only(types::TransportProtocol::Udp), - "tcp" => Constraint::Only(types::TransportProtocol::Tcp), - _ => unreachable!(), + async fn set(subcmd: SetCommands) -> Result<()> { + match subcmd { + SetCommands::Custom(subcmd) => Self::set_custom(subcmd).await, + SetCommands::Location(location) => Self::set_location(location).await, + SetCommands::Hostname { hostname } => Self::set_hostname(hostname).await, + SetCommands::Provider { providers } => Self::set_providers(providers).await, + SetCommands::Ownership { ownership } => Self::set_ownership(ownership).await, + SetCommands::Tunnel(subcmd) => Self::set_tunnel(subcmd).await, + SetCommands::TunnelProtocol { protocol } => Self::set_tunnel_protocol(protocol).await, + } } -} -fn parse_ip_version_constraint(raw_protocol: &str) -> Constraint<types::IpVersion> { - match raw_protocol { - "any" => Constraint::Any, - "4" => Constraint::Only(types::IpVersion::V4), - "6" => Constraint::Only(types::IpVersion::V6), - _ => unreachable!(), + async fn set_tunnel(subcmd: SetTunnelCommands) -> Result<()> { + match subcmd { + SetTunnelCommands::Openvpn { + port, + transport_protocol, + } => Self::set_openvpn_constraints(port, transport_protocol).await, + SetTunnelCommands::Wireguard { + port, + ip_version, + use_multihop, + entry_location, + } => { + Self::set_wireguard_constraints(port, ip_version, use_multihop, entry_location) + .await + } + } + } + + async fn set_custom(subcmd: SetCustomCommands) -> Result<()> { + let custom_endpoint = match subcmd { + SetCustomCommands::Openvpn { + host, + port, + username, + password, + transport_protocol, + } => { + Self::read_custom_openvpn_relay(host, port, username, password, transport_protocol) + } + SetCustomCommands::Wireguard { + host, + port, + peer_pubkey, + tunnel_ip, + v4_gateway, + v6_gateway, + } => { + Self::read_custom_wireguard_relay( + host, + port, + peer_pubkey, + tunnel_ip, + v4_gateway, + v6_gateway, + ) + .await? + } + }; + Self::update_constraints(RelaySettingsUpdate::CustomTunnelEndpoint(custom_endpoint)).await + } + + fn read_custom_openvpn_relay( + host: String, + port: u16, + username: String, + password: String, + protocol: TransportProtocol, + ) -> CustomTunnelEndpoint { + CustomTunnelEndpoint { + host, + config: ConnectionConfig::OpenVpn(openvpn::ConnectionConfig { + endpoint: Endpoint::from_socket_address( + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), + protocol, + ), + username, + password, + }), + } } -} -fn parse_entry_location_constraint<'a, T: Iterator<Item = &'a str>>( - mut location: T, -) -> Option<types::RelayLocation> { - let country = location.next().unwrap(); + async fn read_custom_wireguard_relay( + host: String, + port: u16, + peer_pubkey: wireguard::PublicKey, + tunnel_ip: Vec<IpAddr>, + ipv4_gateway: Ipv4Addr, + ipv6_gateway: Option<Ipv6Addr>, + ) -> Result<CustomTunnelEndpoint> { + println!("Reading private key from standard input"); + + let private_key_str = tokio::task::spawn_blocking(|| { + let mut private_key_str = String::new(); + let _ = std::io::stdin().lock().read_line(&mut private_key_str); + if private_key_str.trim().is_empty() { + eprintln!("Expected to read private key from standard input"); + } + private_key_str + }) + .await + .unwrap(); + + let private_key = + wireguard::PrivateKey::from_base64(&private_key_str).context("Invalid private key")?; - if country == "none" { - return None; + Ok(CustomTunnelEndpoint { + host, + config: ConnectionConfig::Wireguard(wireguard::ConnectionConfig { + tunnel: wireguard::TunnelConfig { + private_key, + addresses: tunnel_ip, + }, + peer: wireguard::PeerConfig { + public_key: peer_pubkey, + allowed_ips: all_of_the_internet(), + endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), + psk: None, + }, + exit_peer: None, + ipv4_gateway, + ipv6_gateway, + // NOTE: Ignored in gRPC + #[cfg(target_os = "linux")] + fwmark: None, + }), + }) } - Some(location::get_constraint( - country, - location.next(), - location.next(), - )) -} + async fn set_hostname(hostname: String) -> Result<()> { + let countries = Self::get_filtered_relays().await?; -fn parse_transport_port( - matches: &clap::ArgMatches, - current_constraint: &mut Option<types::TransportPort>, -) -> Result<Option<types::TransportPort>> { - let protocol = match matches.value_of("transport protocol") { - Some(protocol) => parse_protocol(protocol), - None => { - if let Some(ref transport_port) = current_constraint { - Constraint::Only( - types::TransportProtocol::from_i32(transport_port.protocol).unwrap(), - ) - } else { - Constraint::Any + let find_relay = || { + for country in countries { + for city in country.cities { + for relay in city.relays { + if relay.hostname.to_lowercase() == hostname.to_lowercase() { + return Some(LocationConstraint::Hostname( + country.code, + city.code, + relay.hostname, + )); + } + } + } + } + None + }; + + let location = find_relay().ok_or(anyhow!("Hostname not found"))?; + + println!("Setting location constraint to {location}"); + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + location: Some(Constraint::Only(location)), + ..Default::default() + })) + .await + } + + async fn set_location(location_constraint: LocationArgs) -> Result<()> { + let location_constraint = Constraint::from(location_constraint); + match &location_constraint { + Constraint::Any => (), + Constraint::Only(constraint) => { + let countries = Self::get_filtered_relays().await?; + + let found = countries + .into_iter() + .flat_map(|country| country.cities) + .flat_map(|city| city.relays) + .any(|relay| constraint.matches(&relay)); + + if !found { + eprintln!("Warning: No matching relay was found."); + } } } - }; - let mut port = match matches.value_of("port") { - Some(port) => parse_port_constraint(port)?, - None => { - if let Some(ref transport_port) = current_constraint { - if transport_port.port != 0 { - Constraint::Only(transport_port.port as u16) - } else { - Constraint::Any + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + location: Some(location_constraint), + ..Default::default() + })) + .await + } + + async fn set_providers(providers: Vec<String>) -> Result<()> { + let providers = if providers[0].eq_ignore_ascii_case("any") { + Constraint::Any + } else { + Constraint::Only(Providers::new(providers.into_iter()).unwrap()) + }; + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + providers: Some(providers), + ..Default::default() + })) + .await + } + + async fn set_ownership(ownership: Constraint<Ownership>) -> Result<()> { + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + ownership: Some(ownership), + ..Default::default() + })) + .await + } + + async fn set_openvpn_constraints( + port: Option<Constraint<u16>>, + protocol: Option<Constraint<TransportProtocol>>, + ) -> Result<()> { + let mut openvpn_constraints = { + let mut rpc = MullvadProxyClient::new().await?; + Self::get_openvpn_constraints(&mut rpc).await? + }; + openvpn_constraints.port = + parse_transport_port(port, protocol, &mut openvpn_constraints.port); + + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + openvpn_constraints: Some(openvpn_constraints), + ..Default::default() + })) + .await + } + + async fn get_openvpn_constraints(rpc: &mut MullvadProxyClient) -> Result<OpenVpnConstraints> { + match rpc.get_settings().await?.relay_settings { + RelaySettings::Normal(settings) => Ok(settings.openvpn_constraints), + RelaySettings::CustomTunnelEndpoint(_settings) => { + println!("Clearing custom tunnel constraints"); + Ok(OpenVpnConstraints::default()) + } + } + } + + async fn set_wireguard_constraints( + port: Option<Constraint<u16>>, + ip_version: Option<Constraint<IpVersion>>, + use_multihop: Option<BooleanOption>, + entry_location: Option<EntryLocation>, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let wireguard = rpc.get_relay_locations().await?.wireguard; + let mut wireguard_constraints = Self::get_wireguard_constraints(&mut rpc).await?; + + if let Some(port) = port { + wireguard_constraints.port = match port { + Constraint::Any => Constraint::Any, + Constraint::Only(specific_port) => { + let is_valid_port = wireguard + .port_ranges + .into_iter() + .any(|(first, last)| first <= specific_port && specific_port <= last); + if !is_valid_port { + return Err(anyhow!("The specified port is invalid")); + } + Constraint::Only(specific_port) } - } else { - Constraint::Any } } - }; - if port.is_only() && protocol.is_any() && !matches.is_present("port") { - // Reset the port if the transport protocol is set to any. - println!("The port constraint was set to 'any'"); - port = Constraint::Any; + + if let Some(ipv) = ip_version { + wireguard_constraints.ip_version = ipv; + } + if let Some(use_multihop) = use_multihop { + wireguard_constraints.use_multihop = *use_multihop; + } + if let Some(EntryLocation::EntryLocation(entry)) = entry_location { + wireguard_constraints.entry_location = Constraint::from(entry); + } + + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + wireguard_constraints: Some(wireguard_constraints), + ..Default::default() + })) + .await } - match (port, protocol) { - (Constraint::Any, Constraint::Any) => Ok(None), - (Constraint::Any, Constraint::Only(protocol)) => Ok(Some(types::TransportPort { - protocol: protocol as i32, - // If no port was specified, set it to "any" - ..types::TransportPort::default() - })), - (Constraint::Only(port), Constraint::Only(protocol)) => Ok(Some(types::TransportPort { - protocol: protocol as i32, - port: u32::from(port), - })), - (Constraint::Only(_), Constraint::Any) => Err(Error::InvalidCommand( - "a transport protocol must be given to select a specific port", - )), + + async fn get_wireguard_constraints( + rpc: &mut MullvadProxyClient, + ) -> Result<WireguardConstraints> { + match rpc.get_settings().await?.relay_settings { + RelaySettings::Normal(settings) => Ok(settings.wireguard_constraints), + RelaySettings::CustomTunnelEndpoint(_settings) => { + println!("Clearing custom tunnel constraints"); + Ok(WireguardConstraints::default()) + } + } + } + + async fn set_tunnel_protocol(protocol: Constraint<TunnelType>) -> Result<()> { + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + tunnel_protocol: Some(protocol), + ..Default::default() + })) + .await } } -pub fn parse_ownership_constraint(constraint: &str) -> types::Ownership { - match constraint { - "any" => types::Ownership::Any, - "owned" => types::Ownership::MullvadOwned, - "rented" => types::Ownership::Rented, - _ => unreachable!(), +fn parse_transport_port( + port: Option<Constraint<u16>>, + protocol: Option<Constraint<TransportProtocol>>, + current_constraint: &mut Constraint<TransportPort>, +) -> Constraint<TransportPort> { + let port = match port { + Some(port) => port, + None => current_constraint + .map(|p| p.port) + .unwrap_or(Constraint::Any), + }; + let protocol = match protocol { + Some(protocol) => protocol, + None => current_constraint.map(|p| p.protocol), + }; + match (port, protocol) { + (port, Constraint::Any) => { + if port.is_only() { + println!("The port constraint was set to 'any'"); + } + Constraint::Any + } + (port, Constraint::Only(protocol)) => Constraint::Only(TransportPort { protocol, port }), } } diff --git a/mullvad-cli/src/cmds/relay_constraints.rs b/mullvad-cli/src/cmds/relay_constraints.rs new file mode 100644 index 0000000000..1fc5073a4d --- /dev/null +++ b/mullvad-cli/src/cmds/relay_constraints.rs @@ -0,0 +1,34 @@ +use clap::Args; +use mullvad_types::{ + location::{CityCode, CountryCode, Hostname}, + relay_constraints::{Constraint, LocationConstraint}, +}; + +#[derive(Args, Debug, Clone)] +pub struct LocationArgs { + /// A two-letter country code, or 'any'. + pub country: CountryCode, + /// A three-letter city code. + pub city: Option<CityCode>, + /// A host name, such as "se-got-wg-101". + pub hostname: Option<Hostname>, +} + +impl From<LocationArgs> for Constraint<LocationConstraint> { + fn from(value: LocationArgs) -> Self { + if value.country.eq_ignore_ascii_case("any") { + return Constraint::Any; + } + + match (value.country, value.city, value.hostname) { + (country, None, None) => Constraint::Only(LocationConstraint::Country(country)), + (country, Some(city), None) => { + Constraint::Only(LocationConstraint::City(country, city)) + } + (country, Some(city), Some(hostname)) => { + Constraint::Only(LocationConstraint::Hostname(country, city, hostname)) + } + _ => unreachable!("invalid location arguments"), + } + } +} diff --git a/mullvad-cli/src/cmds/reset.rs b/mullvad-cli/src/cmds/reset.rs index d3e3ec3e62..a8c275a042 100644 --- a/mullvad-cli/src/cmds/reset.rs +++ b/mullvad-cli/src/cmds/reset.rs @@ -1,44 +1,32 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::Result; +use mullvad_management_interface::MullvadProxyClient; use std::io::stdin; -pub struct Reset; -#[mullvad_management_interface::async_trait] -impl Command for Reset { - fn name(&self) -> &'static str { - "factory-reset" +pub async fn handle() -> Result<()> { + if receive_confirmation().await { + let mut rpc = MullvadProxyClient::new().await?; + rpc.factory_reset().await?; + #[cfg(target_os = "linux")] + println!("If you're running systemd, to remove all logs, you must use journalctl"); } + Ok(()) +} - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()).about("Reset settings, caches and logs") - } +async fn receive_confirmation() -> bool { + println!("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service? [Yes/No (default)]"); - async fn run(&self, _: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - if Self::receive_confirmation() { - rpc.factory_reset(()) - .await - .map_err(|error| Error::RpcFailedExt("FAILED TO PERFORM FACTORY RESET", error))?; - #[cfg(target_os = "linux")] - println!("If you're running systemd, to remove all logs, you must use journalctl"); + tokio::task::spawn_blocking(|| loop { + let mut buf = String::new(); + if let Err(e) = stdin().read_line(&mut buf) { + eprintln!("Couldn't read from STDIN: {e}"); + return false; } - Ok(()) - } -} - -impl Reset { - fn receive_confirmation() -> bool { - println!("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service? [Yes/No (default)]"); - loop { - let mut buf = String::new(); - if let Err(e) = stdin().read_line(&mut buf) { - eprintln!("Couldn't read from STDIN: {e}"); - return false; - } - match buf.trim() { - "Yes" => return true, - "No" | "no" | "" => return false, - _ => println!("Unexpected response. Please enter \"Yes\" or \"No\""), - } + match buf.trim() { + "Yes" => return true, + "No" | "no" | "" => return false, + _ => eprintln!("Unexpected response. Please enter \"Yes\" or \"No\""), } - } + }) + .await + .unwrap() } diff --git a/mullvad-cli/src/cmds/split_tunnel/linux.rs b/mullvad-cli/src/cmds/split_tunnel/linux.rs index 8b235f1027..5a66d899ab 100644 --- a/mullvad-cli/src/cmds/split_tunnel/linux.rs +++ b/mullvad-cli/src/cmds/split_tunnel/linux.rs @@ -1,82 +1,61 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct SplitTunnel; - -#[mullvad_management_interface::async_trait] -impl Command for SplitTunnel { - fn name(&self) -> &'static str { - "split-tunnel" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about( - "Manage split tunneling. To launch applications outside \ - the tunnel, use the program 'mullvad-exclude' instead of this command.", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_pid_subcommand()) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("pid", pid_matches)) => Self::handle_pid_cmd(pid_matches).await, - _ => unreachable!("unhandled command"), - } - } -} - -fn create_pid_subcommand() -> clap::App<'static> { - clap::App::new("pid") - .about("Manage processes to exclude from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("add").arg(clap::Arg::new("pid").required(true))) - .subcommand(clap::App::new("delete").arg(clap::Arg::new("pid").required(true))) - .subcommand(clap::App::new("clear")) - .subcommand(clap::App::new("list")) +/// Manage split tunneling. To launch applications outside the tunnel, use the program +/// 'mullvad-exclude' instead of this command +#[derive(Subcommand, Debug)] +pub enum SplitTunnel { + /// List all processes that are excluded from the tunnel + List, + /// Add a PID to exclude from the tunnel + Add { pid: i32 }, + /// Stop excluding a PID from the tunnel + Delete { pid: i32 }, + /// Stop excluding all processes from the tunnel + Clear, } impl SplitTunnel { - async fn handle_pid_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("add", matches)) => { - let pid: i32 = matches.value_of_t_or_exit("pid"); - new_rpc_client() + pub async fn handle(self) -> Result<()> { + match self { + SplitTunnel::List => { + let pids = MullvadProxyClient::new() .await? - .add_split_tunnel_process(pid) + .get_split_tunnel_processes() .await?; + + println!("Excluded PIDs:"); + for pid in &pids { + println!("{pid}"); + } + Ok(()) } - Some(("delete", matches)) => { - let pid: i32 = matches.value_of_t_or_exit("pid"); - new_rpc_client() + SplitTunnel::Add { pid } => { + MullvadProxyClient::new() .await? - .remove_split_tunnel_process(pid) + .add_split_tunnel_process(pid) .await?; + println!("Excluding process"); Ok(()) } - Some(("clear", _)) => { - new_rpc_client() + SplitTunnel::Delete { pid } => { + MullvadProxyClient::new() .await? - .clear_split_tunnel_processes(()) + .remove_split_tunnel_process(pid) .await?; + println!("Stopped excluding process"); Ok(()) } - Some(("list", _)) => { - let mut pids_stream = new_rpc_client() - .await? - .get_split_tunnel_processes(()) + SplitTunnel::Clear => { + MullvadProxyClient::new() .await? - .into_inner(); - println!("Excluded PIDs:"); - - while let Some(pid) = pids_stream.message().await? { - println!(" {pid}"); - } - + .clear_split_tunnel_processes() + .await?; + println!("Stopped excluding all processes"); Ok(()) } - _ => unreachable!("unhandled command"), } } } diff --git a/mullvad-cli/src/cmds/split_tunnel/windows.rs b/mullvad-cli/src/cmds/split_tunnel/windows.rs index a133ab9682..f17a3382f3 100644 --- a/mullvad-cli/src/cmds/split_tunnel/windows.rs +++ b/mullvad-cli/src/cmds/split_tunnel/windows.rs @@ -1,157 +1,110 @@ -use std::{ffi::OsStr, path::Path}; +use anyhow::Result; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; -use crate::{new_rpc_client, Command, Result}; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct SplitTunnel; +use super::super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for SplitTunnel { - fn name(&self) -> &'static str { - "split-tunnel" - } +/// Set options for applications to exclude from the tunnel. +#[derive(Subcommand, Debug)] +pub enum SplitTunnel { + /// Display the split tunnel status and apps + Get { + /// List processes that are currently being excluded, as well as whether they are + /// excluded because of their executable paths or because they're subprocesses of + /// such processes + #[arg(long)] + list_processes: bool, + }, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Set options for applications to exclude from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_app_subcommand()) - .subcommand( - clap::App::new("set") - .about("Enable or disable split tunnel") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand(clap::App::new("get").about("Display the split tunnel status")) - .subcommand(create_pid_subcommand()) - } + /// Enable or disable split tunnel + Set { policy: BooleanOption }, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("app", matches)) => Self::handle_app_subcommand(matches).await, - Some(("pid", matches)) => Self::handle_pid_subcommand(matches).await, - Some(("get", _)) => self.get().await, - Some(("set", matches)) => { - let enabled = matches.value_of("policy").expect("missing policy"); - self.set(enabled == "on").await - } - _ => { - unreachable!("unhandled command"); - } - } - } -} - -fn create_app_subcommand() -> clap::App<'static> { - clap::App::new("app") - .about("Manage applications to exclude from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("list")) - .subcommand(clap::App::new("add").arg(clap::Arg::new("path").required(true))) - .subcommand(clap::App::new("remove").arg(clap::Arg::new("path").required(true))) - .subcommand(clap::App::new("clear")) + /// Manage applications to exclude from the tunnel + #[clap(subcommand)] + App(App), } -fn create_pid_subcommand() -> clap::App<'static> { - clap::App::new("pid") - .about("Manages processes (PIDs) excluded from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("list") - .about("List processes that are currently being excluded, i.e. their PIDs, as well as whether \ - they are excluded because of their executable paths or because they're subprocesses of \ - such processes")) +#[derive(Subcommand, Debug)] +pub enum App { + Add { path: PathBuf }, + Remove { path: PathBuf }, + Clear, } impl SplitTunnel { - async fn handle_app_subcommand(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("list", _)) => { - let paths = new_rpc_client() - .await? - .get_settings(()) - .await? - .into_inner() - .split_tunnel - .unwrap() - .apps; + pub async fn handle(self) -> Result<()> { + match self { + SplitTunnel::Get { list_processes } => { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?.split_tunnel; + + let enable_exclusions = BooleanOption::from(settings.enable_exclusions); + + println!("Split tunneling state: {enable_exclusions}"); println!("Excluded applications:"); - for path in &paths { - println!(" {}", path); + for path in &settings.apps { + println!("{}", path.display()); + } + + if list_processes { + let processes = rpc.get_excluded_processes().await?; + for process in &processes { + let subproc = if process.inherited { "subprocess" } else { "" }; + println!( + "{:<7}{subproc:<12}{}", + process.pid, + Path::new(&process.image) + .file_name() + .unwrap_or(OsStr::new("unknown")) + .to_string_lossy() + ); + } } Ok(()) } - Some(("add", matches)) => { - let path: String = matches.value_of_t_or_exit("path"); - new_rpc_client().await?.add_split_tunnel_app(path).await?; + SplitTunnel::Set { policy } => { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_split_tunnel_state(*policy).await?; + println!("Split tunnel policy: {policy}"); Ok(()) } - Some(("remove", matches)) => { - let path: String = matches.value_of_t_or_exit("path"); - new_rpc_client() + SplitTunnel::App(subcmd) => Self::app(subcmd).await, + } + } + + async fn app(subcmd: App) -> Result<()> { + match subcmd { + App::Add { path } => { + MullvadProxyClient::new() .await? - .remove_split_tunnel_app(path) + .add_split_tunnel_app(path) .await?; + println!("Added path to excluded apps list"); Ok(()) } - Some(("clear", _)) => { - new_rpc_client().await?.clear_split_tunnel_apps(()).await?; + App::Remove { path } => { + MullvadProxyClient::new() + .await? + .remove_split_tunnel_app(path) + .await?; + println!("Stopped excluding app from tunnel"); Ok(()) } - _ => unreachable!("unhandled subcommand"), - } - } - - async fn handle_pid_subcommand(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("list", _)) => { - let processes = new_rpc_client() - .await? - .get_excluded_processes(()) + App::Clear => { + MullvadProxyClient::new() .await? - .into_inner(); - - for process in &processes.processes { - let subproc = if process.inherited { "subprocess" } else { "" }; - println!( - "{:<7}{subproc:<12}{}", - process.pid, - Path::new(&process.image) - .file_name() - .unwrap_or(OsStr::new("unknown")) - .to_string_lossy() - ); - } - + .clear_split_tunnel_apps() + .await?; + println!("Stopped excluding all apps"); Ok(()) } - _ => unreachable!("unhandled subcommand"), } } - - async fn set(&self, enabled: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_split_tunnel_state(enabled).await?; - println!("Changed split tunnel setting"); - Ok(()) - } - - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let enabled = rpc - .get_settings(()) - .await? - .into_inner() - .split_tunnel - .unwrap() - .enable_exclusions; - println!( - "Split tunnel status: {}", - if enabled { "on" } else { "off" } - ); - Ok(()) - } } diff --git a/mullvad-cli/src/cmds/status.rs b/mullvad-cli/src/cmds/status.rs index 828d17af0e..8ddd195333 100644 --- a/mullvad-cli/src/cmds/status.rs +++ b/mullvad-cli/src/cmds/status.rs @@ -1,127 +1,113 @@ -use crate::{format, new_rpc_client, Command, Error, Result}; -use mullvad_management_interface::{ - types::daemon_event::Event as EventType, ManagementServiceClient, -}; -use mullvad_types::{location::GeoIpLocation, states::TunnelState}; +use anyhow::Result; +use clap::{Args, Subcommand}; +use futures::StreamExt; +use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; +use mullvad_types::states::TunnelState; -pub struct Status; +use crate::format; -#[mullvad_management_interface::async_trait] -impl Command for Status { - fn name(&self) -> &'static str { - "status" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("View the state of the VPN tunnel") - .arg( - clap::Arg::new("verbose") - .short('v') - .help("Enables verbose output"), - ) - .arg( - clap::Arg::new("location") - .long("location") - .short('l') - .help("Prints the current location and IP. Based on GeoIP lookups"), - ) - .arg( - clap::Arg::new("debug") - .long("debug") - .global(true) - .help("Enables debug output"), - ) - .subcommand(clap::App::new("listen").about("Listen for VPN tunnel state changes")) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let debug = matches.is_present("debug"); - let verbose = matches.is_present("verbose"); - let show_full_location = matches.is_present("location"); - - let mut rpc = new_rpc_client().await?; - let state = rpc.get_tunnel_state(()).await?.into_inner(); - - if debug { - println!("Tunnel state: {state:#?}"); - } else { - let state = TunnelState::try_from(state).expect("invalid tunnel state"); - format::print_state(&state, verbose); - } +#[derive(Subcommand, Debug, PartialEq)] +pub enum Status { + /// Listen for tunnel state changes + Listen, +} - if show_full_location { - print_location(&mut rpc).await?; - } +#[derive(Args, Debug)] +pub struct StatusArgs { + /// Enable verbose output + #[arg(long, short = 'v')] + verbose: bool, - if matches.subcommand_matches("listen").is_some() { - let mut events = rpc.events_listen(()).await?.into_inner(); + /// Print the current location and IP, based on GeoIP lookups + #[arg(long, short = 'l')] + location: bool, - while let Some(event) = events.message().await? { - match event.event.unwrap() { - EventType::TunnelState(new_state) => { - let new_state = - TunnelState::try_from(new_state).expect("invalid tunnel state"); + /// Enable debug output + #[arg(long, short = 'd')] + debug: bool, +} - if debug { - println!("New tunnel state: {new_state:#?}"); - } else { - format::print_state(&new_state, verbose); - } +impl Status { + pub async fn listen(mut rpc: MullvadProxyClient, args: StatusArgs) -> Result<()> { + while let Some(event) = rpc.events_listen().await?.next().await { + match event? { + DaemonEvent::TunnelState(new_state) => { + if args.debug { + println!("New tunnel state: {new_state:#?}"); + } else { + format::print_state(&new_state, args.verbose); + } - match new_state { - TunnelState::Connected { .. } | TunnelState::Disconnected => { - if show_full_location { - print_location(&mut rpc).await?; - } + match new_state { + TunnelState::Connected { .. } | TunnelState::Disconnected => { + if args.location { + print_location(&mut rpc).await?; } - _ => {} } + _ => {} } - EventType::Settings(settings) => { - if debug { - println!("New settings: {settings:#?}"); - } + } + DaemonEvent::Settings(settings) => { + if args.debug { + println!("New settings: {settings:#?}"); } - EventType::RelayList(relay_list) => { - if debug { - println!("New relay list: {relay_list:#?}"); - } + } + DaemonEvent::RelayList(relay_list) => { + if args.debug { + println!("New relay list: {relay_list:#?}"); } - EventType::VersionInfo(app_version_info) => { - if debug { - println!("New app version info: {app_version_info:#?}"); - } + } + DaemonEvent::AppVersionInfo(app_version_info) => { + if args.debug { + println!("New app version info: {app_version_info:#?}"); } - EventType::Device(device) => { - if debug { - println!("Device event: {device:#?}"); - } + } + DaemonEvent::Device(device) => { + if args.debug { + println!("Device event: {device:#?}"); } - EventType::RemoveDevice(device) => { - if debug { - println!("Remove device event: {device:#?}"); - } + } + DaemonEvent::RemoveDevice(device) => { + if args.debug { + println!("Remove device event: {device:#?}"); } } } } - Ok(()) } } -async fn print_location(rpc: &mut ManagementServiceClient) -> Result<()> { - let location = match rpc.get_current_location(()).await { - Ok(response) => GeoIpLocation::try_from(response.into_inner()).expect("invalid geoip data"), - Err(status) => { - if status.code() == mullvad_management_interface::Code::NotFound { +pub async fn handle(cmd: Option<Status>, args: StatusArgs) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let state = rpc.get_tunnel_state().await?; + + if args.debug { + println!("Tunnel state: {state:#?}"); + } else { + format::print_state(&state, args.verbose); + } + + if args.location { + print_location(&mut rpc).await?; + } + + if cmd == Some(Status::Listen) { + Status::listen(rpc, args).await?; + } + Ok(()) +} + +async fn print_location(rpc: &mut MullvadProxyClient) -> Result<()> { + let location = match rpc.get_current_location().await { + Ok(location) => location, + Err(error) => match &error { + mullvad_management_interface::Error::NoLocationData => { println!("Location data unavailable"); return Ok(()); - } else { - return Err(Error::RpcFailed(status)); } - } + _ => return Err(error.into()), + }, }; if let Some(ipv4) = location.ipv4 { println!("IPv4: {ipv4}"); diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 1d2fd1a7bb..120fad327d 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -1,436 +1,204 @@ -use crate::{new_rpc_client, Command, Error, Result}; -use mullvad_management_interface::types::{self, Timestamp, TunnelOptions}; -use mullvad_types::wireguard::DEFAULT_ROTATION_INTERVAL; -use std::{convert::TryFrom, time::Duration}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{ + relay_constraints::Constraint, + wireguard::{QuantumResistantState, RotationInterval, DEFAULT_ROTATION_INTERVAL}, +}; -pub struct Tunnel; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for Tunnel { - fn name(&self) -> &'static str { - "tunnel" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Manage tunnel specific options") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_openvpn_subcommand()) - .subcommand(create_wireguard_subcommand()) - .subcommand(create_ipv6_subcommand()) - } +#[derive(Subcommand, Debug)] +pub enum Tunnel { + /// Show current tunnel options + Get, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("openvpn", openvpn_matches)) => Self::handle_openvpn_cmd(openvpn_matches).await, - Some(("wireguard", wg_matches)) => Self::handle_wireguard_cmd(wg_matches).await, - Some(("ipv6", ipv6_matches)) => Self::handle_ipv6_cmd(ipv6_matches).await, - _ => { - unreachable!("unhandled command"); - } - } - } + /// Set tunnel options + #[clap(subcommand)] + Set(TunnelOptions), } -fn create_wireguard_subcommand() -> clap::App<'static> { - let subcmd = clap::App::new("wireguard") - .about("Manage options for Wireguard tunnels") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_wireguard_mtu_subcommand()) - .subcommand(create_wireguard_quantum_resistant_tunnel_subcommand()) - .subcommand(create_wireguard_keys_subcommand()); - #[cfg(windows)] - { - subcmd.subcommand(create_wireguard_use_wg_nt_subcommand()) - } - #[cfg(not(windows))] - { - subcmd - } -} - -fn create_wireguard_mtu_subcommand() -> clap::App<'static> { - clap::App::new("mtu") - .about("Configure the MTU of the wireguard tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand(clap::App::new("unset")) - .subcommand(clap::App::new("set").arg(clap::Arg::new("mtu").required(true))) -} +#[derive(Subcommand, Debug, Clone)] +pub enum TunnelOptions { + /// Manage options for OpenVPN tunnels + #[clap(arg_required_else_help = true)] + Openvpn { + /// Configure the mssfix parameter, or 'any' + #[arg(long, short = 'm')] + mssfix: Option<Constraint<u16>>, + }, -fn create_wireguard_quantum_resistant_tunnel_subcommand() -> clap::App<'static> { - clap::App::new("quantum-resistant-tunnel") - .about("Controls the quantum-resistant PSK exchange in the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("set").arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off", "auto"]), - ), - ) -} + /// Manage options for WireGuard tunnels + #[clap(arg_required_else_help = true)] + Wireguard { + /// Configure the tunnel MTU, or 'any' + #[arg(long, short = 'm')] + mtu: Option<Constraint<u16>>, + /// Configure quantum-resistant key exchange + #[arg(long)] + quantum_resistant: Option<QuantumResistantState>, + /// The key rotation interval. Number of hours, or 'any' + #[arg(long)] + rotation_interval: Option<Constraint<RotationInterval>>, + /// Rotate WireGuard key + #[clap(subcommand)] + rotate_key: Option<RotateKey>, + }, -fn create_wireguard_keys_subcommand() -> clap::App<'static> { - clap::App::new("key") - .about("Manage your wireguard key") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("check")) - .subcommand(clap::App::new("regenerate")) - .subcommand(create_wireguard_keys_rotation_interval_subcommand()) + /// Enable or disable IPv6 in the tunnel + #[clap(arg_required_else_help = true)] + Ipv6 { state: BooleanOption }, } -#[cfg(windows)] -fn create_wireguard_use_wg_nt_subcommand() -> clap::App<'static> { - clap::App::new("use-wireguard-nt") - .about("Enable or disable wireguard-nt") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("set").arg( - clap::Arg::new("policy") - .required(true) - .takes_value(true) - .possible_values(["on", "off"]), - ), - ) -} - -fn create_wireguard_keys_rotation_interval_subcommand() -> clap::App<'static> { - clap::App::new("rotation-interval") - .about("Manage automatic key rotation (given in hours)") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand(clap::App::new("reset").about("Use the default rotation interval")) - .subcommand(clap::App::new("set").arg(clap::Arg::new("interval").required(true))) -} - -fn create_openvpn_subcommand() -> clap::App<'static> { - clap::App::new("openvpn") - .about("Manage options for OpenVPN tunnels") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_openvpn_mssfix_subcommand()) -} - -fn create_openvpn_mssfix_subcommand() -> clap::App<'static> { - clap::App::new("mssfix") - .about("Configure the optional mssfix parameter") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand(clap::App::new("unset")) - .subcommand(clap::App::new("set").arg(clap::Arg::new("mssfix").required(true))) -} - -fn create_ipv6_subcommand() -> clap::App<'static> { - clap::App::new("ipv6") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("set").arg( - clap::Arg::new("policy") - .required(true) - .takes_value(true) - .possible_values(["on", "off"]), - ), - ) +#[derive(Subcommand, Debug, Clone)] +pub enum RotateKey { + /// Replace the WireGuard key with a new one + RotateKey, } impl Tunnel { - async fn handle_openvpn_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("mssfix", mssfix_matches)) => { - Self::handle_openvpn_mssfix_cmd(mssfix_matches).await - } - _ => unreachable!("unhandled command"), + pub async fn handle(self) -> Result<()> { + match self { + Tunnel::Get => Self::get().await, + Tunnel::Set(options) => Self::set(options).await, } } - async fn handle_openvpn_mssfix_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("get", _)) => Self::process_openvpn_mssfix_get().await, - Some(("unset", _)) => Self::process_openvpn_mssfix_unset().await, - Some(("set", set_matches)) => Self::process_openvpn_mssfix_set(set_matches).await, - _ => unreachable!("unhandled command"), - } - } + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let tunnel_options = rpc.get_settings().await?.tunnel_options; - async fn handle_wireguard_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("mtu", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_mtu_get().await, - Some(("set", matches)) => Self::process_wireguard_mtu_set(matches).await, - Some(("unset", _)) => Self::process_wireguard_mtu_unset().await, - _ => unreachable!("unhandled command"), - }, + println!("OpenVPN options"); - Some(("key", matches)) => match matches.subcommand() { - Some(("check", _)) => Self::process_wireguard_key_check().await, - Some(("regenerate", _)) => Self::process_wireguard_key_generate().await, - Some(("rotation-interval", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_rotation_interval_get().await, - Some(("set", matches)) => { - Self::process_wireguard_rotation_interval_set(matches).await - } - Some(("reset", _)) => Self::process_wireguard_rotation_interval_reset().await, - _ => unreachable!("unhandled command"), - }, - _ => unreachable!("unhandled command"), - }, - - Some(("quantum-resistant-tunnel", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_quantum_resistant_tunnel_get().await, - Some(("set", matches)) => { - Self::process_wireguard_quantum_resistant_tunnel_set(matches).await - } - _ => unreachable!("unhandled command"), - }, + println!( + "{:<4}{:<24}{}", + "", + "mssfix:", + tunnel_options + .openvpn + .mssfix + .map(|val| val.to_string()) + .unwrap_or("unset".to_string()), + ); - #[cfg(windows)] - Some(("use-wireguard-nt", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_use_wg_nt_get().await, - Some(("set", matches)) => Self::process_wireguard_use_wg_nt_set(matches).await, - _ => unreachable!("unhandled command"), - }, + println!("WireGuard options"); - _ => unreachable!("unhandled command"), - } - } + println!( + "{:<4}{:<24}{}", + "", + "MTU:", + tunnel_options + .wireguard + .mtu + .map(|val| val.to_string()) + .unwrap_or("unset".to_string()), + ); + println!( + "{:<4}{:<24}{}", + "", "Quantum resistance:", tunnel_options.wireguard.quantum_resistant, + ); - async fn process_wireguard_mtu_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - let mtu = tunnel_options.wireguard.unwrap().mtu; + let key = rpc.get_wireguard_key().await?; + println!("{:<4}{:<24}{}", "", "Public key:", key.key,); println!( - "mtu: {}", - if mtu != 0 { - mtu.to_string() - } else { - "unset".to_string() + "{:<4}{:<24}{}", + "", + "", + format_args!("Created {}", key.created.with_timezone(&chrono::Local)), + ); + println!( + "{:<4}{:<24}{}", + "", + "Rotation interval:", + match tunnel_options.wireguard.rotation_interval { + Some(interval) => interval.to_string(), + None => "unset".to_string(), }, ); - Ok(()) - } - - async fn process_wireguard_mtu_set(matches: &clap::ArgMatches) -> Result<()> { - let mtu = matches.value_of_t_or_exit::<u16>("mtu"); - let mut rpc = new_rpc_client().await?; - rpc.set_wireguard_mtu(mtu as u32).await?; - println!("Wireguard MTU has been updated"); - Ok(()) - } - - async fn process_wireguard_mtu_unset() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_wireguard_mtu(0).await?; - println!("Wireguard MTU has been unset"); - Ok(()) - } - - async fn process_wireguard_quantum_resistant_tunnel_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - match tunnel_options - .wireguard - .unwrap() - .quantum_resistant - .and_then(|state| types::quantum_resistant_state::State::from_i32(state.state)) - { - Some(types::quantum_resistant_state::State::On) => println!("enabled"), - Some(types::quantum_resistant_state::State::Off) => println!("disabled"), - None | Some(types::quantum_resistant_state::State::Auto) => println!("auto"), - } - Ok(()) - } - async fn process_wireguard_quantum_resistant_tunnel_set( - matches: &clap::ArgMatches, - ) -> Result<()> { - let quantum_resistant = match matches.value_of("policy").unwrap() { - "auto" => types::quantum_resistant_state::State::Auto, - "on" => types::quantum_resistant_state::State::On, - "off" => types::quantum_resistant_state::State::Off, - _ => unreachable!("invalid PQ state"), - }; - let mut rpc = new_rpc_client().await?; - rpc.set_quantum_resistant_tunnel(types::QuantumResistantState { - state: i32::from(quantum_resistant), - }) - .await?; - println!("Updated quantum resistant tunnel setting"); - Ok(()) - } + println!("Generic options"); - #[cfg(windows)] - async fn process_wireguard_use_wg_nt_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - if tunnel_options.wireguard.unwrap().use_wireguard_nt { - println!("enabled"); + if tunnel_options.generic.enable_ipv6 { + println!("{:<4}{:<24}on", "", "IPv6:"); } else { - println!("disabled"); + println!("{:<4}{:<24}off", "", "IPv6:"); } - Ok(()) - } - #[cfg(windows)] - async fn process_wireguard_use_wg_nt_set(matches: &clap::ArgMatches) -> Result<()> { - let new_state = matches.value_of("policy").unwrap() == "on"; - let mut rpc = new_rpc_client().await?; - rpc.set_use_wireguard_nt(new_state).await?; - println!("Updated wireguard-nt setting"); Ok(()) } - async fn process_wireguard_key_check() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let key = rpc.get_wireguard_key(()).await; - let key = match key { - Ok(response) => Some(response.into_inner()), - Err(status) => { - if status.code() == mullvad_management_interface::Code::NotFound { - None - } else { - return Err(Error::RpcFailedExt("Failed to obtain key", status)); - } + async fn set(options: TunnelOptions) -> Result<()> { + match options { + TunnelOptions::Openvpn { mssfix } => Self::handle_openvpn(mssfix).await, + TunnelOptions::Wireguard { + mtu, + quantum_resistant, + rotation_interval, + rotate_key, + } => { + Self::handle_wireguard(mtu, quantum_resistant, rotation_interval, rotate_key).await } - }; - if let Some(key) = key { - println!("Current key : {}", base64::encode(&key.key)); - println!( - "Key created on : {}", - Self::format_key_timestamp(&key.created.unwrap()) - ); - } else { - println!("No key is set"); - return Ok(()); + TunnelOptions::Ipv6 { state } => Self::handle_ipv6(state).await, } - Ok(()) } - async fn process_wireguard_key_generate() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.rotate_wireguard_key(()).await?; - println!("Rotated WireGuard key"); + async fn handle_ipv6(state: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_enable_ipv6(*state).await?; + println!("IPv6: {state}"); Ok(()) } - async fn process_wireguard_rotation_interval_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - match tunnel_options.wireguard.unwrap().rotation_interval { - Some(interval) => { - let hours = duration_hours(&Duration::try_from(interval).unwrap()); - println!("Rotation interval: {hours} hour(s)"); - } - None => println!( - "Rotation interval: default ({} hours)", - duration_hours(&DEFAULT_ROTATION_INTERVAL) - ), - } - Ok(()) - } - - async fn process_wireguard_rotation_interval_set(matches: &clap::ArgMatches) -> Result<()> { - let rotate_interval = matches.value_of_t_or_exit::<u64>("interval"); - let mut rpc = new_rpc_client().await?; - rpc.set_wireguard_rotation_interval( - types::Duration::try_from(Duration::from_secs(60 * 60 * rotate_interval)) - .expect("Failed to convert rotation interval to prost_types::Duration"), - ) - .await?; - println!("Set key rotation interval: {rotate_interval} hour(s)"); - Ok(()) - } - - async fn process_wireguard_rotation_interval_reset() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.reset_wireguard_rotation_interval(()).await?; - println!( - "Set key rotation interval: default ({} hours)", - duration_hours(&DEFAULT_ROTATION_INTERVAL) - ); - Ok(()) - } + async fn handle_openvpn(mssfix: Option<Constraint<u16>>) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; - async fn handle_ipv6_cmd(matches: &clap::ArgMatches) -> Result<()> { - if matches.subcommand_matches("get").is_some() { - Self::process_ipv6_get().await - } else if let Some(m) = matches.subcommand_matches("set") { - Self::process_ipv6_set(m).await - } else { - unreachable!("unhandled command"); + if let Some(mssfix) = mssfix { + rpc.set_openvpn_mssfix(mssfix.option()).await?; + println!("mssfix parameter has been updated"); } - } - async fn process_openvpn_mssfix_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - let mssfix = tunnel_options.openvpn.unwrap().mssfix; - println!( - "mssfix: {}", - if mssfix != 0 { - mssfix.to_string() - } else { - "unset".to_string() - }, - ); Ok(()) } - async fn get_tunnel_options() -> Result<TunnelOptions> { - let mut rpc = new_rpc_client().await?; - Ok(rpc - .get_settings(()) - .await? - .into_inner() - .tunnel_options - .unwrap()) - } + async fn handle_wireguard( + mtu: Option<Constraint<u16>>, + quantum_resistant: Option<QuantumResistantState>, + rotation_interval: Option<Constraint<RotationInterval>>, + rotate_key: Option<RotateKey>, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; - async fn process_openvpn_mssfix_unset() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_openvpn_mssfix(0).await?; - println!("mssfix parameter has been unset"); - Ok(()) - } + if let Some(mtu) = mtu { + rpc.set_wireguard_mtu(mtu.option()).await?; + println!("MTU parameter has been updated"); + } - async fn process_openvpn_mssfix_set(matches: &clap::ArgMatches) -> Result<()> { - let new_value = matches.value_of_t_or_exit::<u16>("mssfix"); - let mut rpc = new_rpc_client().await?; - rpc.set_openvpn_mssfix(new_value as u32).await?; - println!("mssfix parameter has been updated"); - Ok(()) - } + if let Some(quantum_resistant) = quantum_resistant { + rpc.set_quantum_resistant_tunnel(quantum_resistant).await?; + println!("Quantum resistant setting has been updated"); + } - async fn process_ipv6_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - println!( - "IPv6: {}", - if tunnel_options.generic.unwrap().enable_ipv6 { - "on" - } else { - "off" + if let Some(interval) = rotation_interval { + match interval { + Constraint::Only(interval) => { + rpc.set_wireguard_rotation_interval(interval).await?; + println!("Set key rotation interval to {}", interval); + } + Constraint::Any => { + rpc.reset_wireguard_rotation_interval().await?; + println!( + "Reset key rotation interval to {}", + RotationInterval::new(DEFAULT_ROTATION_INTERVAL).unwrap() + ); + } } - ); - Ok(()) - } - - async fn process_ipv6_set(matches: &clap::ArgMatches) -> Result<()> { - let enabled = matches.value_of("policy").unwrap() == "on"; + } - let mut rpc = new_rpc_client().await?; - rpc.set_enable_ipv6(enabled).await?; - if enabled { - println!("Enabled IPv6"); - } else { - println!("Disabled IPv6"); + if matches!(rotate_key, Some(RotateKey::RotateKey)) { + rpc.rotate_wireguard_key().await?; + println!("Rotated WireGuard key"); } - Ok(()) - } - fn format_key_timestamp(timestamp: &Timestamp) -> String { - let ndt = chrono::NaiveDateTime::from_timestamp(timestamp.seconds, timestamp.nanos as u32); - let utc = chrono::DateTime::<chrono::Utc>::from_utc(ndt, chrono::Utc); - utc.with_timezone(&chrono::Local).to_string() + Ok(()) } } - -fn duration_hours(duration: &Duration) -> u64 { - duration.as_secs() / 60 / 60 -} diff --git a/mullvad-cli/src/cmds/tunnel_state.rs b/mullvad-cli/src/cmds/tunnel_state.rs new file mode 100644 index 0000000000..383b343ef4 --- /dev/null +++ b/mullvad-cli/src/cmds/tunnel_state.rs @@ -0,0 +1,85 @@ +use crate::format; +use anyhow::{anyhow, Result}; +use futures::{Stream, StreamExt}; +use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; +use mullvad_types::states::TunnelState; + +pub async fn connect(wait: bool) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let listener = if wait { + Some(rpc.events_listen().await?) + } else { + None + }; + + if rpc.connect_tunnel().await? { + if let Some(receiver) = listener { + wait_for_tunnel_state(receiver, |state| match state { + TunnelState::Connected { .. } => Ok(true), + TunnelState::Error(_) => Err(anyhow!("Failed to connect")), + _ => Ok(false), + }) + .await?; + } + } + + Ok(()) +} + +pub async fn disconnect(wait: bool) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let listener = if wait { + Some(rpc.events_listen().await?) + } else { + None + }; + + if rpc.disconnect_tunnel().await? { + if let Some(receiver) = listener { + wait_for_tunnel_state(receiver, |state| Ok(state.is_disconnected())).await?; + } + } + + Ok(()) +} + +pub async fn reconnect(wait: bool) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let listener = if wait { + Some(rpc.events_listen().await?) + } else { + None + }; + + if rpc.reconnect_tunnel().await? { + if let Some(receiver) = listener { + wait_for_tunnel_state(receiver, |state| match state { + TunnelState::Connected { .. } => Ok(true), + TunnelState::Error(_) => Err(anyhow!("Failed to reconnect")), + _ => Ok(false), + }) + .await?; + } + } + + Ok(()) +} + +async fn wait_for_tunnel_state( + mut event_stream: impl Stream<Item = std::result::Result<DaemonEvent, mullvad_management_interface::Error>> + + Unpin, + matches_event: impl Fn(&TunnelState) -> Result<bool>, +) -> Result<()> { + while let Some(state) = event_stream.next().await { + if let DaemonEvent::TunnelState(new_state) = state? { + format::print_state(&new_state, false); + if matches_event(&new_state)? { + return Ok(()); + } + } + } + Err(anyhow!("Failed to wait for expected tunnel state")) +} diff --git a/mullvad-cli/src/cmds/version.rs b/mullvad-cli/src/cmds/version.rs index 2f034eff32..9a0cce2f41 100644 --- a/mullvad-cli/src/cmds/version.rs +++ b/mullvad-cli/src/cmds/version.rs @@ -1,54 +1,39 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::{Context, Result}; +use mullvad_management_interface::MullvadProxyClient; -pub struct Version; +pub async fn print() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let current_version = rpc + .get_current_version() + .await + .context("Failed to get current version")?; + println!("{:21}: {}", "Current version", current_version); + let version_info = rpc + .get_version_info() + .await + .context("Failed to get version info")?; + println!("{:21}: {}", "Is supported", version_info.supported); -#[mullvad_management_interface::async_trait] -impl Command for Version { - fn name(&self) -> &'static str { - "version" + if let Some(suggested_upgrade) = version_info.suggested_upgrade { + println!("{:21}: {}", "Suggested upgrade", suggested_upgrade); + } else { + println!("{:21}: none", "Suggested upgrade"); } - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Shows current version, and the currently supported versions") + if !version_info.latest_stable.is_empty() { + println!( + "{:21}: {}", + "Latest stable version", version_info.latest_stable + ); } - async fn run(&self, _: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let current_version = rpc - .get_current_version(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain current version", error))? - .into_inner(); - println!("{:21}: {}", "Current version", current_version); - let version_info = rpc - .get_version_info(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain version info", error))? - .into_inner(); - println!("{:21}: {}", "Is supported", version_info.supported); + let settings = rpc + .get_settings() + .await + .context("Failed to obtain settings")?; + if settings.show_beta_releases { + println!("{:21}: {}", "Latest beta version", version_info.latest_beta); + }; - if !version_info.suggested_upgrade.is_empty() { - println!( - "{:21}: {}", - "Suggested upgrade", version_info.suggested_upgrade - ); - } else { - println!("{:21}: none", "Suggested upgrade"); - } - - if !version_info.latest_stable.is_empty() { - println!( - "{:21}: {}", - "Latest stable version", version_info.latest_stable - ); - } - - let settings = rpc.get_settings(()).await?.into_inner(); - if settings.show_beta_releases { - println!("{:21}: {}", "Latest beta version", version_info.latest_beta); - }; - - Ok(()) - } + Ok(()) } diff --git a/mullvad-cli/src/location.rs b/mullvad-cli/src/location.rs deleted file mode 100644 index 2fff68998c..0000000000 --- a/mullvad-cli/src/location.rs +++ /dev/null @@ -1,81 +0,0 @@ -use mullvad_management_interface::types::RelayLocation; - -pub fn get_subcommand() -> clap::App<'static> { - clap::App::new("location") - .arg( - clap::Arg::new("country") - .help("The two letter country code, or 'any' for no preference.") - .required(true) - .index(1) - .validator(country_code_validator), - ) - .arg( - clap::Arg::new("city") - .help("The three letter city code") - .index(2) - .validator(city_code_validator), - ) - .arg(clap::Arg::new("hostname").help("The hostname").index(3)) -} - -pub fn get_constraint_from_args(matches: &clap::ArgMatches) -> RelayLocation { - let country = matches.value_of("country").unwrap(); - let city = matches.value_of("city"); - let hostname = matches.value_of("hostname"); - get_constraint(country, city, hostname) -} - -pub fn get_constraint<T: AsRef<str>>( - country: T, - city: Option<T>, - hostname: Option<T>, -) -> RelayLocation { - let country_original = country.as_ref(); - let country = country_original.to_lowercase(); - let city = city.map(|s| s.as_ref().to_lowercase()); - let hostname = hostname.map(|s| s.as_ref().to_lowercase()); - - match (country_original, city, hostname) { - ("any", None, None) => RelayLocation::default(), - ("any", ..) => clap::Error::raw( - clap::ErrorKind::InvalidValue, - "City can't be given when selecting 'any' country", - ) - .exit(), - (_, None, None) => RelayLocation { - country, - ..Default::default() - }, - (_, Some(city), None) => RelayLocation { - country, - city, - ..Default::default() - }, - (_, Some(city), Some(hostname)) => RelayLocation { - country, - city, - hostname, - }, - (..) => clap::Error::raw( - clap::ErrorKind::InvalidValue, - "Invalid country, city and hostname combination given", - ) - .exit(), - } -} - -pub fn country_code_validator(code: &str) -> std::result::Result<(), String> { - if code.len() == 2 || code == "any" { - Ok(()) - } else { - Err(String::from("Country codes must be two letters, or 'any'.")) - } -} - -pub fn city_code_validator(code: &str) -> std::result::Result<(), String> { - if code.len() == 3 { - Ok(()) - } else { - Err(String::from("City codes must be three letters")) - } -} diff --git a/mullvad-cli/src/main.rs b/mullvad-cli/src/main.rs index 39479d4054..a36ec58a81 100644 --- a/mullvad-cli/src/main.rs +++ b/mullvad-cli/src/main.rs @@ -1,150 +1,152 @@ #![deny(rust_2018_idioms)] -use clap::{crate_authors, crate_description}; #[cfg(all(unix, not(target_os = "android")))] -use clap_complete::{generator::generate_to, Shell}; -use mullvad_management_interface::async_trait; -use std::{collections::HashMap, io}; -use talpid_types::ErrorExt; - -pub use mullvad_management_interface::{self, new_rpc_client}; +use anyhow::anyhow; +use anyhow::Result; +use clap::Parser; mod cmds; mod format; -mod location; -mod state; +use cmds::*; -pub const BIN_NAME: &str = "mullvad"; +pub const BIN_NAME: &str = env!("CARGO_BIN_NAME"); -pub type Result<T> = std::result::Result<T, Error>; +#[derive(Debug, Parser)] +#[command(author, version = mullvad_version::VERSION, about, long_about = None)] +#[command(propagate_version = true)] +enum Cli { + /// Control and display information about your Mullvad account + #[clap(subcommand)] + Account(account::Account), -#[derive(err_derive::Error, Debug)] -pub enum Error { - #[error(display = "Failed to connect to daemon")] - DaemonNotRunning(#[error(source)] io::Error), + /// Control the daemon auto-connect setting + #[clap(subcommand)] + AutoConnect(auto_connect::AutoConnect), - #[error(display = "Management interface error")] - ManagementInterfaceError(#[error(source)] mullvad_management_interface::Error), + /// Receive notifications about beta updates + #[clap(subcommand)] + BetaProgram(beta_program::BetaProgram), - #[error(display = "RPC failed")] - RpcFailed(#[error(source)] mullvad_management_interface::Status), + /// Control whether to block network access when disconnected from VPN + #[clap(subcommand)] + LockdownMode(lockdown::LockdownMode), - #[error(display = "RPC failed: {}", _0)] - RpcFailedExt( - &'static str, - #[error(source)] mullvad_management_interface::Status, - ), + /// Configure DNS servers to use when connected + #[clap(subcommand)] + Dns(dns::Dns), - /// The given command is not correct in some way - #[error(display = "Invalid command: {}", _0)] - InvalidCommand(&'static str), + /// Control the allow local network sharing setting + #[clap(subcommand)] + Lan(lan::Lan), - #[error(display = "Command failed: {}", _0)] - CommandFailed(&'static str), + /// Connect to a VPN relay + Connect { + /// Wait until connected before exiting + #[arg(long, short = 'w')] + wait: bool, + }, - #[error(display = "Failed to listen for status updates")] - StatusListenerFailed, + /// Disconnect from the VPN + Disconnect { + /// Wait until disconnected before exiting + #[arg(long, short = 'w')] + wait: bool, + }, - //#[cfg(all(unix, not(target_os = "android")) - #[error(display = "Failed to generate shell completions")] - CompletionsError(#[error(source, no_from)] io::Error), + /// Reconnect to any matching VPN relay + Reconnect { + /// Wait until connected before exiting + #[arg(long, short = 'w')] + wait: bool, + }, - #[error(display = "{}", _0)] - Other(&'static str), -} + /// Manage use of bridges, socks proxies and Shadowsocks for OpenVPN. + /// Can make OpenVPN tunnels use Shadowsocks via one of the Mullvad bridge servers. + /// Can also make OpenVPN connect through any custom SOCKS5 proxy. + /// These settings also affect how the app reaches the API over Shadowsocks. + #[clap(subcommand)] + Bridge(bridge::Bridge), -#[tokio::main] -async fn main() { - let exit_code = match run().await { - Ok(_) => 0, - Err(error) => { - match &error { - Error::RpcFailed(status) => { - eprintln!("{}: {:?}: {}", error, status.code(), status.message()) - } - Error::RpcFailedExt(_message, status) => eprintln!( - "{}\nCaused by: {:?}: {}", - error, - status.code(), - status.message() - ), - error => eprintln!("{}", error.display_chain()), - } - 1 - } - }; - std::process::exit(exit_code); -} + /// Manage relay and tunnel constraints + #[clap(subcommand)] + Relay(relay::Relay), -async fn run() -> Result<()> { - env_logger::init(); + /// 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 + #[clap(subcommand)] + Obfuscation(obfuscation::Obfuscation), - let commands = cmds::get_commands(); - let app = build_cli(&commands); + #[cfg(any(target_os = "windows", target_os = "linux"))] + #[clap(subcommand)] + SplitTunnel(split_tunnel::SplitTunnel), + /// Return the state of the VPN tunnel + Status { + #[clap(subcommand)] + cmd: Option<status::Status>, + + #[clap(flatten)] + args: status::StatusArgs, + }, + + /// Manage tunnel options + #[clap(subcommand)] + Tunnel(tunnel::Tunnel), + + /// Show information about the current Mullvad version + /// and available versions + Version, + + /// Generate completion scripts for the specified shell #[cfg(all(unix, not(target_os = "android")))] - let app = app.subcommand( - clap::App::new("shell-completions") - .about("Generates completion scripts for your shell") - .arg( - clap::Arg::new("SHELL") - .required(true) - .possible_values(Shell::possible_values()) - .help("The shell to generate the script for"), - ) - .arg( - clap::Arg::new("DIR") - .allow_invalid_utf8(true) - .default_value("./") - .help("Output directory where the shell completions are written"), - ) - .setting(clap::AppSettings::Hidden), - ); + #[command(hide = true)] + ShellCompletions { + /// The shell to generate the script for + shell: clap_complete::Shell, - let app_matches = app.get_matches(); - match app_matches.subcommand() { - #[cfg(all(unix, not(target_os = "android")))] - Some(("shell-completions", sub_matches)) => { - let shell: Shell = sub_matches - .value_of("SHELL") - .unwrap() - .parse() - .expect("Invalid shell"); - let out_dir = sub_matches.value_of_os("DIR").unwrap(); - let mut app = build_cli(&commands); - generate_to(shell, &mut app, BIN_NAME, out_dir) - .map(|_output_file| ()) - .map_err(Error::CompletionsError) - } - Some((sub_name, sub_matches)) => { - if let Some(cmd) = commands.get(sub_name) { - cmd.run(sub_matches).await - } else { - unreachable!("No command matched"); - } - } - _ => { - unreachable!("No subcommand matches"); - } - } -} + /// Output directory where the shell completions are written + #[arg(default_value = "./")] + dir: std::path::PathBuf, + }, -fn build_cli(commands: &HashMap<&'static str, Box<dyn Command>>) -> clap::App<'static> { - clap::App::new(BIN_NAME) - .version(mullvad_version::VERSION) - .author(crate_authors!()) - .about(crate_description!()) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .global_setting(clap::AppSettings::DisableHelpSubcommand) - .global_setting(clap::AppSettings::DisableVersionFlag) - .subcommands(commands.values().map(|cmd| cmd.clap_subcommand())) + /// Reset settings, caches, and logs + FactoryReset, } -#[async_trait] -pub trait Command { - fn name(&self) -> &'static str; +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + + match Cli::parse() { + Cli::Account(cmd) => cmd.handle().await, + Cli::Bridge(cmd) => cmd.handle().await, + Cli::Connect { wait } => tunnel_state::connect(wait).await, + Cli::Reconnect { wait } => tunnel_state::reconnect(wait).await, + Cli::Disconnect { wait } => tunnel_state::disconnect(wait).await, + Cli::AutoConnect(cmd) => cmd.handle().await, + Cli::BetaProgram(cmd) => cmd.handle().await, + Cli::LockdownMode(cmd) => cmd.handle().await, + Cli::Dns(cmd) => cmd.handle().await, + Cli::Lan(cmd) => cmd.handle().await, + Cli::Obfuscation(cmd) => cmd.handle().await, + Cli::Version => version::print().await, + Cli::FactoryReset => reset::handle().await, + Cli::Relay(cmd) => cmd.handle().await, + Cli::Tunnel(cmd) => cmd.handle().await, + #[cfg(any(target_os = "windows", target_os = "linux"))] + Cli::SplitTunnel(cmd) => cmd.handle().await, + Cli::Status { cmd, args } => status::handle(cmd, args).await, - fn clap_subcommand(&self) -> clap::App<'static>; + #[cfg(all(unix, not(target_os = "android")))] + Cli::ShellCompletions { shell, dir } => { + use clap::CommandFactory; - async fn run(&self, matches: &clap::ArgMatches) -> Result<()>; + // FIXME: The shell completions include hidden commands (including "shell-completions") + println!("Generating shell completions to {}", dir.display()); + clap_complete::generate_to(shell, &mut Cli::command(), BIN_NAME, dir) + .map_err(|_| anyhow!("Failed to generate shell completions"))?; + Ok(()) + } + } } diff --git a/mullvad-cli/src/state.rs b/mullvad-cli/src/state.rs deleted file mode 100644 index 7b3dfdc955..0000000000 --- a/mullvad-cli/src/state.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::{Error, Result}; -use futures::{ - channel::{mpsc, mpsc::Receiver}, - SinkExt, -}; -use mullvad_management_interface::{ - types::daemon_event::Event as EventType, ManagementServiceClient, -}; -use mullvad_types::states::TunnelState; - -// Spawns a new task that listens for tunnel state changes and forwards it through the returned -// channel. Panics if called from outside of the Tokio runtime. -pub fn state_listen(mut rpc: ManagementServiceClient) -> Receiver<Result<TunnelState>> { - let (mut sender, receiver) = mpsc::channel::<Result<TunnelState>>(1); - tokio::spawn(async move { - match rpc.events_listen(()).await { - Ok(events) => { - let mut events = events.into_inner(); - loop { - let forward = match events.message().await { - Ok(Some(event)) => match event.event.unwrap() { - EventType::TunnelState(new_state) => { - Ok(TunnelState::try_from(new_state).expect("invalid tunnel state")) - } - _ => continue, - }, - Ok(None) => break, - Err(status) => Err(Error::RpcFailed(status)), - }; - - if sender.send(forward).await.is_err() { - break; - } - } - } - Err(status) => { - let _ = sender.send(Err(Error::RpcFailed(status))).await; - } - } - }); - - receiver -} |
