diff --git a/mullvad-api/src/https_client_with_sni.rs b/mullvad-api/src/https_client_with_sni.rs index e97a180423b4..3c93b81bf6ec 100644 --- a/mullvad-api/src/https_client_with_sni.rs +++ b/mullvad-api/src/https_client_with_sni.rs @@ -125,11 +125,21 @@ impl InnerConnectionMode { InnerConnectionMode::Socks5(socks) => { let first_hop = socks.peer; let make_proxy_stream = |tcp_stream| async { - tokio_socks::tcp::Socks5Stream::connect_with_socket(tcp_stream, addr) - .await - .map_err(|error| { - io::Error::new(io::ErrorKind::Other, format!("SOCKS error: {error}")) - }) + match socks.authentication { + SocksAuth::None => { + tokio_socks::tcp::Socks5Stream::connect_with_socket(tcp_stream, addr) + .await + } + SocksAuth::Password { username, password } => { + tokio_socks::tcp::Socks5Stream::connect_with_password_and_socket( + tcp_stream, addr, &username, &password, + ) + .await + } + } + .map_err(|error| { + io::Error::new(io::ErrorKind::Other, format!("SOCKS error: {error}")) + }) }; Self::connect_proxied( first_hop, @@ -201,6 +211,13 @@ impl From for ServerConfig { #[derive(Clone)] struct SocksConfig { peer: SocketAddr, + authentication: SocksAuth, +} + +#[derive(Clone)] +pub enum SocksAuth { + None, + Password { username: String, password: String }, } #[derive(err_derive::Error, Debug)] @@ -213,6 +230,8 @@ impl TryFrom for InnerConnectionMode { type Error = ProxyConfigError; fn try_from(config: ApiConnectionMode) -> Result { + use mullvad_types::access_method; + use std::net::Ipv4Addr; Ok(match config { ApiConnectionMode::Direct => InnerConnectionMode::Direct, ApiConnectionMode::Proxied(proxy_settings) => match proxy_settings { @@ -228,13 +247,23 @@ impl TryFrom for InnerConnectionMode { }) } ProxyConfig::Socks(config) => match config { - mullvad_types::access_method::Socks5::Local(config) => { + access_method::Socks5::Local(config) => { InnerConnectionMode::Socks5(SocksConfig { - peer: SocketAddr::new("127.0.0.1".parse().unwrap(), config.port), + peer: SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), config.port), + authentication: SocksAuth::None, }) } - mullvad_types::access_method::Socks5::Remote(config) => { - InnerConnectionMode::Socks5(SocksConfig { peer: config.peer }) + access_method::Socks5::Remote(config) => { + let authentication = match config.authentication { + Some(access_method::SocksAuth { username, password }) => { + SocksAuth::Password { username, password } + } + None => SocksAuth::None, + }; + InnerConnectionMode::Socks5(SocksConfig { + peer: config.peer, + authentication, + }) } }, }, diff --git a/mullvad-cli/src/cmds/api_access.rs b/mullvad-cli/src/cmds/api_access.rs index a3b17aca0bba..c1b7552313dc 100644 --- a/mullvad-cli/src/cmds/api_access.rs +++ b/mullvad-cli/src/cmds/api_access.rs @@ -241,6 +241,8 @@ pub enum AddSocks5Commands { remote_ip: IpAddr, /// The port of the remote proxy server remote_port: u16, + #[clap(flatten)] + authentication: Option, /// Disable the use of this custom access method. It has to be manually /// enabled at a later stage to be used when accessing the Mullvad API. #[arg(default_value_t = false, short, long)] @@ -263,6 +265,16 @@ pub enum AddSocks5Commands { }, } +#[derive(Args, Debug, Clone)] +pub struct SocksAuthentication { + /// Username for authentication against a remote SOCKS5 proxy + #[arg(short, long)] + username: String, + /// Password for authentication against a remote SOCKS5 proxy + #[arg(short, long)] + password: String, +} + impl AddCustomCommands { fn name(&self) -> &str { match self { @@ -344,7 +356,7 @@ mod conversions { use anyhow::{anyhow, Error}; use mullvad_types::access_method as daemon_types; - use super::{AddCustomCommands, AddSocks5Commands}; + use super::{AddCustomCommands, AddSocks5Commands, SocksAuthentication}; impl TryFrom for daemon_types::AccessMethod { type Error = Error; @@ -373,18 +385,31 @@ mod conversions { AddSocks5Commands::Remote { remote_ip, remote_port, + authentication, name: _, disabled: _, } => { - println!("Adding SOCKS5-proxy: {remote_ip}:{remote_port}"); - let socks_proxy = daemon_types::Socks5::Remote( - daemon_types::Socks5Remote::from_args( - remote_ip.to_string(), - remote_port, - ) - .ok_or(anyhow!("Could not create a remote Socks5 api proxy"))?, - ); - daemon_types::AccessMethod::from(socks_proxy) + match authentication { + Some(SocksAuthentication { username, password }) => { + println!("Adding SOCKS5-proxy: {username}:{password}@{remote_ip}:{remote_port}"); + daemon_types::Socks5Remote::from_args_with_password( + remote_ip.to_string(), + remote_port, + username, + password + ) + } + None => { + println!("Adding SOCKS5-proxy: {remote_ip}:{remote_port}"); + daemon_types::Socks5Remote::from_args( + remote_ip.to_string(), + remote_port, + ) + } + } + .map(daemon_types::Socks5::Remote) + .map(daemon_types::AccessMethod::from) + .ok_or(anyhow!("Could not create a remote Socks5 api proxy"))? } }, AddCustomCommands::Shadowsocks { @@ -415,7 +440,7 @@ mod conversions { /// Pretty printing of [`ApiAccessMethod`]s mod pp { use mullvad_types::access_method::{ - AccessMethod, AccessMethodSetting, CustomAccessMethod, Socks5, + AccessMethod, AccessMethodSetting, CustomAccessMethod, Socks5, SocksAuth, }; pub struct ApiAccessMethodFormatter<'a> { @@ -462,6 +487,13 @@ mod pp { 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) => { diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 3677f83921ff..35c6c322ee1e 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -357,9 +357,14 @@ message AccessMethod { uint32 port = 5; uint32 local_port = 6; } + message SocksAuth { + string username = 1; + string password = 2; + } message Socks5Remote { - string ip = 2; - uint32 port = 3; + string ip = 1; + uint32 port = 2; + SocksAuth authentication = 3; } message Shadowsocks { string ip = 2; diff --git a/mullvad-management-interface/src/types/conversions/access_method.rs b/mullvad-management-interface/src/types/conversions/access_method.rs index 0f39d34e9eef..8907c4da2947 100644 --- a/mullvad-management-interface/src/types/conversions/access_method.rs +++ b/mullvad-management-interface/src/types/conversions/access_method.rs @@ -45,7 +45,7 @@ mod data { use crate::types::{proto, FromProtobufTypeError}; use mullvad_types::access_method::{ AccessMethod, AccessMethodSetting, BuiltInAccessMethod, CustomAccessMethod, Id, - Shadowsocks, Socks5, Socks5Local, Socks5Remote, + Shadowsocks, Socks5, Socks5Local, Socks5Remote, SocksAuth, }; impl TryFrom for AccessMethodSetting { @@ -154,13 +154,24 @@ mod data { type Error = FromProtobufTypeError; fn try_from(value: proto::access_method::Socks5Remote) -> Result { - Socks5Remote::from_args(value.ip, value.port as u16) - .ok_or({ - FromProtobufTypeError::InvalidArgument( - "Could not parse Socks5 (remote) message from protobuf", - ) - }) - .map(AccessMethod::from) + let proto::access_method::Socks5Remote { + ip, + port, + authentication, + } = value; + let port = port as u16; + match authentication.map(SocksAuth::from) { + Some(SocksAuth { username, password }) => { + Socks5Remote::from_args_with_password(ip, port, username, password) + } + None => Socks5Remote::from_args(ip, port), + } + .ok_or({ + FromProtobufTypeError::InvalidArgument( + "Could not parse Socks5 (remote) message from protobuf", + ) + }) + .map(AccessMethod::from) } } @@ -214,14 +225,16 @@ mod data { }, ) } - CustomAccessMethod::Socks5(Socks5::Remote(Socks5Remote { peer })) => { - proto::access_method::AccessMethod::Socks5remote( - proto::access_method::Socks5Remote { - ip: peer.ip().to_string(), - port: peer.port() as u32, - }, - ) - } + CustomAccessMethod::Socks5(Socks5::Remote(Socks5Remote { + peer, + authentication, + })) => proto::access_method::AccessMethod::Socks5remote( + proto::access_method::Socks5Remote { + ip: peer.ip().to_string(), + port: peer.port() as u32, + authentication: authentication.map(proto::access_method::SocksAuth::from), + }, + ), }; proto::AccessMethod { @@ -248,6 +261,24 @@ mod data { } } + impl From for proto::access_method::SocksAuth { + fn from(value: SocksAuth) -> Self { + proto::access_method::SocksAuth { + username: value.username, + password: value.password, + } + } + } + + impl From for SocksAuth { + fn from(value: proto::access_method::SocksAuth) -> Self { + Self { + username: value.username, + password: value.password, + } + } + } + impl TryFrom<&proto::AccessMethodSetting> for AccessMethodSetting { type Error = FromProtobufTypeError; diff --git a/mullvad-types/src/access_method.rs b/mullvad-types/src/access_method.rs index a300072c82f8..30c1f25192ae 100644 --- a/mullvad-types/src/access_method.rs +++ b/mullvad-types/src/access_method.rs @@ -209,6 +209,13 @@ pub struct Socks5Local { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct Socks5Remote { pub peer: SocketAddr, + pub authentication: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct SocksAuth { + pub username: String, + pub password: String, } impl AccessMethod { @@ -262,7 +269,10 @@ impl Socks5Local { impl Socks5Remote { pub fn new(peer: SocketAddr) -> Self { - Self { peer } + Self { + peer, + authentication: None, + } } /// Like [new()], but tries to parse `ip` and `port` into a [`std::net::SocketAddr`] for you. @@ -272,6 +282,18 @@ impl Socks5Remote { let peer = SocketAddr::new(peer_ip, port); Some(Self::new(peer)) } + + /// Like [from_args()], but with authentication. + pub fn from_args_with_password( + ip: String, + port: u16, + username: String, + password: String, + ) -> Option { + let mut socks = Self::from_args(ip, port)?; + socks.authentication = Some(SocksAuth { username, password }); + Some(socks) + } } impl From for AccessMethod {