summaryrefslogtreecommitdiffhomepage
path: root/mullvad-cli/src
diff options
context:
space:
mode:
authorJonathan <jonathan@mullvad.net>2023-12-05 10:03:08 +0100
committerJonathan <jonathan@mullvad.net>2024-01-03 14:38:41 +0100
commit4fdc34acbba60d5092e45ce3e513d30ec996c317 (patch)
tree80d3a23c1a96bd3d80e05ac66b530e39c252d48a /mullvad-cli/src
parentc510df96772b1e4ab7998e739ced42806c78e931 (diff)
downloadmullvadvpn-4fdc34acbba60d5092e45ce3e513d30ec996c317.tar.xz
mullvadvpn-4fdc34acbba60d5092e45ce3e513d30ec996c317.zip
Allow app to use custom socks5 and shadwosocks proxies
This PR has a couple of different purposes - Allow users to use socks5 local proxies with the CLI without having to be root nor use split-tunneling. This only works for OpenVPN. - Unify the types used by different proxy parts of the codebase, such as the Access Methods as well as some already existing OpenVPN proxy code. This PR changes the firewall on all desktop platforms as well as changes the routing table slightly on MacOS and Windows. On Linux the firewall code is modified to apply the appropriate firewall marks to all packages that go to a remote endpoint corresponding to the remote part of a local socks5 proxy. The firewall marks will allow the routing to be done without having to modify the routing table. On MacOS and Windows the routing table is modified to allow packages to go to that same endpoint to pass outside the VPN tunnel, it will additionally punch a hole in the firewall. The PR also migrates the settings file from version 7 to version 8 in order to properly and neatly unify Proxy related types. Finally it provides some slight extensions to the gRPC interface in order to allow for control over the custom proxy settings.
Diffstat (limited to 'mullvad-cli/src')
-rw-r--r--mullvad-cli/src/cmds/api_access.rs286
-rw-r--r--mullvad-cli/src/cmds/bridge.rs350
-rw-r--r--mullvad-cli/src/cmds/mod.rs1
-rw-r--r--mullvad-cli/src/cmds/proxies.rs214
4 files changed, 462 insertions, 389 deletions
diff --git a/mullvad-cli/src/cmds/api_access.rs b/mullvad-cli/src/cmds/api_access.rs
index c6e01c52d6..887d378fa4 100644
--- a/mullvad-cli/src/cmds/api_access.rs
+++ b/mullvad-cli/src/cmds/api_access.rs
@@ -1,10 +1,11 @@
use anyhow::{anyhow, Result};
use mullvad_management_interface::MullvadProxyClient;
-use mullvad_types::access_method::{AccessMethod, AccessMethodSetting, CustomAccessMethod};
-use std::net::IpAddr;
+use mullvad_types::access_method::{AccessMethod, AccessMethodSetting};
+use talpid_types::net::proxy::CustomProxy;
use clap::{Args, Subcommand};
-use talpid_types::net::{openvpn::SHADOWSOCKS_CIPHERS, TransportProtocol};
+
+use super::proxies::{ProxyEditParams, ShadowsocksAdd, Socks5LocalAdd, Socks5RemoteAdd};
#[derive(Subcommand, Debug, Clone)]
pub enum ApiAccess {
@@ -99,9 +100,7 @@ impl ApiAccess {
/// Edit the data of an API access method.
async fn edit(cmd: EditCustomCommands) -> Result<()> {
- use mullvad_types::access_method::{
- Shadowsocks, Socks5, Socks5Local, Socks5Remote, SocksAuth,
- };
+ use talpid_types::net::proxy::{Shadowsocks, Socks5Local, Socks5Remote, SocksAuth};
let mut rpc = MullvadProxyClient::new().await?;
let mut api_access_method = Self::get_access_method(&mut rpc, &cmd.item).await?;
@@ -109,49 +108,47 @@ impl ApiAccess {
let access_method = match api_access_method.as_custom() {
None => return Err(anyhow!("Can not edit built-in access method")),
Some(x) => match x.clone() {
- CustomAccessMethod::Shadowsocks(shadowsocks) => {
- let ip = cmd.params.ip.unwrap_or(shadowsocks.peer.ip());
- let port = cmd.params.port.unwrap_or(shadowsocks.peer.port());
+ CustomProxy::Shadowsocks(shadowsocks) => {
+ let ip = cmd.params.ip.unwrap_or(shadowsocks.endpoint.ip());
+ let port = cmd.params.port.unwrap_or(shadowsocks.endpoint.port());
let password = cmd.params.password.unwrap_or(shadowsocks.password);
let cipher = cmd.params.cipher.unwrap_or(shadowsocks.cipher);
AccessMethod::from(Shadowsocks::new((ip, port), cipher, password))
}
- CustomAccessMethod::Socks5(socks) => match socks {
- Socks5::Local(local) => {
- let remote_ip = cmd.params.ip.unwrap_or(local.remote_endpoint.address.ip());
- let remote_port = cmd
- .params
- .port
- .unwrap_or(local.remote_endpoint.address.port());
- let local_port = cmd.params.local_port.unwrap_or(local.local_port);
- let remote_peer_transport_protocol = cmd
- .params
- .transport_protocol
- .unwrap_or(local.remote_endpoint.protocol);
- AccessMethod::from(Socks5Local::new_with_transport_protocol(
- (remote_ip, remote_port),
- local_port,
- remote_peer_transport_protocol,
- ))
- }
- Socks5::Remote(remote) => {
- let ip = cmd.params.ip.unwrap_or(remote.peer.ip());
- let port = cmd.params.port.unwrap_or(remote.peer.port());
- AccessMethod::from(match remote.authentication {
- None => Socks5Remote::new((ip, port)),
- Some(SocksAuth { username, password }) => {
- let username = cmd.params.username.unwrap_or(username);
- let password = cmd.params.password.unwrap_or(password);
- let auth = SocksAuth { username, password };
- Socks5Remote::new_with_authentication((ip, port), auth)
- }
- })
- }
- },
+ CustomProxy::Socks5Local(local) => {
+ let remote_ip = cmd.params.ip.unwrap_or(local.remote_endpoint.address.ip());
+ let remote_port = cmd
+ .params
+ .port
+ .unwrap_or(local.remote_endpoint.address.port());
+ let local_port = cmd.params.local_port.unwrap_or(local.local_port);
+ let remote_peer_transport_protocol = cmd
+ .params
+ .transport_protocol
+ .unwrap_or(local.remote_endpoint.protocol);
+ AccessMethod::from(Socks5Local::new_with_transport_protocol(
+ (remote_ip, remote_port),
+ local_port,
+ remote_peer_transport_protocol,
+ ))
+ }
+ CustomProxy::Socks5Remote(remote) => {
+ let ip = cmd.params.ip.unwrap_or(remote.endpoint.ip());
+ let port = cmd.params.port.unwrap_or(remote.endpoint.port());
+ AccessMethod::from(match remote.auth {
+ None => Socks5Remote::new((ip, port)),
+ Some(SocksAuth { username, password }) => {
+ let username = cmd.params.username.unwrap_or(username);
+ let password = cmd.params.password.unwrap_or(password);
+ let auth = SocksAuth { username, password };
+ Socks5Remote::new_with_authentication((ip, port), auth)
+ }
+ })
+ }
},
};
- if let Some(name) = cmd.params.name {
+ if let Some(name) = cmd.name {
api_access_method.name = name;
};
api_access_method.access_method = access_method;
@@ -259,19 +256,12 @@ pub enum AddCustomCommands {
Shadowsocks {
/// An easy to remember name for this custom proxy
name: String,
- /// The IP of the remote Shadowsocks-proxy
- remote_ip: IpAddr,
- /// Port on which the remote Shadowsocks-proxy listens for traffic
- remote_port: u16,
- /// Password for authentication
- password: String,
- /// Cipher to use
- #[arg(long, value_parser = SHADOWSOCKS_CIPHERS)]
- cipher: String,
/// Disable the use of this custom access method. It has to be manually
/// enabled at a later stage to be used when accessing the Mullvad API.
#[arg(default_value_t = false, short, long)]
disabled: bool,
+ #[clap(flatten)]
+ add: ShadowsocksAdd,
},
}
@@ -281,53 +271,26 @@ pub enum AddSocks5Commands {
Remote {
/// An easy to remember name for this custom proxy
name: String,
- /// IP of the remote SOCKS5-proxy
- remote_ip: IpAddr,
- /// Port on which the remote SOCKS5-proxy listens for traffic
- remote_port: u16,
- #[clap(flatten)]
- authentication: Option<SocksAuthentication>,
/// Disable the use of this custom access method. It has to be manually
/// enabled at a later stage to be used when accessing the Mullvad API.
#[arg(default_value_t = false, short, long)]
disabled: bool,
+ #[clap(flatten)]
+ add: Socks5RemoteAdd,
},
/// Configure a local SOCKS5 proxy
Local {
/// An easy to remember name for this custom proxy
name: String,
- /// The port that the server on localhost is listening on
- local_port: u16,
- /// The IP of the remote peer
- remote_ip: IpAddr,
- /// The port of the remote peer
- remote_port: u16,
- /// The Mullvad App can not know which transport protocol that the
- /// remote peer accepts, but it needs to know this in order to correctly
- /// exempt the connection traffic in the firewall.
- ///
- /// By default, the transport protocol is assumed to be `TCP`, but it
- /// can optionally be set to `UDP` as well.
- #[arg(long, default_value_t = TransportProtocol::Tcp)]
- transport_protocol: TransportProtocol,
/// Disable the use of this custom access method. It has to be manually
/// enabled at a later stage to be used when accessing the Mullvad API.
#[arg(default_value_t = false, short, long)]
disabled: bool,
+ #[clap(flatten)]
+ add: Socks5LocalAdd,
},
}
-#[derive(Args, Debug, Clone)]
-#[group(requires_all = ["username", "password"])] // https://github.com/clap-rs/clap/issues/5092
-pub struct SocksAuthentication {
- /// Username for authentication against a remote SOCKS5 proxy
- #[arg(short, long, required = false)]
- username: String,
- /// Password for authentication against a remote SOCKS5 proxy
- #[arg(short, long, required = false)]
- password: String,
-}
-
impl AddCustomCommands {
fn name(&self) -> &str {
match self {
@@ -374,9 +337,12 @@ pub struct EditCustomCommands {
/// Which API access method to edit
#[clap(flatten)]
item: SelectItem,
+ /// Name of the API access method in the Mullvad client [All]
+ #[arg(long)]
+ name: Option<String>,
/// Editing parameters
#[clap(flatten)]
- params: EditParams,
+ params: ProxyEditParams,
}
#[derive(Args, Debug, Clone)]
@@ -384,27 +350,8 @@ pub struct EditParams {
/// Name of the API access method in the Mullvad client [All]
#[arg(long)]
name: Option<String>,
- /// Username for authentication [Socks5 (Remote proxy)]
- #[arg(long)]
- username: Option<String>,
- /// Password for authentication [Socks5 (Remote proxy), Shadowsocks]
- #[arg(long)]
- password: Option<String>,
- /// Cipher to use [Shadowsocks]
- #[arg(value_parser = SHADOWSOCKS_CIPHERS, long)]
- cipher: Option<String>,
- /// The IP of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks]
- #[arg(long)]
- ip: Option<IpAddr>,
- /// The port of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks]
- #[arg(long)]
- port: Option<u16>,
- /// The port that the server on localhost is listening on [Socks5 (Local proxy)]
- #[arg(long)]
- local_port: Option<u16>,
- /// The transport protocol used by the remote proxy [Socks5 (Local proxy)]
- #[arg(long)]
- transport_protocol: Option<TransportProtocol>,
+ #[clap(flatten)]
+ edit_params: ProxyEditParams,
}
/// Implement conversions from CLI types to Daemon types.
@@ -412,23 +359,29 @@ pub struct EditParams {
/// Since these are not supposed to be used outside of the CLI,
/// we define them in a hidden-away module.
mod conversions {
- use super::{AddCustomCommands, AddSocks5Commands, SocksAuthentication};
+ use crate::cmds::proxies::SocksAuthentication;
+
+ use super::{AddCustomCommands, AddSocks5Commands};
use mullvad_types::access_method as daemon_types;
+ use talpid_types::net::proxy as talpid_types;
impl From<AddCustomCommands> for daemon_types::AccessMethod {
fn from(value: AddCustomCommands) -> Self {
match value {
AddCustomCommands::Socks5(socks) => match socks {
AddSocks5Commands::Local {
- local_port,
- remote_ip,
- remote_port,
name: _,
disabled: _,
- transport_protocol,
+ add,
} => {
+ let (local_port, remote_ip, remote_port, transport_protocol) = (
+ add.local_port,
+ add.remote_ip,
+ add.remote_port,
+ add.transport_protocol,
+ );
println!("Adding SOCKS5-proxy: localhost:{local_port} => {remote_ip}:{remote_port}/{transport_protocol}");
- daemon_types::Socks5Local::new_with_transport_protocol(
+ talpid_types::Socks5Local::new_with_transport_protocol(
(remote_ip, remote_port),
local_port,
transport_protocol,
@@ -436,41 +389,38 @@ mod conversions {
.into()
}
AddSocks5Commands::Remote {
- remote_ip,
- remote_port,
- authentication,
+ add,
name: _,
disabled: _,
- } => daemon_types::AccessMethod::from(daemon_types::Socks5::Remote(
- match authentication {
- Some(SocksAuthentication { username, password }) => {
- println!("Adding SOCKS5-proxy: {username}:{password}@{remote_ip}:{remote_port}");
- let auth =
- mullvad_types::access_method::SocksAuth { username, password };
- daemon_types::Socks5Remote::new_with_authentication(
- (remote_ip, remote_port),
- auth,
- )
- }
- None => {
- println!("Adding SOCKS5-proxy: {remote_ip}:{remote_port}");
- daemon_types::Socks5Remote::new((remote_ip, remote_port))
- }
- },
- )),
+ } => daemon_types::AccessMethod::from(match add.authentication {
+ Some(SocksAuthentication { username, password }) => {
+ println!(
+ "Adding SOCKS5-proxy: {username}:{password}@{}:{}",
+ add.remote_ip, add.remote_port
+ );
+ let auth = talpid_types::SocksAuth { username, password };
+ talpid_types::Socks5Remote::new_with_authentication(
+ (add.remote_ip, add.remote_port),
+ auth,
+ )
+ }
+ None => {
+ println!("Adding SOCKS5-proxy: {}:{}", add.remote_ip, add.remote_port);
+ talpid_types::Socks5Remote::new((add.remote_ip, add.remote_port))
+ }
+ }),
},
AddCustomCommands::Shadowsocks {
- remote_ip,
- remote_port,
- password,
- cipher,
+ add,
name: _,
disabled: _,
} => {
+ let (password, cipher, remote_ip, remote_port) =
+ (add.password, add.cipher, add.remote_ip, add.remote_port);
println!(
"Adding Shadowsocks-proxy: {password} @ {remote_ip}:{remote_port} using {cipher}"
);
- daemon_types::AccessMethod::from(daemon_types::Shadowsocks::new(
+ daemon_types::AccessMethod::from(talpid_types::Shadowsocks::new(
(remote_ip, remote_port),
cipher,
password,
@@ -483,9 +433,8 @@ mod conversions {
/// Pretty printing of [`ApiAccessMethod`]s
mod pp {
- use mullvad_types::access_method::{
- AccessMethod, AccessMethodSetting, CustomAccessMethod, Socks5, SocksAuth,
- };
+ use crate::cmds::proxies::pp::CustomProxyFormatter;
+ use mullvad_types::access_method::{AccessMethod, AccessMethodSetting};
pub struct ApiAccessMethodFormatter<'a> {
api_access_method: &'a AccessMethodSetting,
@@ -517,8 +466,6 @@ mod pp {
impl<'a> std::fmt::Display for ApiAccessMethodFormatter<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- use crate::print_option;
-
let write_status = |f: &mut std::fmt::Formatter<'_>, enabled: bool| {
if enabled {
write!(f, " *")
@@ -535,55 +482,18 @@ mod pp {
}
Ok(())
}
- AccessMethod::Custom(method) => match &method {
- CustomAccessMethod::Shadowsocks(shadowsocks) => {
- write!(f, "{}", self.api_access_method.get_name())?;
- if self.settings.write_enabled {
- write_status(f, self.api_access_method.enabled())?;
- }
- writeln!(f)?;
- print_option!("Protocol", format!("Shadowsocks [{}]", shadowsocks.cipher));
- print_option!("Peer", shadowsocks.peer);
- print_option!("Password", shadowsocks.password);
- Ok(())
+ AccessMethod::Custom(method) => {
+ write!(f, "{}", self.api_access_method.get_name())?;
+ if self.settings.write_enabled {
+ write_status(f, self.api_access_method.enabled())?;
}
- CustomAccessMethod::Socks5(socks) => match socks {
- Socks5::Remote(remote) => {
- write!(f, "{}", self.api_access_method.get_name())?;
- if self.settings.write_enabled {
- write_status(f, self.api_access_method.enabled())?;
- }
- writeln!(f)?;
- print_option!("Protocol", "Socks5");
- print_option!("Peer", remote.peer);
- match &remote.authentication {
- Some(SocksAuth { username, password }) => {
- print_option!("Username", username);
- print_option!("Password", password);
- }
- None => (),
- }
- Ok(())
- }
- Socks5::Local(local) => {
- write!(f, "{}", self.api_access_method.get_name())?;
- if self.settings.write_enabled {
- write_status(f, self.api_access_method.enabled())?;
- }
- writeln!(f)?;
- print_option!("Protocol", "Socks5 (local)");
- print_option!(
- "Peer",
- format!(
- "{}/{}",
- local.remote_endpoint.address, local.remote_endpoint.protocol
- )
- );
- print_option!("Local port", local.local_port);
- Ok(())
- }
- },
- },
+ writeln!(f)?;
+ let formatter = CustomProxyFormatter {
+ custom_proxy: method,
+ };
+ write!(f, "{}", formatter)?;
+ Ok(())
+ }
}
}
}
diff --git a/mullvad-cli/src/cmds/bridge.rs b/mullvad-cli/src/cmds/bridge.rs
index 4abccb99e8..329921868b 100644
--- a/mullvad-cli/src/cmds/bridge.rs
+++ b/mullvad-cli/src/cmds/bridge.rs
@@ -1,17 +1,22 @@
-use anyhow::Result;
+use anyhow::{bail, Result};
use clap::Subcommand;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::{
relay_constraints::{
- BridgeConstraints, BridgeConstraintsFormatter, BridgeSettings, BridgeState, Constraint,
- LocationConstraint, Ownership, Provider, Providers,
+ BridgeConstraintsFormatter, BridgeState, BridgeType, Constraint, LocationConstraint,
+ Ownership, Provider, Providers,
},
relay_list::RelayEndpointData,
};
-use std::net::{IpAddr, SocketAddr};
-use talpid_types::net::openvpn::{self, SHADOWSOCKS_CIPHERS};
+use talpid_types::net::proxy::{CustomProxy, Shadowsocks, Socks5Local, Socks5Remote};
-use super::{relay::resolve_location_constraint, relay_constraints::LocationArgs};
+use crate::cmds::proxies::pp::CustomProxyFormatter;
+
+use super::{
+ proxies::{ProxyEditParams, ShadowsocksAdd, Socks5LocalAdd, Socks5RemoteAdd},
+ relay::resolve_location_constraint,
+ relay_constraints::LocationArgs,
+};
#[derive(Subcommand, Debug)]
pub enum Bridge {
@@ -73,73 +78,48 @@ pub enum SetCommands {
/// Configure a SOCKS5 proxy
#[clap(subcommand)]
- Custom(SetCustomCommands),
+ Custom(CustomCommands),
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum CustomCommands {
+ /// Create or update and enable the custom bridge configuration.
+ #[clap(subcommand)]
+ Set(AddCustomCommands),
+ /// Edit an existing custom bridge configuration.
+ Edit(ProxyEditParams),
+ /// Use an existing custom bridge configuration.
+ Use,
+ /// Stop using the custom bridge configuration.
+ Disable,
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum AddCustomCommands {
+ #[clap(subcommand)]
+ Socks5(AddSocks5Commands),
+ /// Configure bundled Shadowsocks proxy
+ Shadowsocks {
+ #[clap(flatten)]
+ add: ShadowsocksAdd,
+ },
}
#[derive(Subcommand, Debug, Clone)]
-pub enum SetCustomCommands {
+pub enum AddSocks5Commands {
/// 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"
- )
- )]
- #[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"
- )
- )]
- #[cfg_attr(
- target_os = "macos",
- clap(
- about = "Registers a local SOCKS5 proxy. The server must run as root to bypass \
- firewall restrictions"
- )
+ #[clap(
+ about = "Registers a local SOCKS5 proxy. Will allow all local programs to leak traffic *only* to the remote endpoint."
)]
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,
+ #[clap(flatten)]
+ add: Socks5LocalAdd,
},
/// 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,
-
- /// 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,
-
- /// Password for authentication
- #[arg(default_value = "mullvad")]
- password: String,
-
- /// Cipher to use
- #[arg(value_parser = SHADOWSOCKS_CIPHERS, default_value = "aes-256-gcm")]
- cipher: String,
+ #[clap(flatten)]
+ add: Socks5RemoteAdd,
},
}
@@ -188,133 +168,35 @@ impl Bridge {
};
Self::update_bridge_settings(&mut rpc, None, Some(providers), None).await
}
- SetCommands::Custom(subcmd) => Self::set_custom(subcmd).await,
+ SetCommands::Custom(subcmd) => Self::handle_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?;
- }
- 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?;
- }
- 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?;
- }
- }
-
- println!("Updated bridge settings");
- Ok(())
- }
-
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) => {
- Self::print_remote_proxy(&remote_proxy)
- }
- openvpn::ProxySettings::Shadowsocks(shadowsocks_proxy) => {
- Self::print_shadowsocks_proxy(&shadowsocks_proxy)
- }
- },
- BridgeSettings::Normal(ref constraints) => {
- println!(
- "Bridge constraints: {}",
- BridgeConstraintsFormatter {
- constraints,
- custom_lists: &settings.custom_lists
- }
- )
- }
- };
- Ok(())
- }
-
- fn print_local_proxy(proxy: &openvpn::LocalProxySettings) {
- println!("proxy: local");
- println!(" local port: {}", proxy.port);
- println!(" peer address: {}", proxy.peer);
- }
+ println!(
+ "Active bridge type: {}",
+ settings.bridge_settings.bridge_type
+ );
- fn print_remote_proxy(proxy: &openvpn::RemoteProxySettings) {
- println!("proxy: remote");
- println!(" server address: {}", proxy.address);
+ println!("Normal constraints");
+ println!(
+ "{:<4}{}",
+ "",
+ BridgeConstraintsFormatter {
+ constraints: &settings.bridge_settings.normal,
+ custom_lists: &settings.custom_lists
+ }
+ );
- if let Some(ref auth) = proxy.auth {
- println!(" auth username: {}", auth.username);
- println!(" auth password: {}", auth.password);
- } else {
- println!(" auth: none");
+ if let Some(ref custom_proxy) = settings.bridge_settings.custom {
+ println!("Custom proxy");
+ println!("{}", CustomProxyFormatter { custom_proxy });
}
- }
- fn print_shadowsocks_proxy(proxy: &openvpn::ShadowsocksProxySettings) {
- println!("proxy: Shadowsocks");
- println!(" peer address: {}", proxy.peer);
- println!(" password: {}", proxy.password);
- println!(" cipher: {}", proxy.cipher);
+ Ok(())
}
async fn list() -> Result<()> {
@@ -379,33 +261,99 @@ impl Bridge {
providers: Option<Constraint<Providers>>,
ownership: Option<Constraint<Ownership>>,
) -> Result<()> {
- 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
+ let mut settings = rpc.get_settings().await?.bridge_settings;
+ if let Some(new_location) = location {
+ settings.normal.location = new_location;
+ }
+ if let Some(new_providers) = providers {
+ settings.normal.providers = new_providers;
+ }
+ if let Some(new_ownership) = ownership {
+ settings.normal.ownership = new_ownership;
+ }
+
+ settings.bridge_type = BridgeType::Normal;
+
+ rpc.set_bridge_settings(settings).await?;
+
+ println!("Updated bridge settings");
+
+ Ok(())
+ }
+
+ pub async fn handle_custom(subcmd: CustomCommands) -> Result<()> {
+ match subcmd {
+ CustomCommands::Set(set_custom_commands) => {
+ Self::custom_bridge_set(set_custom_commands).await
}
- _ => BridgeConstraints {
- location: location
- .unwrap_or(Constraint::Any)
- .map(LocationConstraint::from),
- providers: providers.unwrap_or(Constraint::Any),
- ownership: ownership.unwrap_or(Constraint::Any),
- },
+ CustomCommands::Edit(edit) => Self::custom_bridge_edit(edit).await,
+ CustomCommands::Use => Self::custom_bridge_use().await,
+ CustomCommands::Disable => Self::custom_bridge_disable().await,
+ }
+ }
+
+ async fn custom_bridge_edit(edit: ProxyEditParams) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let mut settings = rpc.get_settings().await?;
+
+ let Some(ref mut custom_bridge) = settings.bridge_settings.custom else {
+ bail!("Can not edit as there is no currently saved custom bridge");
+ };
+
+ match custom_bridge {
+ CustomProxy::Shadowsocks(ss) => *ss = edit.merge_shadowsocks(ss),
+ CustomProxy::Socks5Local(local) => *local = edit.merge_socks_local(local),
+ CustomProxy::Socks5Remote(remote) => *remote = edit.merge_socks_remote(remote),
};
- rpc.set_bridge_settings(BridgeSettings::Normal(constraints))
- .await?;
+ rpc.set_bridge_settings(settings.bridge_settings)
+ .await
+ .map_err(anyhow::Error::from)
+ }
- println!("Updated bridge settings");
+ async fn custom_bridge_use() -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+
+ let mut settings = rpc.get_settings().await?;
+ if settings.bridge_settings.custom.is_none() {
+ bail!("Cannot enable custom bridge as there are no settings");
+ }
+ settings.bridge_settings.bridge_type = BridgeType::Custom;
+ rpc.set_bridge_settings(settings.bridge_settings).await?;
+
+ Ok(())
+ }
+ async fn custom_bridge_disable() -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let mut settings = rpc.get_settings().await?;
+
+ settings.bridge_settings.bridge_type = BridgeType::Normal;
+
+ rpc.set_bridge_settings(settings.bridge_settings).await?;
Ok(())
}
+
+ async fn custom_bridge_set(set_commands: AddCustomCommands) -> Result<()> {
+ let mut rpc = MullvadProxyClient::new().await?;
+ let mut settings = rpc.get_settings().await?;
+
+ settings.bridge_settings.custom = Some(match set_commands {
+ AddCustomCommands::Socks5(AddSocks5Commands::Local { add }) => {
+ CustomProxy::Socks5Local(Socks5Local::from(add))
+ }
+ AddCustomCommands::Socks5(AddSocks5Commands::Remote { add }) => {
+ CustomProxy::Socks5Remote(Socks5Remote::from(add))
+ }
+ AddCustomCommands::Shadowsocks { add } => {
+ CustomProxy::Shadowsocks(Shadowsocks::from(add))
+ }
+ });
+
+ settings.bridge_settings.bridge_type = BridgeType::Custom;
+
+ rpc.set_bridge_settings(settings.bridge_settings)
+ .await
+ .map_err(anyhow::Error::from)
+ }
}
diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs
index 1984f1493b..29e0508d80 100644
--- a/mullvad-cli/src/cmds/mod.rs
+++ b/mullvad-cli/src/cmds/mod.rs
@@ -13,6 +13,7 @@ pub mod import_settings;
pub mod lan;
pub mod lockdown;
pub mod obfuscation;
+pub mod proxies;
pub mod relay;
pub mod relay_constraints;
pub mod reset;
diff --git a/mullvad-cli/src/cmds/proxies.rs b/mullvad-cli/src/cmds/proxies.rs
new file mode 100644
index 0000000000..6e32836a25
--- /dev/null
+++ b/mullvad-cli/src/cmds/proxies.rs
@@ -0,0 +1,214 @@
+use std::net::{IpAddr, SocketAddr};
+use talpid_types::net::{
+ proxy::{Shadowsocks, Socks5Local, Socks5Remote, SocksAuth, SHADOWSOCKS_CIPHERS},
+ Endpoint, TransportProtocol,
+};
+
+use clap::Args;
+
+#[derive(Args, Debug, Clone)]
+pub struct Socks5LocalAdd {
+ /// The port that the server on localhost is listening on
+ pub local_port: u16,
+ /// The IP of the remote peer
+ pub remote_ip: IpAddr,
+ /// The port of the remote peer
+ pub remote_port: u16,
+ /// The Mullvad App can not know which transport protocol that the
+ /// remote peer accepts, but it needs to know this in order to correctly
+ /// exempt the connection traffic in the firewall.
+ ///
+ /// By default, the transport protocol is assumed to be `TCP`, but it
+ /// can optionally be set to `UDP` as well.
+ #[arg(long, default_value_t = TransportProtocol::Tcp)]
+ pub transport_protocol: TransportProtocol,
+}
+
+impl From<Socks5LocalAdd> for Socks5Local {
+ fn from(add: Socks5LocalAdd) -> Self {
+ Self {
+ remote_endpoint: Endpoint {
+ address: SocketAddr::new(add.remote_ip, add.remote_port),
+ protocol: add.transport_protocol,
+ },
+ local_port: add.local_port,
+ }
+ }
+}
+
+// We do not support setting the protocol as anything other than tcp for remote socks5 servers
+#[derive(Args, Debug, Clone)]
+pub struct Socks5RemoteAdd {
+ /// The IP of the remote proxy server
+ pub remote_ip: IpAddr,
+ /// The port of the remote proxy server
+ pub remote_port: u16,
+
+ #[clap(flatten)]
+ pub authentication: Option<SocksAuthentication>,
+}
+
+impl From<Socks5RemoteAdd> for Socks5Remote {
+ fn from(add: Socks5RemoteAdd) -> Self {
+ Self {
+ endpoint: SocketAddr::new(add.remote_ip, add.remote_port),
+ auth: add.authentication.map(|auth| SocksAuth {
+ username: auth.username,
+ password: auth.password,
+ }),
+ }
+ }
+}
+
+#[derive(Args, Debug, Clone)]
+pub struct ShadowsocksAdd {
+ /// The IP of the remote Shadowsocks-proxy
+ pub remote_ip: IpAddr,
+ /// Port on which the remote Shadowsocks-proxy listens for traffic
+ pub remote_port: u16,
+ /// Password for authentication
+ pub password: String,
+ /// Cipher to use
+ #[arg(long, value_parser = SHADOWSOCKS_CIPHERS)]
+ pub cipher: String,
+}
+
+impl From<ShadowsocksAdd> for Shadowsocks {
+ fn from(add: ShadowsocksAdd) -> Self {
+ Self {
+ endpoint: SocketAddr::new(add.remote_ip, add.remote_port),
+ password: add.password,
+ cipher: add.cipher,
+ }
+ }
+}
+
+#[derive(Args, Debug, Clone)]
+#[group(requires_all = ["username", "password"])] // https://github.com/clap-rs/clap/issues/5092
+pub struct SocksAuthentication {
+ /// Username for authentication against a remote SOCKS5 proxy
+ #[arg(short, long, required = false)]
+ pub username: String,
+ /// Password for authentication against a remote SOCKS5 proxy
+ #[arg(short, long, required = false)]
+ pub password: String,
+}
+
+#[derive(Args, Debug, Clone)]
+pub struct ProxyEditParams {
+ /// Username for authentication [Socks5 (Remote proxy)]
+ #[arg(long)]
+ pub username: Option<String>,
+ /// Password for authentication [Socks5 (Remote proxy), Shadowsocks]
+ #[arg(long)]
+ pub password: Option<String>,
+ /// Cipher to use [Shadowsocks]
+ #[arg(value_parser = SHADOWSOCKS_CIPHERS, long)]
+ pub cipher: Option<String>,
+ /// The IP of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks]
+ #[arg(long)]
+ pub ip: Option<IpAddr>,
+ /// The port of the remote proxy server [Socks5 (Local & Remote proxy), Shadowsocks]
+ #[arg(long)]
+ pub port: Option<u16>,
+ /// The port that the server on localhost is listening on [Socks5 (Local proxy)]
+ #[arg(long)]
+ pub local_port: Option<u16>,
+ /// The transport protocol used by the remote proxy [Socks5 (Local proxy)]
+ #[arg(long)]
+ pub transport_protocol: Option<TransportProtocol>,
+}
+
+impl ProxyEditParams {
+ pub fn merge_socks_local(self, local: &Socks5Local) -> Socks5Local {
+ let remote_ip = self.ip.unwrap_or(local.remote_endpoint.address.ip());
+ let remote_port = self.port.unwrap_or(local.remote_endpoint.address.port());
+ let local_port = self.local_port.unwrap_or(local.local_port);
+ let remote_peer_transport_protocol = self
+ .transport_protocol
+ .unwrap_or(local.remote_endpoint.protocol);
+ Socks5Local::new_with_transport_protocol(
+ (remote_ip, remote_port),
+ local_port,
+ remote_peer_transport_protocol,
+ )
+ }
+
+ pub fn merge_socks_remote(self, remote: &Socks5Remote) -> Socks5Remote {
+ let ip = self.ip.unwrap_or(remote.endpoint.ip());
+ let port = self.port.unwrap_or(remote.endpoint.port());
+ match &remote.auth {
+ None => match (self.username, self.password) {
+ (Some(username), Some(password)) => {
+ let auth = SocksAuth { username, password };
+ Socks5Remote::new_with_authentication((ip, port), auth)
+ }
+ (None, None) => Socks5Remote::new((ip, port)),
+ _ => {
+ println!("Remote SOCKS5 proxy does not have a username and password set already, so you must provide both or neither when you edit.");
+ Socks5Remote::new((ip, port))
+ }
+ },
+ Some(SocksAuth { username, password }) => {
+ let username = self.username.unwrap_or(username.to_owned());
+ let password = self.password.unwrap_or(password.to_owned());
+ let auth = SocksAuth { username, password };
+ Socks5Remote::new_with_authentication((ip, port), auth)
+ }
+ }
+ }
+
+ pub fn merge_shadowsocks(self, shadowsocks: &Shadowsocks) -> Shadowsocks {
+ let ip = self.ip.unwrap_or(shadowsocks.endpoint.ip());
+ let port = self.port.unwrap_or(shadowsocks.endpoint.port());
+ let password = self.password.unwrap_or(shadowsocks.password.to_owned());
+ let cipher = self.cipher.unwrap_or(shadowsocks.cipher.to_owned());
+ Shadowsocks::new((ip, port), cipher, password)
+ }
+}
+
+pub mod pp {
+ use crate::print_option;
+ use talpid_types::net::proxy::{CustomProxy, SocksAuth};
+
+ pub struct CustomProxyFormatter<'a> {
+ pub custom_proxy: &'a CustomProxy,
+ }
+
+ impl<'a> std::fmt::Display for CustomProxyFormatter<'a> {
+ fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self.custom_proxy {
+ CustomProxy::Shadowsocks(shadowsocks) => {
+ print_option!("Protocol", format!("Shadowsocks [{}]", shadowsocks.cipher));
+ print_option!("Peer", shadowsocks.endpoint);
+ print_option!("Password", shadowsocks.password);
+ Ok(())
+ }
+ CustomProxy::Socks5Remote(remote) => {
+ print_option!("Protocol", "Socks5");
+ print_option!("Peer", remote.endpoint);
+ match &remote.auth {
+ Some(SocksAuth { username, password }) => {
+ print_option!("Username", username);
+ print_option!("Password", password);
+ }
+ None => (),
+ }
+ Ok(())
+ }
+ CustomProxy::Socks5Local(local) => {
+ print_option!("Protocol", "Socks5 (local)");
+ print_option!(
+ "Peer",
+ format!(
+ "{}/{}",
+ local.remote_endpoint.address, local.remote_endpoint.protocol
+ )
+ );
+ print_option!("Local port", local.local_port);
+ Ok(())
+ }
+ }
+ }
+ }
+}