summaryrefslogtreecommitdiffhomepage
path: root/mullvad-cli/src
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2023-05-03 11:20:31 +0200
committerDavid Lönnhager <david.l@mullvad.net>2023-05-03 11:20:31 +0200
commit49ea114adddba1a1db6ffc6c440e743c01797a47 (patch)
tree66f1bf1e3e1d208e233e5622045503abe85a3a89 /mullvad-cli/src
parentbeaa6d3b80d9c9dfed99c710c793830db3ddc7ec (diff)
parentaade46c9c73c874e4153caa450e713d8f8b37760 (diff)
downloadmullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.tar.xz
mullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.zip
Merge branch 'update-clap'
Diffstat (limited to 'mullvad-cli/src')
-rw-r--r--mullvad-cli/src/cmds/account.rs434
-rw-r--r--mullvad-cli/src/cmds/auto_connect.rs63
-rw-r--r--mullvad-cli/src/cmds/beta_program.rs84
-rw-r--r--mullvad-cli/src/cmds/block_when_disconnected.rs68
-rw-r--r--mullvad-cli/src/cmds/bridge.rs623
-rw-r--r--mullvad-cli/src/cmds/connect.rs50
-rw-r--r--mullvad-cli/src/cmds/disconnect.rs47
-rw-r--r--mullvad-cli/src/cmds/dns.rs256
-rw-r--r--mullvad-cli/src/cmds/lan.rs69
-rw-r--r--mullvad-cli/src/cmds/lockdown.rs36
-rw-r--r--mullvad-cli/src/cmds/mod.rs138
-rw-r--r--mullvad-cli/src/cmds/obfuscation.rs177
-rw-r--r--mullvad-cli/src/cmds/reconnect.rs50
-rw-r--r--mullvad-cli/src/cmds/relay.rs1209
-rw-r--r--mullvad-cli/src/cmds/relay_constraints.rs34
-rw-r--r--mullvad-cli/src/cmds/reset.rs60
-rw-r--r--mullvad-cli/src/cmds/split_tunnel/linux.rs99
-rw-r--r--mullvad-cli/src/cmds/split_tunnel/windows.rs207
-rw-r--r--mullvad-cli/src/cmds/status.rs184
-rw-r--r--mullvad-cli/src/cmds/tunnel.rs534
-rw-r--r--mullvad-cli/src/cmds/tunnel_state.rs85
-rw-r--r--mullvad-cli/src/cmds/version.rs77
-rw-r--r--mullvad-cli/src/location.rs81
-rw-r--r--mullvad-cli/src/main.rs244
-rw-r--r--mullvad-cli/src/state.rs43
25 files changed, 1878 insertions, 3074 deletions
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
-}