diff options
| author | Jonathan <jonathan@mullvad.net> | 2024-01-03 14:39:12 +0100 |
|---|---|---|
| committer | Jonathan <jonathan@mullvad.net> | 2024-01-03 14:39:12 +0100 |
| commit | 711d4e439866ab12e03d33d5efae3c2355c0c229 (patch) | |
| tree | 80d3a23c1a96bd3d80e05ac66b530e39c252d48a | |
| parent | c510df96772b1e4ab7998e739ced42806c78e931 (diff) | |
| parent | 4fdc34acbba60d5092e45ce3e513d30ec996c317 (diff) | |
| download | mullvadvpn-711d4e439866ab12e03d33d5efae3c2355c0c229.tar.xz mullvadvpn-711d4e439866ab12e03d33d5efae3c2355c0c229.zip | |
Merge branch 'implement-custom-openvpn-socks5-bridge-client-in-daemon-des-430'
53 files changed, 2512 insertions, 1272 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c4e06fa1..08e03e88a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ Line wrap the file at 100 chars. Th ### Changed - Remove `--location` flag from `mullvad status` CLI. Location and IP will now always be printed (if available). `mullvad status listen` no longer prints location info. +- Custom socks5 bridges get a new CLI interface and now work without split tunneling or root. + In the CLI these can be found under `mullvad bridge set custom`. #### Android - Migrate to Compose Navigation which also improves screen transition animations. diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index ad7b60dec2..d5d29ad709 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -15,6 +15,7 @@ import { AuthFailedError, BridgeSettings, BridgeState, + BridgeType, ConnectionConfig, Constraint, CustomListError, @@ -51,7 +52,6 @@ import { ObfuscationSettings, ObfuscationType, Ownership, - ProxySettings, ProxyType, RelayEndpointType, RelayLocation, @@ -341,13 +341,52 @@ export class DaemonRpc { public async setBridgeSettings(bridgeSettings: BridgeSettings): Promise<void> { const grpcBridgeSettings = new grpcTypes.BridgeSettings(); - if ('normal' in bridgeSettings) { - const normalSettings = convertToNormalBridgeSettings(bridgeSettings.normal); - grpcBridgeSettings.setNormal(normalSettings); + if (bridgeSettings.type === 'custom') { + throw configNotSupported; } - if ('custom' in bridgeSettings) { - throw configNotSupported; + grpcBridgeSettings.setBridgeType(grpcTypes.BridgeSettings.BridgeType.NORMAL); + + const normalSettings = convertToNormalBridgeSettings(bridgeSettings.normal); + grpcBridgeSettings.setNormal(normalSettings); + + if (bridgeSettings.custom) { + const customProxy = new grpcTypes.CustomProxy(); + + const customSettings = bridgeSettings.custom; + + if ('local' in customSettings) { + const local = customSettings.local; + const socks5Local = new grpcTypes.Socks5Local(); + socks5Local.setLocalPort(local.localPort); + socks5Local.setRemoteIp(local.remoteIp); + socks5Local.setRemotePort(local.remotePort); + customProxy.setSocks5local(socks5Local); + } + if ('remote' in customSettings) { + const remote = customSettings.remote; + const socks5Remote = new grpcTypes.Socks5Remote(); + if (remote.auth) { + const auth = new grpcTypes.SocksAuth(); + auth.setUsername(remote.auth.username); + auth.setPassword(remote.auth.password); + socks5Remote.setAuth(auth); + } + socks5Remote.setIp(remote.ip); + socks5Remote.setPort(remote.port); + customProxy.setSocks5remote(socks5Remote); + } + if ('shadowsocks' in customSettings) { + const shadowsocks = customSettings.shadowsocks; + const shadowOut = new grpcTypes.Shadowsocks(); + shadowOut.setCipher(shadowsocks.cipher); + shadowOut.setIp(shadowsocks.ip); + shadowOut.setPort(shadowsocks.port); + shadowOut.setPassword(shadowsocks.password); + customProxy.setShadowsocks(shadowOut); + } + + grpcBridgeSettings.setCustom(customProxy); } await this.call<grpcTypes.BridgeSettings, Empty>( @@ -1140,49 +1179,64 @@ function convertFromRelaySettings( function convertFromBridgeSettings(bridgeSettings: grpcTypes.BridgeSettings): BridgeSettings { const bridgeSettingsObject = bridgeSettings.toObject(); + + const detailsMap: Record<grpcTypes.BridgeSettings.BridgeType, BridgeType> = { + [grpcTypes.BridgeSettings.BridgeType.NORMAL]: 'normal', + [grpcTypes.BridgeSettings.BridgeType.CUSTOM]: 'custom', + }; + const type = detailsMap[bridgeSettingsObject.bridgeType]; + const normalSettings = bridgeSettingsObject.normal; - if (normalSettings) { - const locationConstraint = convertFromLocationConstraint( - bridgeSettings.getNormal()?.getLocation(), - ); - const location = wrapConstraint(locationConstraint); - const providers = normalSettings.providersList; - const ownership = convertFromOwnership(normalSettings.ownership); - return { - normal: { - location, - providers, - ownership, - }, - }; - } + const locationConstraint = convertFromLocationConstraint( + bridgeSettings.getNormal()?.getLocation(), + ); + const location = wrapConstraint(locationConstraint); + const providers = normalSettings!.providersList; + const ownership = convertFromOwnership(normalSettings!.ownership); - const customSettings = (settings: ProxySettings): BridgeSettings => { - return { custom: settings }; + const normal = { + location, + providers, + ownership, }; - const localSettings = bridgeSettingsObject.local; - if (localSettings) { - return customSettings({ - port: localSettings.port, - peer: localSettings.peer, - }); - } + let custom = undefined; - const remoteSettings = bridgeSettingsObject.remote; - if (remoteSettings) { - return customSettings({ - address: remoteSettings.address, - auth: remoteSettings.auth && { ...remoteSettings.auth }, - }); + if (bridgeSettingsObject.custom) { + const localSettings = bridgeSettingsObject.custom.socks5local; + if (localSettings) { + custom = { + local: { + localPort: localSettings.localPort, + remoteIp: localSettings.remoteIp, + remotePort: localSettings.remotePort, + }, + }; + } + const remoteSettings = bridgeSettingsObject.custom.socks5remote; + if (remoteSettings) { + custom = { + remote: { + ip: remoteSettings.ip, + port: remoteSettings.port, + auth: remoteSettings.auth && { ...remoteSettings.auth }, + }, + }; + } + const shadowsocksSettings = bridgeSettingsObject.custom.shadowsocks; + if (shadowsocksSettings) { + custom = { + shadowsocks: { + ip: shadowsocksSettings.ip, + port: shadowsocksSettings.port, + password: shadowsocksSettings.password, + cipher: shadowsocksSettings.cipher, + }, + }; + } } - const shadowsocksSettings = bridgeSettingsObject.shadowsocks!; - return customSettings({ - peer: shadowsocksSettings.peer!, - password: shadowsocksSettings.password!, - cipher: shadowsocksSettings.cipher!, - }); + return { type, normal, custom }; } function convertFromConnectionConfig( diff --git a/gui/src/main/default-settings.ts b/gui/src/main/default-settings.ts index 55f420b659..fea88e4c27 100644 --- a/gui/src/main/default-settings.ts +++ b/gui/src/main/default-settings.ts @@ -29,11 +29,13 @@ export function getDefaultSettings(): ISettings { }, }, bridgeSettings: { + type: 'normal', normal: { location: 'any', providers: [], ownership: Ownership.any, }, + custom: undefined, }, bridgeState: 'auto', tunnelOptions: { diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index b5eacf17b3..3806c12413 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -624,17 +624,13 @@ export default class AppRenderer { private setBridgeSettings(bridgeSettings: BridgeSettings) { const actions = this.reduxActions; - if ('normal' in bridgeSettings) { - actions.settings.updateBridgeSettings({ - normal: { - location: liftConstraint(bridgeSettings.normal.location), - }, - }); - } else if ('custom' in bridgeSettings) { - actions.settings.updateBridgeSettings({ - custom: bridgeSettings.custom, - }); - } + actions.settings.updateBridgeSettings({ + type: bridgeSettings.type, + normal: { + location: liftConstraint(bridgeSettings.normal.location), + }, + custom: bridgeSettings.custom, + }); } private onDaemonConnected() { diff --git a/gui/src/renderer/components/select-location/select-location-hooks.ts b/gui/src/renderer/components/select-location/select-location-hooks.ts index 48d81e594c..b795f4a74f 100644 --- a/gui/src/renderer/components/select-location/select-location-hooks.ts +++ b/gui/src/renderer/components/select-location/select-location-hooks.ts @@ -11,6 +11,7 @@ import log from '../../../shared/logging'; import { useAppContext } from '../../context'; import { useRelaySettingsModifier } from '../../lib/constraint-updater'; import { useHistory } from '../../lib/history'; +import { useSelector } from '../../redux/store'; import { LocationType, SpecialBridgeLocationType } from './select-location-types'; import { useSelectLocationContext } from './SelectLocationContainer'; @@ -88,6 +89,7 @@ function useOnSelectLocation() { export function useOnSelectBridgeLocation() { const { updateBridgeSettings } = useAppContext(); const { setLocationType } = useSelectLocationContext(); + const bridgeSettings = useSelector((state) => state.settings.bridgeSettings); const setLocation = useCallback(async (bridgeUpdate: BridgeSettings) => { if (bridgeUpdate) { @@ -101,10 +103,14 @@ export function useOnSelectBridgeLocation() { } }, []); - const onSelectRelay = useCallback((location: RelayLocation) => { - const bridgeUpdate = new BridgeSettingsBuilder().location.fromRaw(location).build(); - return setLocation(bridgeUpdate); - }, []); + const onSelectRelay = useCallback( + (location: RelayLocation) => { + const bridgeUpdate = new BridgeSettingsBuilder().location.fromRaw(location).build(); + bridgeUpdate.custom = bridgeSettings.custom; + return setLocation(bridgeUpdate); + }, + [bridgeSettings], + ); const onSelectSpecial = useCallback((location: SpecialBridgeLocationType) => { switch (location) { diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts index 49f508a883..8c3925762e 100644 --- a/gui/src/renderer/lib/utilityHooks.ts +++ b/gui/src/renderer/lib/utilityHooks.ts @@ -67,5 +67,5 @@ export function useNormalRelaySettings() { export function useNormalBridgeSettings() { const bridgeSettings = useSelector((state) => state.settings.bridgeSettings); - return 'normal' in bridgeSettings ? bridgeSettings.normal : undefined; + return bridgeSettings.normal; } diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index b400799095..3d03093e0a 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -1,6 +1,7 @@ import { IWindowsApplication } from '../../../shared/application-types'; import { BridgeState, + BridgeType, CustomLists, IDnsOptions, IpVersion, @@ -51,13 +52,11 @@ export type RelaySettingsRedux = }; }; -export type BridgeSettingsRedux = - | { - normal: NormalBridgeSettingsRedux; - } - | { - custom: ProxySettings; - }; +export type BridgeSettingsRedux = { + type: BridgeType; + normal: NormalBridgeSettingsRedux; + custom?: ProxySettings; +}; export interface IRelayLocationRelayRedux { hostname: string; @@ -140,9 +139,11 @@ const initialState: ISettingsReduxState = { allowLan: false, enableIpv6: true, bridgeSettings: { + type: 'normal', normal: { location: 'any', }, + custom: undefined, }, bridgeState: 'auto', blockWhenDisconnected: false, diff --git a/gui/src/shared/bridge-settings-builder.ts b/gui/src/shared/bridge-settings-builder.ts index 858bea055d..2ee5469707 100644 --- a/gui/src/shared/bridge-settings-builder.ts +++ b/gui/src/shared/bridge-settings-builder.ts @@ -7,11 +7,13 @@ export default class BridgeSettingsBuilder { public build(): BridgeSettings { if (this.payload.location) { return { + type: 'normal', normal: { location: this.payload.location, providers: this.payload.providers ?? [], ownership: this.payload.ownership ?? Ownership.any, }, + custom: undefined, }; } else { throw new Error('Unsupported configuration'); diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index e83f9e5afc..48a4110e13 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -345,15 +345,20 @@ export interface IDnsOptions { }; } -export type ProxySettings = ILocalProxySettings | IRemoteProxySettings | IShadowsocksProxySettings; +export type ProxySettings = + | { local: ILocalProxySettings } + | { remote: IRemoteProxySettings } + | { shadowsocks: IShadowsocksProxySettings }; export interface ILocalProxySettings { - port: number; - peer: string; + localPort: number; + remoteIp: string; + remotePort: number; } export interface IRemoteProxySettings { - address: string; + ip: string; + port: number; auth?: IRemoteProxyAuth; } @@ -363,7 +368,8 @@ export interface IRemoteProxyAuth { } export interface IShadowsocksProxySettings { - peer: string; + ip: string; + port: number; password: string; cipher: string; } @@ -451,7 +457,13 @@ export interface IBridgeConstraints { ownership: Ownership; } -export type BridgeSettings = { normal: IBridgeConstraints } | { custom: ProxySettings }; +export type BridgeType = 'normal' | 'custom'; + +export interface BridgeSettings { + type: BridgeType; + normal: IBridgeConstraints; + custom?: ProxySettings; +} export interface ISocketAddress { host: string; diff --git a/mullvad-api/src/https_client_with_sni.rs b/mullvad-api/src/https_client_with_sni.rs index 7d559faa7a..3a9bb8d75f 100644 --- a/mullvad-api/src/https_client_with_sni.rs +++ b/mullvad-api/src/https_client_with_sni.rs @@ -236,15 +236,15 @@ impl TryFrom<ApiConnectionMode> for InnerConnectionMode { type Error = ProxyConfigError; fn try_from(config: ApiConnectionMode) -> Result<Self, Self::Error> { - use mullvad_types::access_method; use std::net::Ipv4Addr; + use talpid_types::net::proxy; Ok(match config { ApiConnectionMode::Direct => InnerConnectionMode::Direct, ApiConnectionMode::Proxied(proxy_settings) => match proxy_settings { ProxyConfig::Shadowsocks(config) => { InnerConnectionMode::Shadowsocks(ShadowsocksConfig { params: ParsedShadowsocksConfig { - peer: config.peer, + peer: config.endpoint, password: config.password, cipher: CipherKind::from_str(&config.cipher) .map_err(|_| ProxyConfigError::InvalidCipher(config.cipher))?, @@ -252,29 +252,22 @@ impl TryFrom<ApiConnectionMode> for InnerConnectionMode { proxy_context: SsContext::new_shared(ServerType::Local), }) } - ProxyConfig::Socks(config) => match config { - access_method::Socks5::Local(config) => { - InnerConnectionMode::Socks5(SocksConfig { - peer: SocketAddr::new( - IpAddr::from(Ipv4Addr::LOCALHOST), - config.local_port, - ), - authentication: SocksAuth::None, - }) - } - 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, - }) - } - }, + ProxyConfig::Socks5Local(config) => InnerConnectionMode::Socks5(SocksConfig { + peer: SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), config.local_port), + authentication: SocksAuth::None, + }), + ProxyConfig::Socks5Remote(config) => { + let authentication = match config.auth { + Some(proxy::SocksAuth { username, password }) => { + SocksAuth::Password { username, password } + } + None => SocksAuth::None, + }; + InnerConnectionMode::Socks5(SocksConfig { + peer: config.endpoint, + authentication, + }) + } }, }) } diff --git a/mullvad-api/src/proxy.rs b/mullvad-api/src/proxy.rs index 783cc8a157..caf2068abb 100644 --- a/mullvad-api/src/proxy.rs +++ b/mullvad-api/src/proxy.rs @@ -1,6 +1,5 @@ use futures::Stream; use hyper::client::connect::Connected; -use mullvad_types::access_method; use serde::{Deserialize, Serialize}; use std::{ fmt, io, @@ -8,6 +7,7 @@ use std::{ pin::Pin, task::{self, Poll}, }; +use talpid_types::net::proxy; use talpid_types::{ net::{AllowedClients, Endpoint, TransportProtocol}, ErrorExt, @@ -38,8 +38,9 @@ impl fmt::Display for ApiConnectionMode { #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum ProxyConfig { - Shadowsocks(access_method::Shadowsocks), - Socks(access_method::Socks5), + Shadowsocks(proxy::Shadowsocks), + Socks5Local(proxy::Socks5Local), + Socks5Remote(proxy::Socks5Remote), } impl ProxyConfig { @@ -47,14 +48,12 @@ impl ProxyConfig { fn get_endpoint(&self) -> Endpoint { match self { ProxyConfig::Shadowsocks(shadowsocks) => { - Endpoint::from_socket_address(shadowsocks.peer, TransportProtocol::Tcp) + Endpoint::from_socket_address(shadowsocks.endpoint, TransportProtocol::Tcp) + } + ProxyConfig::Socks5Local(local) => local.remote_endpoint, + ProxyConfig::Socks5Remote(remote) => { + Endpoint::from_socket_address(remote.endpoint, TransportProtocol::Tcp) } - ProxyConfig::Socks(socks) => match socks { - access_method::Socks5::Local(local) => local.remote_endpoint, - access_method::Socks5::Remote(remote) => { - Endpoint::from_socket_address(remote.peer, TransportProtocol::Tcp) - } - }, } } } @@ -64,12 +63,10 @@ impl fmt::Display for ProxyConfig { let endpoint = self.get_endpoint(); match self { ProxyConfig::Shadowsocks(_) => write!(f, "Shadowsocks {}", endpoint), - ProxyConfig::Socks(socks) => match socks { - access_method::Socks5::Remote(_) => write!(f, "Socks5 {}", endpoint), - access_method::Socks5::Local(local) => { - write!(f, "Socks5 {} via localhost:{}", endpoint, local.local_port) - } - }, + ProxyConfig::Socks5Remote(_) => write!(f, "Socks5 {}", endpoint), + ProxyConfig::Socks5Local(local) => { + write!(f, "Socks5 {} via localhost:{}", endpoint, local.local_port) + } } } } @@ -145,20 +142,16 @@ impl ApiConnectionMode { #[cfg(unix)] pub fn allowed_clients(&self) -> AllowedClients { - use access_method::Socks5; match self { - ApiConnectionMode::Proxied(ProxyConfig::Socks(Socks5::Local(_))) => AllowedClients::All, + ApiConnectionMode::Proxied(ProxyConfig::Socks5Local(_)) => AllowedClients::All, ApiConnectionMode::Direct | ApiConnectionMode::Proxied(_) => AllowedClients::Root, } } #[cfg(windows)] pub fn allowed_clients(&self) -> AllowedClients { - use access_method::Socks5; match self { - ApiConnectionMode::Proxied(ProxyConfig::Socks(Socks5::Local(_))) => { - AllowedClients::all() - } + ApiConnectionMode::Proxied(ProxyConfig::Socks5Local(_)) => AllowedClients::all(), ApiConnectionMode::Direct | ApiConnectionMode::Proxied(_) => { let daemon_exe = std::env::current_exe().expect("failed to obtain executable path"); vec![ 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(()) + } + } + } + } +} diff --git a/mullvad-daemon/src/api.rs b/mullvad-daemon/src/api.rs index 82f8e94fbc..c13bf77ded 100644 --- a/mullvad-daemon/src/api.rs +++ b/mullvad-daemon/src/api.rs @@ -16,7 +16,7 @@ use mullvad_api::{ ApiEndpointUpdateCallback, }; use mullvad_relay_selector::RelaySelector; -use mullvad_types::access_method::{self, AccessMethod, AccessMethodSetting, BuiltInAccessMethod}; +use mullvad_types::access_method::{AccessMethod, AccessMethodSetting, BuiltInAccessMethod}; use std::{ path::PathBuf, sync::{Arc, Mutex, Weak}, @@ -24,7 +24,7 @@ use std::{ #[cfg(target_os = "android")] use talpid_core::mpsc::Sender; use talpid_core::tunnel_state_machine::TunnelCommand; -use talpid_types::net::{openvpn::ProxySettings, AllowedEndpoint, Endpoint}; +use talpid_types::net::{AllowedEndpoint, Endpoint}; pub enum Message { Get(ResponseTx<AccessMethodSetting>), @@ -283,6 +283,7 @@ impl AccessModeSelector { /// [`ApiConnectionModeProvider`] the standard [`std::convert::From`] trait /// can not be implemented. fn from(&mut self, access_method: AccessMethod) -> ApiConnectionMode { + use talpid_types::net::proxy; match access_method { AccessMethod::BuiltIn(access_method) => match access_method { BuiltInAccessMethod::Direct => ApiConnectionMode::Direct, @@ -290,13 +291,12 @@ impl AccessModeSelector { .relay_selector .get_bridge_forced() .and_then(|settings| match settings { - ProxySettings::Shadowsocks(ss_settings) => { - let ss_settings: access_method::Shadowsocks = - access_method::Shadowsocks::new( - ss_settings.peer, - ss_settings.cipher, - ss_settings.password, - ); + proxy::CustomProxy::Shadowsocks(ss_settings) => { + let ss_settings: proxy::Shadowsocks = proxy::Shadowsocks::new( + ss_settings.endpoint, + ss_settings.cipher, + ss_settings.password, + ); Some(ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks( ss_settings, ))) @@ -309,11 +309,14 @@ impl AccessModeSelector { .unwrap_or(ApiConnectionMode::Direct), }, AccessMethod::Custom(access_method) => match access_method { - access_method::CustomAccessMethod::Shadowsocks(shadowsocks_config) => { + proxy::CustomProxy::Shadowsocks(shadowsocks_config) => { ApiConnectionMode::Proxied(ProxyConfig::Shadowsocks(shadowsocks_config)) } - access_method::CustomAccessMethod::Socks5(socks_config) => { - ApiConnectionMode::Proxied(ProxyConfig::Socks(socks_config)) + proxy::CustomProxy::Socks5Local(socks_config) => { + ApiConnectionMode::Proxied(ProxyConfig::Socks5Local(socks_config)) + } + proxy::CustomProxy::Socks5Remote(socks_config) => { + ApiConnectionMode::Proxied(ProxyConfig::Socks5Remote(socks_config)) } }, } diff --git a/mullvad-daemon/src/custom_list.rs b/mullvad-daemon/src/custom_list.rs index 8d244765cc..ce41b0059b 100644 --- a/mullvad-daemon/src/custom_list.rs +++ b/mullvad-daemon/src/custom_list.rs @@ -2,7 +2,7 @@ use crate::{new_selector_config, Daemon, Error, EventListener}; use mullvad_types::{ custom_list::{CustomList, Id}, relay_constraints::{ - BridgeSettings, BridgeState, Constraint, LocationConstraint, RelaySettings, + BridgeState, Constraint, LocationConstraint, RelaySettings, ResolvedBridgeSettings, }, }; use talpid_types::net::TunnelType; @@ -157,8 +157,8 @@ where TunnelType::OpenVpn => { if !matches!(self.settings.bridge_state, BridgeState::Off) { - if let BridgeSettings::Normal(bridge_settings) = - &self.settings.bridge_settings + if let Ok(ResolvedBridgeSettings::Normal(bridge_settings)) = + self.settings.bridge_settings.resolve() { if let Constraint::Only(LocationConstraint::CustomList { list_id, diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 72f5a87190..b429366492 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -49,7 +49,7 @@ use mullvad_types::{ device::{Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceState, RemoveDeviceEvent}, location::{GeoIpLocation, LocationEventData}, relay_constraints::{ - BridgeSettings, BridgeState, ObfuscationSettings, RelayOverride, RelaySettings, + BridgeSettings, BridgeState, BridgeType, ObfuscationSettings, RelayOverride, RelaySettings, }, relay_list::RelayList, settings::{DnsOptions, Settings}, @@ -182,6 +182,8 @@ pub enum Error { #[error(display = "API connection mode error")] ApiConnectionModeError(#[error(source)] api::Error), + #[error(display = "No custom bridge has been specified")] + NoCustomProxySaved, #[cfg(target_os = "macos")] #[error(display = "Failed to set exclusion group")] @@ -248,7 +250,7 @@ pub enum DaemonCommand { /// Set the mssfix argument for OpenVPN SetOpenVpnMssfix(ResponseTx<(), settings::Error>, Option<u16>), /// Set proxy details for OpenVPN - SetBridgeSettings(ResponseTx<(), settings::Error>, BridgeSettings), + SetBridgeSettings(ResponseTx<(), Error>, BridgeSettings), /// Set proxy state SetBridgeState(ResponseTx<(), settings::Error>, BridgeState), /// Set if IPv6 should be enabled in the tunnel @@ -2027,9 +2029,19 @@ where async fn on_set_bridge_settings( &mut self, - tx: ResponseTx<(), settings::Error>, + tx: ResponseTx<(), Error>, new_settings: BridgeSettings, ) { + if new_settings.custom.is_none() && new_settings.bridge_type == BridgeType::Custom { + log::info!("Tried to select custom bridge but no custom bridge settings exist"); + Self::oneshot_send( + tx, + Err(Error::NoCustomProxySaved), + "set_bridge_settings response", + ); + return; + } + match self .settings .update(move |settings| settings.bridge_settings = new_settings) @@ -2050,7 +2062,7 @@ where "{}", e.display_chain_with_msg("Failed to set new bridge settings") ); - Self::oneshot_send(tx, Err(e), "set_bridge_settings"); + Self::oneshot_send(tx, Err(Error::SettingsError(e)), "set_bridge_settings"); } } } diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index cf007e665d..6985af685c 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -202,7 +202,7 @@ impl ManagementService for ManagementServiceImpl { let (tx, rx) = oneshot::channel(); self.send_command_to_daemon(DaemonCommand::SetBridgeSettings(tx, settings))?; - self.wait_for_result(rx).await??; + self.wait_for_result(rx).await?.map_err(map_daemon_error)?; Ok(Response::new(())) } diff --git a/mullvad-daemon/src/migrations/mod.rs b/mullvad-daemon/src/migrations/mod.rs index e6977d9e22..5d13372a07 100644 --- a/mullvad-daemon/src/migrations/mod.rs +++ b/mullvad-daemon/src/migrations/mod.rs @@ -51,6 +51,7 @@ mod v3; mod v4; mod v5; mod v6; +mod v7; const SETTINGS_FILE: &str = "settings.json"; @@ -148,6 +149,7 @@ pub async fn migrate_all(cache_dir: &Path, settings_dir: &Path) -> Result<Option let migration_data = v5::migrate(&mut settings)?; v6::migrate(&mut settings)?; + v7::migrate(&mut settings)?; if settings == old_settings { // Nothing changed diff --git a/mullvad-daemon/src/migrations/v7.rs b/mullvad-daemon/src/migrations/v7.rs new file mode 100644 index 0000000000..1453685e57 --- /dev/null +++ b/mullvad-daemon/src/migrations/v7.rs @@ -0,0 +1,995 @@ +use std::net::SocketAddr; + +use super::{Error, Result}; +use mullvad_types::{ + relay_constraints::{BridgeConstraints, BridgeSettings as NewBridgeSettings, BridgeType}, + settings::SettingsVersion, +}; +use serde::{Deserialize, Serialize}; +use talpid_types::net::{ + proxy::{CustomProxy, Shadowsocks, Socks5Local, Socks5Remote, SocksAuth}, + Endpoint, TransportProtocol, +}; + +// ====================================================== +// Section for vendoring types and values that +// this settings version depend on. See `mod.rs`. + +/// Specifies a specific endpoint or [`BridgeConstraints`] to use when `mullvad-daemon` selects a +/// bridge server. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum BridgeSettings { + /// Let the relay selection algorithm decide on bridges, based on the relay list. + Normal(BridgeConstraints), + Custom(ProxySettings), +} + +/// Proxy server options to be used by `OpenVpnMonitor` when starting a tunnel. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ProxySettings { + Local(LocalProxySettings), + Remote(RemoteProxySettings), + Shadowsocks(ShadowsocksProxySettings), +} + +/// Options for a generic proxy running on localhost. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct LocalProxySettings { + pub port: u16, + pub peer: SocketAddr, +} + +/// Options for a generic proxy running on remote host. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct RemoteProxySettings { + pub address: SocketAddr, + pub auth: Option<ProxyAuth>, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct ProxyAuth { + pub username: String, + pub password: String, +} + +/// Options for a bundled Shadowsocks proxy. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct ShadowsocksProxySettings { + pub peer: SocketAddr, + /// Password on peer. + pub password: String, + pub cipher: String, + #[cfg(target_os = "linux")] + pub fwmark: Option<u32>, +} + +// ====================================================== +/// This is a closed migration. + +/// We change bridge settings to no longer be an enum with custom and normal variants. It now is a +/// struct which contains a bridge type, a normal relay constraint and optional custom constraints. +/// +/// We also migrate api access methods to use a slightly more shallow CustomProxy implementation +/// that instead of having a Socks5 and Shadowsocks variant instead has a Socks5Local, Socks5Remote +/// and Shadowsocks variant. +/// +/// We also take the oppertunity to rename a couple of fields that relate to proxy types. +/// We rename +/// - shadowsocks.peer to shadowsocks.endpoint +/// - socks5_remote.authentication to socks5_remote.auth +/// - socks5_remote.peer to socks5_remote.endpoint +/// +pub fn migrate(settings: &mut serde_json::Value) -> Result<()> { + if !version_matches(settings) { + return Ok(()); + } + + log::info!("Migrating settings format to V8"); + + migrate_bridge_settings(settings)?; + + migrate_api_access_settings(settings)?; + + settings["settings_version"] = serde_json::json!(SettingsVersion::V8); + + Ok(()) +} + +fn migrate_api_access_settings(settings: &mut serde_json::Value) -> Result<()> { + if let Some(access_method_settings_list) = settings + .get_mut("api_access_methods") + .and_then(|api_access_methods| api_access_methods.get_mut("access_method_settings")) + .and_then(|access_method_settings| access_method_settings.as_array_mut()) + { + for access_method_setting in access_method_settings_list { + let access_method = access_method_setting + .get_mut("access_method") + .ok_or(Error::InvalidSettingsContent)?; + match access_method.get_mut("custom") { + None => continue, + Some(custom_access_method) => { + if let Some(shadowsocks) = custom_access_method.get_mut("shadowsocks") { + rename_field(shadowsocks, "peer", "endpoint")?; + } else if let Some(new_socks5_local) = custom_access_method + .get("socks5") + .and_then(|socks5| socks5.get("local")) + { + custom_access_method["socks5_local"] = new_socks5_local.clone(); + custom_access_method + .as_object_mut() + .ok_or(Error::InvalidSettingsContent)? + .remove("socks5"); + } else if let Some(socks5_remote) = custom_access_method + .get("socks5") + .and_then(|socks5| socks5.get("remote")) + { + let mut new_socks5_remote = socks5_remote.clone(); + rename_field(&mut new_socks5_remote, "authentication", "auth")?; + rename_field(&mut new_socks5_remote, "peer", "endpoint")?; + + custom_access_method["socks5_remote"] = new_socks5_remote; + custom_access_method + .as_object_mut() + .ok_or(Error::InvalidSettingsContent)? + .remove("socks5"); + } else { + return Err(Error::InvalidSettingsContent); + } + } + } + } + } + + Ok(()) +} + +fn migrate_bridge_settings(settings: &mut serde_json::Value) -> Result<()> { + let new = if let Some(custom_bridge_local) = settings + .get_mut("bridge_settings") + .and_then(|bridge_settings| bridge_settings.get_mut("custom")) + .and_then(|bridge_settings_custom| bridge_settings_custom.get_mut("local")) + { + NewBridgeSettings { + bridge_type: BridgeType::Custom, + normal: BridgeConstraints::default(), + custom: Some(CustomProxy::Socks5Local(Socks5Local { + remote_endpoint: Endpoint { + address: extract_str(custom_bridge_local.get("peer"))? + .parse() + .map_err(|_| Error::InvalidSettingsContent)?, + protocol: TransportProtocol::Tcp, + }, + local_port: custom_bridge_local + .get("port") + .ok_or(Error::InvalidSettingsContent)? + .as_u64() + .ok_or(Error::InvalidSettingsContent)? + .try_into() + .map_err(|_| Error::InvalidSettingsContent)?, + })), + } + } else if let Some(custom_bridge_remote) = settings + .get_mut("bridge_settings") + .and_then(|bridge_settings| bridge_settings.get_mut("custom")) + .and_then(|bridge_settings_custom| bridge_settings_custom.get_mut("remote")) + { + NewBridgeSettings { + bridge_type: BridgeType::Custom, + normal: BridgeConstraints::default(), + custom: Some(CustomProxy::Socks5Remote(Socks5Remote { + endpoint: extract_str(custom_bridge_remote.get("address"))? + .parse() + .map_err(|_| Error::InvalidSettingsContent)?, + auth: custom_bridge_remote.get("auth").and_then(|auth| { + Some(SocksAuth { + username: auth.get("username")?.to_string(), + password: auth.get("password")?.to_string(), + }) + }), + })), + } + } else if let Some(custom_bridge_shadowsocks) = settings + .get_mut("bridge_settings") + .and_then(|bridge_settings| bridge_settings.get_mut("custom")) + .and_then(|bridge_settings_custom| bridge_settings_custom.get_mut("shadowsocks")) + { + NewBridgeSettings { + bridge_type: BridgeType::Custom, + normal: BridgeConstraints::default(), + custom: Some(CustomProxy::Shadowsocks(Shadowsocks { + endpoint: extract_str(custom_bridge_shadowsocks.get("peer"))? + .parse() + .map_err(|_| Error::InvalidSettingsContent)?, + password: extract_str(custom_bridge_shadowsocks.get("password"))?.to_string(), + cipher: extract_str(custom_bridge_shadowsocks.get("cipher"))?.to_string(), + })), + } + } else if let Some(normal_bridge) = settings + .get_mut("bridge_settings") + .and_then(|bridge_settings| bridge_settings.get_mut("normal")) + { + NewBridgeSettings { + bridge_type: BridgeType::Normal, + normal: serde_json::from_value(normal_bridge.clone()).map_err(Error::Serialize)?, + custom: None, + } + } else { + return Ok(()); + }; + + settings["bridge_settings"] = serde_json::json!(new); + + Ok(()) +} + +fn extract_str(opt: Option<&serde_json::Value>) -> Result<&str> { + opt.ok_or(Error::InvalidSettingsContent)? + .as_str() + .ok_or(Error::InvalidSettingsContent) +} + +fn rename_field(object: &mut serde_json::Value, old_name: &str, new_name: &str) -> Result<()> { + object[new_name] = object + .get(old_name) + .ok_or(Error::InvalidSettingsContent)? + .clone(); + object + .as_object_mut() + .ok_or(Error::InvalidSettingsContent)? + .remove(old_name); + Ok(()) +} + +fn version_matches(settings: &mut serde_json::Value) -> bool { + settings + .get("settings_version") + .map(|version| version == SettingsVersion::V7 as u64) + .unwrap_or(false) +} + +#[cfg(test)] +mod test { + use crate::migrations::v7::{migrate_api_access_settings, migrate_bridge_settings}; + + use super::{migrate, version_matches}; + use serde_json; + + pub const V7_SETTINGS: &str = r#" +{ + + "relay_settings": { + "normal": { + "location": { + "only": { + "location": { + "hostname": [ + "ch", + "zrh", + "ch-zrh-ovpn-001" + ] + } + } + }, + "providers": "any", + "ownership": "any", + "tunnel_protocol": "any", + "wireguard_constraints": { + "port": "any", + "ip_version": "any", + "use_multihop": false, + "entry_location": { + "only": { + "location": { + "country": "se" + } + } + } + }, + "openvpn_constraints": { + "port": { + "only": { + "protocol": "udp", + "port": { + "only": 1195 + } + } + } + } + } + }, + "bridge_settings": { + "custom": { + "local": { + "port": 1080, + "peer": "1.3.3.7:22" + } + } + }, + "api_access_methods": { + "access_method_settings": [ + { + "id": "8cbdcfc8-fa7b-41de-8d12-26fa37439f89", + "name": "Direct", + "enabled": true, + "access_method": { + "built_in": "direct" + } + }, + { + "id": "1d0d8891-dbb3-4439-a8f7-0e7d742ddbe4", + "name": "Mullvad Bridges", + "enabled": true, + "access_method": { + "built_in": "bridge" + } + }, + { + "id": "1aaff7ab-e09f-4c03-af02-765e41943a7b", + "name": "localsox", + "enabled": false, + "access_method": { + "custom": { + "socks5": { + "local": { + "remote_endpoint": { + "address": "1.3.3.7:1080", + "protocol": "tcp" + }, + "local_port": 1079 + } + } + } + } + }, + { + "id": "1e377232-8a53-4414-8b8f-f487227aaedb", + "name": "remotesox", + "enabled": false, + "access_method": { + "custom": { + "socks5": { + "remote": { + "peer": "1.3.3.7:1080", + "authentication": null + } + } + } + } + }, + { + "id": "74e5c659-acdd-4cad-a632-a25bf63c20e2", + "name": "remotess", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "peer": "1.3.3.7:1080", + "password": "mypass", + "cipher": "aes-128-cfb" + } + } + } + } + ] + }, + "obfuscation_settings": { + "selected_obfuscation": "udp2_tcp", + "udp2tcp": { + "port": "any" + } + }, + "bridge_state": "auto", + "allow_lan": true, + "block_when_disconnected": false, + "auto_connect": false, + "tunnel_options": { + "openvpn": { + "mssfix": null + }, + "wireguard": { + "mtu": null, + "rotation_interval": { + "secs": 86400, + "nanos": 0 + }, + "quantum_resistant": "auto" + }, + "generic": { + "enable_ipv6": false + }, + "dns_options": { + "state": "default", + "default_options": { + "block_ads": false, + "block_trackers": false + }, + "custom_options": { + "addresses": [ + "1.1.1.1", + "1.2.3.4" + ] + } + } + }, + "settings_version": 7 +} +"#; + + pub const V8_SETTINGS: &str = r#" +{ + + "relay_settings": { + "normal": { + "location": { + "only": { + "location": { + "hostname": [ + "ch", + "zrh", + "ch-zrh-ovpn-001" + ] + } + } + }, + "providers": "any", + "ownership": "any", + "tunnel_protocol": "any", + "wireguard_constraints": { + "port": "any", + "ip_version": "any", + "use_multihop": false, + "entry_location": { + "only": { + "location": { + "country": "se" + } + } + } + }, + "openvpn_constraints": { + "port": { + "only": { + "protocol": "udp", + "port": { + "only": 1195 + } + } + } + } + } + }, + "bridge_settings": { + "bridge_type": "custom", + "normal": { + "location": "any", + "providers": "any", + "ownership": "any" + }, + "custom": { + "socks5_local": { + "local_port": 1080, + "remote_endpoint": { + "address": "1.3.3.7:22", + "protocol": "tcp" + } + } + } + }, + "api_access_methods": { + "access_method_settings": [ + { + "id": "8cbdcfc8-fa7b-41de-8d12-26fa37439f89", + "name": "Direct", + "enabled": true, + "access_method": { + "built_in": "direct" + } + }, + { + "id": "1d0d8891-dbb3-4439-a8f7-0e7d742ddbe4", + "name": "Mullvad Bridges", + "enabled": true, + "access_method": { + "built_in": "bridge" + } + }, + { + "id": "1aaff7ab-e09f-4c03-af02-765e41943a7b", + "name": "localsox", + "enabled": false, + "access_method": { + "custom": { + "socks5_local": { + "remote_endpoint": { + "address": "1.3.3.7:1080", + "protocol": "tcp" + }, + "local_port": 1079 + } + } + } + }, + { + "id": "1e377232-8a53-4414-8b8f-f487227aaedb", + "name": "remotesox", + "enabled": false, + "access_method": { + "custom": { + "socks5_remote": { + "endpoint": "1.3.3.7:1080", + "auth": null + } + } + } + }, + { + "id": "74e5c659-acdd-4cad-a632-a25bf63c20e2", + "name": "remotess", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "1.3.3.7:1080", + "password": "mypass", + "cipher": "aes-128-cfb" + } + } + } + } + ] + }, + "obfuscation_settings": { + "selected_obfuscation": "udp2_tcp", + "udp2tcp": { + "port": "any" + } + }, + "bridge_state": "auto", + "allow_lan": true, + "block_when_disconnected": false, + "auto_connect": false, + "tunnel_options": { + "openvpn": { + "mssfix": null + }, + "wireguard": { + "mtu": null, + "rotation_interval": { + "secs": 86400, + "nanos": 0 + }, + "quantum_resistant": "auto" + }, + "generic": { + "enable_ipv6": false + }, + "dns_options": { + "state": "default", + "default_options": { + "block_ads": false, + "block_trackers": false + }, + "custom_options": { + "addresses": [ + "1.1.1.1", + "1.2.3.4" + ] + } + } + }, + "settings_version": 8 +} +"#; + + #[test] + fn test_v7_to_v8_migration() { + let mut old_settings = serde_json::from_str(V7_SETTINGS).unwrap(); + + assert!(version_matches(&mut old_settings)); + migrate(&mut old_settings).unwrap(); + let new_settings: serde_json::Value = serde_json::from_str(V8_SETTINGS).unwrap(); + + assert_eq!(&old_settings, &new_settings); + } + + #[test] + fn test_bridge_settings_custom_local_proxy() { + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "custom": { + "local": { + "port": 1080, + "peer": "1.3.3.7:22" + } + } + } +}"#, + ) + .unwrap(); + + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "bridge_type": "custom", + "normal": { + "location": "any", + "providers": "any", + "ownership": "any" + }, + "custom": { + "socks5_local": { + "local_port": 1080, + "remote_endpoint": { + "address": "1.3.3.7:22", + "protocol": "tcp" + } + } + } + } +}"#, + ) + .unwrap(); + + migrate_bridge_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } + + #[test] + fn test_bridge_settings_custom_remote_proxy() { + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "custom": { + "remote": { + "address": "1.3.3.7:1080", + "auth": null + } + } + } +}"#, + ) + .unwrap(); + + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "bridge_type": "custom", + "normal": { + "location": "any", + "providers": "any", + "ownership": "any" + }, + "custom": { + "socks5_remote": { + "endpoint": "1.3.3.7:1080", + "auth": null + } + } + } +}"#, + ) + .unwrap(); + + migrate_bridge_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } + + #[test] + fn test_bridge_settings_custom_shadowsocks_proxy() { + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "custom": { + "shadowsocks": { + "peer": "1.3.3.7:1080", + "password": "mypass", + "cipher": "aes-128-cfb", + "fwmark": 1836018789 + } + } + } +}"#, + ) + .unwrap(); + + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "bridge_type": "custom", + "normal": { + "location": "any", + "providers": "any", + "ownership": "any" + }, + "custom": { + "shadowsocks": { + "endpoint": "1.3.3.7:1080", + "password": "mypass", + "cipher": "aes-128-cfb" + } + } + } +}"#, + ) + .unwrap(); + migrate_bridge_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } + + #[test] + fn test_bridge_settings_normal() { + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "normal": { + "location": { + "only": { + "location": { + "country": "se" + } + } + }, + "providers": "any", + "ownership": "any" + } + } +}"#, + ) + .unwrap(); + + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "bridge_type": "normal", + "normal": { + "location": { + "only": { + "location": { + "country": "se" + } + } + }, + "providers": "any", + "ownership": "any" + }, + "custom": null + } +}"#, + ) + .unwrap(); + migrate_bridge_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } + + #[test] + fn test_bridge_settings_specific_location() { + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "normal": { + "location": { + "only": { + "location": { + "country": "se" + } + } + }, + "providers": "any", + "ownership": "any" + } + } +}"#, + ) + .unwrap(); + + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "bridge_settings": { + "bridge_type": "normal", + "normal": { + "location": { + "only": { + "location": { + "country": "se" + } + } + }, + "providers": "any", + "ownership": "any" + }, + "custom": null + } +}"#, + ) + .unwrap(); + migrate_bridge_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } + + #[test] + fn test_api_access_methods_custom_socks5_local() { + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "api_access_methods": { + "access_method_settings": [ + { + "id": "5eb9b2ee-f764-47c8-8111-ee95910d0099", + "name": "mysocks", + "enabled": false, + "access_method": { + "custom": { + "socks5": { + "local": { + "remote_endpoint": { + "address": "1.3.3.7:22", + "protocol": "tcp" + }, + "local_port": 1080 + } + } + } + } + } + ] + } +}"#, + ) + .unwrap(); + + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "api_access_methods": { + "access_method_settings": [ + { + "id": "5eb9b2ee-f764-47c8-8111-ee95910d0099", + "name": "mysocks", + "enabled": false, + "access_method": { + "custom": { + "socks5_local": { + "remote_endpoint": { + "address": "1.3.3.7:22", + "protocol": "tcp" + }, + "local_port": 1080 + } + } + } + } + ] + } +}"#, + ) + .unwrap(); + + migrate_api_access_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } + + #[test] + fn test_api_access_methods_custom_socks5_remote() { + println!("wew"); + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "api_access_methods": { + "access_method_settings": [ + { + "id": "8e377232-8a53-4414-8b8f-f487227aaedb", + "name": "remotesox", + "enabled": false, + "access_method": { + "custom": { + "socks5": { + "remote": { + "peer": "1.3.3.7:1080", + "authentication": null + } + } + } + } + } + ] + } +}"#, + ) + .unwrap(); + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "api_access_methods": { + "access_method_settings": [ + { + "id": "8e377232-8a53-4414-8b8f-f487227aaedb", + "name": "remotesox", + "enabled": false, + "access_method": { + "custom": { + "socks5_remote": { + "endpoint": "1.3.3.7:1080", + "auth": null + } + } + } + } + ] + } +}"#, + ) + .unwrap(); + + migrate_api_access_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } + + #[test] + fn test_api_access_methods_custom_socks5_shadowsocks() { + let mut pre: serde_json::Value = serde_json::from_str( + r#" +{ + "api_access_methods": { + "access_method_settings": [ + { + "id": "74e5c659-acdd-4cad-a632-a25bf63c20e2", + "name": "remotess", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "peer": "1.3.3.7:1080", + "password": "mypass", + "cipher": "aes-128-cfb" + } + } + } + } + ] + } +}"#, + ) + .unwrap(); + + let post: serde_json::Value = serde_json::from_str( + r#" +{ + "api_access_methods": { + "access_method_settings": [ + { + "id": "74e5c659-acdd-4cad-a632-a25bf63c20e2", + "name": "remotess", + "enabled": true, + "access_method": { + "custom": { + "shadowsocks": { + "endpoint": "1.3.3.7:1080", + "password": "mypass", + "cipher": "aes-128-cfb" + } + } + } + } + ] + } +}"#, + ) + .unwrap(); + + migrate_api_access_settings(&mut pre).unwrap(); + assert_eq!(pre, post); + } +} diff --git a/mullvad-daemon/src/settings/mod.rs b/mullvad-daemon/src/settings/mod.rs index c5639b8b76..8a63b5c5a6 100644 --- a/mullvad-daemon/src/settings/mod.rs +++ b/mullvad-daemon/src/settings/mod.rs @@ -462,8 +462,18 @@ mod test { } }, "bridge_settings": { + "bridge_type": "normal", "normal": { "location": "any" + }, + "custom": { + "socks5_local": { + "local_port": 1080, + "remote_endpoint": { + "address": "1.3.3.7:22", + "protocol": "tcp" + } + } } }, "bridge_state": "auto", @@ -482,7 +492,7 @@ mod test { "enable_ipv6": true } }, - "settings_version": 5, + "settings_version": 8, "show_beta_releases": false, "custom_lists": { "custom_lists": [] diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index eb3aa6bba1..268e70327d 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -260,36 +260,19 @@ enum Ownership { } message BridgeSettings { + enum BridgeType { + NORMAL = 0; + CUSTOM = 1; + } message BridgeConstraints { LocationConstraint location = 1; repeated string providers = 2; Ownership ownership = 3; } - message LocalProxySettings { - uint32 port = 1; - string peer = 2; - } - message RemoteProxySettings { - string address = 1; - RemoteProxyAuth auth = 2; - } - message RemoteProxyAuth { - string username = 1; - string password = 2; - } - message ShadowsocksProxySettings { - string peer = 1; - string password = 2; - string cipher = 3; - } - - oneof type { - BridgeConstraints normal = 1; - LocalProxySettings local = 2; - RemoteProxySettings remote = 3; - ShadowsocksProxySettings shadowsocks = 4; - } + BridgeType bridge_type = 1; + BridgeConstraints normal = 2; + CustomProxy custom = 3; } message LocationConstraint { @@ -334,30 +317,39 @@ message CustomList { message CustomListSettings { repeated CustomList custom_lists = 1; } +message Socks5Local { + string remote_ip = 1; + uint32 remote_port = 2; + TransportProtocol remote_transport_protocol = 3; + uint32 local_port = 4; +} +message SocksAuth { + string username = 1; + string password = 2; +} +message Socks5Remote { + string ip = 1; + uint32 port = 2; + SocksAuth auth = 3; +} +message Shadowsocks { + string ip = 1; + uint32 port = 2; + string password = 3; + string cipher = 4; +} + +message CustomProxy { + oneof proxy_method { + Socks5Local socks5local = 1; + Socks5Remote socks5remote = 2; + Shadowsocks shadowsocks = 3; + } +} + message AccessMethod { message Direct {} message Bridges {} - message Socks5Local { - string remote_ip = 1; - uint32 remote_port = 2; - TransportProtocol remote_transport_protocol = 3; - uint32 local_port = 4; - } - message SocksAuth { - string username = 1; - string password = 2; - } - message Socks5Remote { - string ip = 1; - uint32 port = 2; - SocksAuth authentication = 3; - } - message Shadowsocks { - string ip = 1; - uint32 port = 2; - string password = 3; - string cipher = 4; - } oneof access_method { Direct direct = 1; Bridges bridges = 2; diff --git a/mullvad-management-interface/src/types/conversions/access_method.rs b/mullvad-management-interface/src/types/conversions/access_method.rs index 425e0a70da..368528f5bc 100644 --- a/mullvad-management-interface/src/types/conversions/access_method.rs +++ b/mullvad-management-interface/src/types/conversions/access_method.rs @@ -46,8 +46,10 @@ mod data { use crate::types::{proto, FromProtobufTypeError}; use mullvad_types::access_method::{ - AccessMethod, AccessMethodSetting, BuiltInAccessMethod, CustomAccessMethod, Id, - Shadowsocks, Socks5, Socks5Local, Socks5Remote, SocksAuth, + AccessMethod, AccessMethodSetting, BuiltInAccessMethod, Id, + }; + use talpid_types::net::proxy::{ + CustomProxy, Shadowsocks, Socks5Local, Socks5Remote, SocksAuth, }; impl TryFrom<proto::AccessMethodSetting> for AccessMethodSetting { @@ -140,10 +142,10 @@ mod data { } } - impl TryFrom<proto::access_method::Socks5Local> for AccessMethod { + impl TryFrom<proto::Socks5Local> for AccessMethod { type Error = FromProtobufTypeError; - fn try_from(value: proto::access_method::Socks5Local) -> Result<Self, Self::Error> { + fn try_from(value: proto::Socks5Local) -> Result<Self, Self::Error> { use crate::types::conversions::net::try_transport_protocol_from_i32; let remote_ip = value.remote_ip.parse::<Ipv4Addr>().map_err(|_| { FromProtobufTypeError::InvalidArgument( @@ -160,15 +162,11 @@ mod data { } } - impl TryFrom<proto::access_method::Socks5Remote> for AccessMethod { + impl TryFrom<proto::Socks5Remote> for AccessMethod { type Error = FromProtobufTypeError; - fn try_from(value: proto::access_method::Socks5Remote) -> Result<Self, Self::Error> { - let proto::access_method::Socks5Remote { - ip, - port, - authentication, - } = value; + fn try_from(value: proto::Socks5Remote) -> Result<Self, Self::Error> { + let proto::Socks5Remote { ip, port, auth } = value; let ip = ip.parse::<Ipv4Addr>().map_err(|_| { FromProtobufTypeError::InvalidArgument( "Could not parse Socks5 (remote) message from protobuf", @@ -176,19 +174,17 @@ mod data { })?; let port = port as u16; - Ok(AccessMethod::from( - match authentication.map(SocksAuth::from) { - Some(auth) => Socks5Remote::new_with_authentication((ip, port), auth), - None => Socks5Remote::new((ip, port)), - }, - )) + Ok(AccessMethod::from(match auth.map(SocksAuth::from) { + Some(auth) => Socks5Remote::new_with_authentication((ip, port), auth), + None => Socks5Remote::new((ip, port)), + })) } } - impl TryFrom<proto::access_method::Shadowsocks> for AccessMethod { + impl TryFrom<proto::Shadowsocks> for AccessMethod { type Error = FromProtobufTypeError; - fn try_from(value: proto::access_method::Shadowsocks) -> Result<Self, Self::Error> { + fn try_from(value: proto::Shadowsocks) -> Result<Self, Self::Error> { let ip = value.ip.parse::<Ipv4Addr>().map_err(|_| { FromProtobufTypeError::InvalidArgument( "Could not parse Socks5 (remote) message from protobuf", @@ -219,42 +215,35 @@ mod data { } } - impl From<CustomAccessMethod> for proto::AccessMethod { - fn from(value: CustomAccessMethod) -> Self { + impl From<CustomProxy> for proto::AccessMethod { + fn from(value: CustomProxy) -> Self { let access_method = match value { - CustomAccessMethod::Shadowsocks(ss) => { - proto::access_method::AccessMethod::Shadowsocks( - proto::access_method::Shadowsocks { - ip: ss.peer.ip().to_string(), - port: ss.peer.port() as u32, - password: ss.password, - cipher: ss.cipher, - }, - ) + CustomProxy::Shadowsocks(ss) => { + proto::access_method::AccessMethod::Shadowsocks(proto::Shadowsocks { + ip: ss.endpoint.ip().to_string(), + port: u32::from(ss.endpoint.port()), + password: ss.password, + cipher: ss.cipher, + }) } - CustomAccessMethod::Socks5(Socks5::Local(Socks5Local { + CustomProxy::Socks5Local(Socks5Local { remote_endpoint, local_port, - })) => proto::access_method::AccessMethod::Socks5local( - proto::access_method::Socks5Local { - remote_ip: remote_endpoint.address.ip().to_string(), - remote_port: remote_endpoint.address.port() as u32, - remote_transport_protocol: i32::from(proto::TransportProtocol::from( - remote_endpoint.protocol, - )), - local_port: local_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::access_method::AccessMethod::Socks5local(proto::Socks5Local { + remote_ip: remote_endpoint.address.ip().to_string(), + remote_port: remote_endpoint.address.port() as u32, + remote_transport_protocol: i32::from(proto::TransportProtocol::from( + remote_endpoint.protocol, + )), + local_port: u32::from(local_port), + }), + CustomProxy::Socks5Remote(Socks5Remote { endpoint, auth }) => { + proto::access_method::AccessMethod::Socks5remote(proto::Socks5Remote { + ip: endpoint.ip().to_string(), + port: u32::from(endpoint.port()), + auth: auth.map(proto::SocksAuth::from), + }) + } }; proto::AccessMethod { @@ -281,24 +270,6 @@ mod data { } } - impl From<SocksAuth> for proto::access_method::SocksAuth { - fn from(value: SocksAuth) -> Self { - proto::access_method::SocksAuth { - username: value.username, - password: value.password, - } - } - } - - impl From<proto::access_method::SocksAuth> 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-management-interface/src/types/conversions/net.rs b/mullvad-management-interface/src/types/conversions/net.rs index e2df5553a0..9a873e82ab 100644 --- a/mullvad-management-interface/src/types/conversions/net.rs +++ b/mullvad-management-interface/src/types/conversions/net.rs @@ -182,3 +182,145 @@ pub fn try_transport_protocol_from_i32( .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid transport protocol"))? .into()) } + +mod proxy { + use std::net::Ipv4Addr; + + use crate::types::{proto, FromProtobufTypeError}; + use talpid_types::net::proxy::{ + CustomProxy, Shadowsocks, Socks5Local, Socks5Remote, SocksAuth, + }; + + impl TryFrom<proto::CustomProxy> for CustomProxy { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::CustomProxy) -> Result<Self, Self::Error> { + Ok(match value.proxy_method { + Some(proto::custom_proxy::ProxyMethod::Socks5local(local)) => { + CustomProxy::Socks5Local(Socks5Local::try_from(local)?) + } + Some(proto::custom_proxy::ProxyMethod::Socks5remote(remote)) => { + CustomProxy::Socks5Remote(Socks5Remote::try_from(remote)?) + } + Some(proto::custom_proxy::ProxyMethod::Shadowsocks(shadowsocks)) => { + CustomProxy::Shadowsocks(Shadowsocks::try_from(shadowsocks)?) + } + None => { + return Err(FromProtobufTypeError::InvalidArgument( + "CustomProxy missing proxy_method field", + )); + } + }) + } + } + + impl TryFrom<proto::Socks5Local> for Socks5Local { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::Socks5Local) -> Result<Self, Self::Error> { + use crate::types::conversions::net::try_transport_protocol_from_i32; + let remote_ip = value.remote_ip.parse::<Ipv4Addr>().map_err(|_| { + FromProtobufTypeError::InvalidArgument( + "Could not parse Socks5 (local) message from protobuf", + ) + })?; + Ok(Socks5Local::new_with_transport_protocol( + (remote_ip, value.remote_port as u16), + value.local_port as u16, + try_transport_protocol_from_i32(value.remote_transport_protocol)?, + )) + } + } + + impl TryFrom<proto::Socks5Remote> for Socks5Remote { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::Socks5Remote) -> Result<Self, Self::Error> { + let proto::Socks5Remote { ip, port, auth } = value; + let ip = ip.parse::<Ipv4Addr>().map_err(|_| { + FromProtobufTypeError::InvalidArgument( + "Could not parse Socks5 (remote) message from protobuf", + ) + })?; + let port = port as u16; + + Ok(match auth.map(SocksAuth::from) { + Some(auth) => Socks5Remote::new_with_authentication((ip, port), auth), + None => Socks5Remote::new((ip, port)), + }) + } + } + + impl TryFrom<proto::Shadowsocks> for Shadowsocks { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::Shadowsocks) -> Result<Self, Self::Error> { + let ip = value.ip.parse::<Ipv4Addr>().map_err(|_| { + FromProtobufTypeError::InvalidArgument( + "Could not parse Socks5 (remote) message from protobuf", + ) + })?; + + Ok(Shadowsocks::new( + (ip, value.port as u16), + value.cipher, + value.password, + )) + } + } + + impl From<CustomProxy> for proto::CustomProxy { + fn from(value: CustomProxy) -> Self { + let proxy_method = match value { + CustomProxy::Shadowsocks(ss) => { + proto::custom_proxy::ProxyMethod::Shadowsocks(proto::Shadowsocks { + ip: ss.endpoint.ip().to_string(), + port: ss.endpoint.port() as u32, + password: ss.password, + cipher: ss.cipher, + }) + } + CustomProxy::Socks5Local(Socks5Local { + remote_endpoint, + local_port, + }) => proto::custom_proxy::ProxyMethod::Socks5local(proto::Socks5Local { + remote_ip: remote_endpoint.address.ip().to_string(), + remote_port: remote_endpoint.address.port() as u32, + remote_transport_protocol: i32::from(proto::TransportProtocol::from( + remote_endpoint.protocol, + )), + local_port: local_port as u32, + }), + CustomProxy::Socks5Remote(Socks5Remote { endpoint, auth }) => { + proto::custom_proxy::ProxyMethod::Socks5remote(proto::Socks5Remote { + ip: endpoint.ip().to_string(), + port: endpoint.port() as u32, + auth: auth.map(proto::SocksAuth::from), + }) + } + }; + + proto::CustomProxy { + proxy_method: Some(proxy_method), + } + } + } + + impl From<SocksAuth> for proto::SocksAuth { + fn from(value: SocksAuth) -> Self { + proto::SocksAuth { + username: value.username, + password: value.password, + } + } + } + + impl From<proto::SocksAuth> for SocksAuth { + fn from(value: proto::SocksAuth) -> Self { + Self { + username: value.username, + password: value.password, + } + } + } +} diff --git a/mullvad-management-interface/src/types/conversions/relay_constraints.rs b/mullvad-management-interface/src/types/conversions/relay_constraints.rs index a1d468c976..cdae59d60f 100644 --- a/mullvad-management-interface/src/types/conversions/relay_constraints.rs +++ b/mullvad-management-interface/src/types/conversions/relay_constraints.rs @@ -4,6 +4,7 @@ use mullvad_types::{ relay_constraints::{Constraint, GeographicLocationConstraint}, }; use std::str::FromStr; +use talpid_types::net::proxy::CustomProxy; impl TryFrom<&proto::WireguardConstraints> for mullvad_types::relay_constraints::WireguardConstraints @@ -178,52 +179,34 @@ impl From<&mullvad_types::relay_constraints::Udp2TcpObfuscationSettings> impl From<mullvad_types::relay_constraints::BridgeSettings> for proto::BridgeSettings { fn from(settings: mullvad_types::relay_constraints::BridgeSettings) -> Self { - use mullvad_types::relay_constraints::BridgeSettings as MullvadBridgeSettings; use proto::bridge_settings; - use talpid_types::net as talpid_net; - let settings = match settings { - MullvadBridgeSettings::Normal(constraints) => { - bridge_settings::Type::Normal(bridge_settings::BridgeConstraints { - location: constraints - .location - .clone() - .option() - .map(proto::LocationConstraint::from), - providers: convert_providers_constraint(&constraints.providers), - ownership: convert_ownership_constraint(&constraints.ownership) as i32, - }) + let mode = match settings.bridge_type { + mullvad_types::relay_constraints::BridgeType::Normal => { + bridge_settings::BridgeType::Normal } - MullvadBridgeSettings::Custom(proxy_settings) => match proxy_settings { - talpid_net::openvpn::ProxySettings::Local(proxy_settings) => { - bridge_settings::Type::Local(bridge_settings::LocalProxySettings { - port: u32::from(proxy_settings.port), - peer: proxy_settings.peer.to_string(), - }) - } - talpid_net::openvpn::ProxySettings::Remote(proxy_settings) => { - bridge_settings::Type::Remote(bridge_settings::RemoteProxySettings { - address: proxy_settings.address.to_string(), - auth: proxy_settings.auth.as_ref().map(|auth| { - bridge_settings::RemoteProxyAuth { - username: auth.username.clone(), - password: auth.password.clone(), - } - }), - }) - } - talpid_net::openvpn::ProxySettings::Shadowsocks(proxy_settings) => { - bridge_settings::Type::Shadowsocks(bridge_settings::ShadowsocksProxySettings { - peer: proxy_settings.peer.to_string(), - password: proxy_settings.password.clone(), - cipher: proxy_settings.cipher, - }) - } - }, + mullvad_types::relay_constraints::BridgeType::Custom => { + bridge_settings::BridgeType::Custom + } + }; + + let normal = bridge_settings::BridgeConstraints { + location: settings + .normal + .location + .clone() + .option() + .map(proto::LocationConstraint::from), + providers: convert_providers_constraint(&settings.normal.providers), + ownership: i32::from(convert_ownership_constraint(&settings.normal.ownership)), }; + let custom = settings.custom.map(proto::CustomProxy::from); + proto::BridgeSettings { - r#type: Some(settings), + bridge_type: i32::from(mode), + normal: Some(normal), + custom, } } } @@ -389,79 +372,59 @@ impl TryFrom<proto::GeographicLocationConstraint> for GeographicLocationConstrai } } +pub fn try_bridge_mode_from_i32( + mode: i32, +) -> Result<mullvad_types::relay_constraints::BridgeType, FromProtobufTypeError> { + proto::bridge_settings::BridgeType::try_from(mode) + .map(mullvad_types::relay_constraints::BridgeType::from) + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid bridge mode argument")) +} + +impl From<proto::bridge_settings::BridgeType> for mullvad_types::relay_constraints::BridgeType { + fn from(value: proto::bridge_settings::BridgeType) -> Self { + use mullvad_types::relay_constraints::BridgeType; + + match value { + proto::bridge_settings::BridgeType::Normal => BridgeType::Normal, + proto::bridge_settings::BridgeType::Custom => BridgeType::Custom, + } + } +} + impl TryFrom<proto::BridgeSettings> for mullvad_types::relay_constraints::BridgeSettings { type Error = FromProtobufTypeError; fn try_from(settings: proto::BridgeSettings) -> Result<Self, Self::Error> { - use mullvad_types::relay_constraints as mullvad_constraints; - use talpid_types::net as talpid_net; + use mullvad_types::relay_constraints::{BridgeConstraints, BridgeSettings}; - match settings - .r#type + // convert normal bridge settings + let constraints = settings + .normal .ok_or(FromProtobufTypeError::InvalidArgument( - "no settings provided", - ))? { - proto::bridge_settings::Type::Normal(constraints) => { - let location = match constraints.location { - None => Constraint::Any, - Some(location) => Constraint::< - mullvad_types::relay_constraints::LocationConstraint, - >::try_from(location)?, - }; - let providers = try_providers_constraint_from_proto(&constraints.providers)?; - let ownership = try_ownership_constraint_from_i32(constraints.ownership)?; - - Ok(mullvad_constraints::BridgeSettings::Normal( - mullvad_constraints::BridgeConstraints { - location, - providers, - ownership, - }, - )) - } - proto::bridge_settings::Type::Local(proxy_settings) => { - let peer = proxy_settings.peer.parse().map_err(|_| { - FromProtobufTypeError::InvalidArgument("failed to parse peer address") - })?; - let proxy_settings = talpid_net::openvpn::ProxySettings::Local( - talpid_net::openvpn::LocalProxySettings { - port: proxy_settings.port as u16, - peer, - }, - ); - Ok(mullvad_constraints::BridgeSettings::Custom(proxy_settings)) - } - proto::bridge_settings::Type::Remote(proxy_settings) => { - let address = proxy_settings.address.parse().map_err(|_| { - FromProtobufTypeError::InvalidArgument("failed to parse IP address") - })?; - let auth = proxy_settings - .auth - .map(|auth| talpid_net::openvpn::ProxyAuth { - username: auth.username, - password: auth.password, - }); - let proxy_settings = talpid_net::openvpn::ProxySettings::Remote( - talpid_net::openvpn::RemoteProxySettings { address, auth }, - ); - Ok(mullvad_constraints::BridgeSettings::Custom(proxy_settings)) - } - proto::bridge_settings::Type::Shadowsocks(proxy_settings) => { - let peer = proxy_settings.peer.parse().map_err(|_| { - FromProtobufTypeError::InvalidArgument("failed to parse peer address") - })?; - let proxy_settings = talpid_net::openvpn::ProxySettings::Shadowsocks( - talpid_net::openvpn::ShadowsocksProxySettings { - #[cfg(target_os = "linux")] - fwmark: Some(mullvad_types::TUNNEL_FWMARK), - peer, - password: proxy_settings.password, - cipher: proxy_settings.cipher, - }, - ); - Ok(mullvad_constraints::BridgeSettings::Custom(proxy_settings)) + "missing normal bridge constraints", + ))?; + let location = match constraints.location { + None => Constraint::Any, + Some(location) => { + Constraint::<mullvad_types::relay_constraints::LocationConstraint>::try_from( + location, + )? } - } + }; + let normal = BridgeConstraints { + location, + providers: try_providers_constraint_from_proto(&constraints.providers)?, + ownership: try_ownership_constraint_from_i32(constraints.ownership)?, + }; + + // convert custom bridge settings + let custom = settings.custom.map(CustomProxy::try_from).transpose()?; + + Ok(BridgeSettings { + bridge_type: try_bridge_mode_from_i32(settings.bridge_type)?, + normal, + custom, + }) } } diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index e66e574146..ac0da9f9b3 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -9,9 +9,10 @@ use mullvad_types::{ location::{Coordinates, Location}, relay_constraints::{ BridgeSettings, BridgeState, Constraint, InternalBridgeConstraints, LocationConstraint, - Match, ObfuscationSettings, OpenVpnConstraints, Ownership, Providers, RelayConstraints, - RelayConstraintsFormatter, RelayOverride, RelaySettings, ResolvedLocationConstraint, - SelectedObfuscation, Set, TransportPort, Udp2TcpObfuscationSettings, + Match, MissingCustomBridgeSettings, ObfuscationSettings, OpenVpnConstraints, Ownership, + Providers, RelayConstraints, RelayConstraintsFormatter, RelayOverride, RelaySettings, + ResolvedBridgeSettings, ResolvedLocationConstraint, SelectedObfuscation, Set, + TransportPort, Udp2TcpObfuscationSettings, }, relay_list::{BridgeEndpointData, Relay, RelayEndpointData, RelayList}, settings::Settings, @@ -29,8 +30,8 @@ use std::{ }; use talpid_types::{ net::{ - obfuscation::ObfuscatorConfig, openvpn::ProxySettings, wireguard, IpVersion, - TransportProtocol, TunnelType, + obfuscation::ObfuscatorConfig, proxy::CustomProxy, wireguard, IpVersion, TransportProtocol, + TunnelType, }, ErrorExt, }; @@ -77,6 +78,9 @@ pub enum Error { #[error(display = "Downloader already shut down")] DownloaderShutDown, + + #[error(display = "Invalid bridge settings")] + InvalidBridgeSettings(#[error(source)] MissingCustomBridgeSettings), } struct ParsedRelays { @@ -869,8 +873,12 @@ impl RelaySelector { retry_attempt: u32, custom_lists: &CustomListsSettings, ) -> Result<Option<SelectedBridge>, Error> { - match &config.bridge_settings { - BridgeSettings::Normal(settings) => { + match config + .bridge_settings + .resolve() + .map_err(Error::InvalidBridgeSettings)? + { + ResolvedBridgeSettings::Normal(settings) => { let bridge_constraints = InternalBridgeConstraints { location: settings.location.clone(), providers: settings.providers.clone(), @@ -896,7 +904,7 @@ impl RelaySelector { BridgeState::Auto | BridgeState::Off => Ok(None), } } - BridgeSettings::Custom(bridge_settings) => match config.bridge_state { + ResolvedBridgeSettings::Custom(bridge_settings) => match config.bridge_state { BridgeState::On => Ok(Some(SelectedBridge::Custom(bridge_settings.clone()))), BridgeState::Auto if Self::should_use_bridge(retry_attempt) => { Ok(Some(SelectedBridge::Custom(bridge_settings.clone()))) @@ -906,8 +914,9 @@ impl RelaySelector { } } - /// Returns a bridge based on the relay and bridge constraints, ignoring the bridge state. - pub fn get_bridge_forced(&self) -> Option<ProxySettings> { + /// Returns a non-custom bridge based on the relay and bridge constraints, ignoring the bridge + /// state. + pub fn get_bridge_forced(&self) -> Option<CustomProxy> { let config = self.config.lock(); let near_location = match &config.relay_settings { @@ -917,14 +926,14 @@ impl RelaySelector { _ => None, }; - let constraints = match &config.bridge_settings { - BridgeSettings::Normal(settings) => InternalBridgeConstraints { + let constraints = match config.bridge_settings.resolve() { + Ok(ResolvedBridgeSettings::Normal(settings)) => InternalBridgeConstraints { location: settings.location.clone(), providers: settings.providers.clone(), ownership: settings.ownership, transport_protocol: Constraint::Only(TransportProtocol::Tcp), }, - BridgeSettings::Custom(_bridge_settings) => InternalBridgeConstraints { + _ => InternalBridgeConstraints { location: Constraint::Any, providers: Constraint::Any, ownership: Constraint::Any, @@ -951,7 +960,7 @@ impl RelaySelector { constraints: &InternalBridgeConstraints, location: Option<T>, custom_lists: &CustomListsSettings, - ) -> Option<(ProxySettings, Relay)> { + ) -> Option<(CustomProxy, Relay)> { let matcher = RelayMatcher { locations: ResolvedLocationConstraint::from_constraint( constraints.location.clone(), @@ -1236,11 +1245,7 @@ impl RelaySelector { } /// Picks a random bridge from a relay. - fn pick_random_bridge( - &self, - data: &BridgeEndpointData, - relay: &Relay, - ) -> Option<ProxySettings> { + fn pick_random_bridge(&self, data: &BridgeEndpointData, relay: &Relay) -> Option<CustomProxy> { if relay.endpoint_data != RelayEndpointData::Bridge { return None; } @@ -1254,11 +1259,7 @@ impl RelaySelector { shadowsocks_endpoint.port, shadowsocks_endpoint.protocol ); - shadowsocks_endpoint.to_proxy_settings( - relay.ipv4_addr_in.into(), - #[cfg(target_os = "linux")] - mullvad_types::TUNNEL_FWMARK, - ) + shadowsocks_endpoint.to_proxy_settings(relay.ipv4_addr_in.into()) }) } @@ -1275,12 +1276,12 @@ impl RelaySelector { #[derive(Debug)] pub enum SelectedBridge { Normal(NormalSelectedBridge), - Custom(ProxySettings), + Custom(CustomProxy), } #[derive(Debug)] pub struct NormalSelectedBridge { - pub settings: ProxySettings, + pub settings: CustomProxy, pub relay: Relay, } diff --git a/mullvad-types/src/access_method.rs b/mullvad-types/src/access_method.rs index 0c697acbbf..73ab671c9c 100644 --- a/mullvad-types/src/access_method.rs +++ b/mullvad-types/src/access_method.rs @@ -1,10 +1,9 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; -use std::net::SocketAddr; -use talpid_types::net::{Endpoint, TransportProtocol}; +use talpid_types::net::proxy::{CustomProxy, Shadowsocks, Socks5Local, Socks5Remote}; -/// Daemon settings for API access methods. +/// Dttings for API access methods. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Settings { pub access_method_settings: Vec<AccessMethodSetting>, @@ -136,7 +135,7 @@ impl std::fmt::Display for Id { #[serde(rename_all = "snake_case")] pub enum AccessMethod { BuiltIn(BuiltInAccessMethod), - Custom(CustomAccessMethod), + Custom(CustomProxy), } impl AccessMethodSetting { @@ -179,7 +178,7 @@ impl AccessMethodSetting { self.enabled } - pub fn as_custom(&self) -> Option<&CustomAccessMethod> { + pub fn as_custom(&self) -> Option<&CustomProxy> { self.access_method.as_custom() } @@ -206,53 +205,8 @@ pub enum BuiltInAccessMethod { Bridge, } -/// Custom access method datastructure. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "snake_case")] -pub enum CustomAccessMethod { - Shadowsocks(Shadowsocks), - Socks5(Socks5), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "snake_case")] -pub enum Socks5 { - Local(Socks5Local), - Remote(Socks5Remote), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct Shadowsocks { - pub peer: SocketAddr, - pub password: String, - /// One of [`shadowsocks_ciphers`]. - /// Gets validated at a later stage. Is assumed to be valid. - /// - /// shadowsocks_ciphers: talpid_types::net::openvpn::SHADOWSOCKS_CIPHERS - pub cipher: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct Socks5Local { - pub remote_endpoint: Endpoint, - /// Port on localhost where the SOCKS5-proxy listens to. - pub local_port: u16, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct Socks5Remote { - pub peer: SocketAddr, - pub authentication: Option<SocksAuth>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct SocksAuth { - pub username: String, - pub password: String, -} - impl AccessMethod { - pub fn as_custom(&self) -> Option<&CustomAccessMethod> { + pub fn as_custom(&self) -> Option<&CustomProxy> { match self { AccessMethod::BuiltIn(_) => None, AccessMethod::Custom(access_method) => Some(access_method), @@ -269,98 +223,32 @@ impl BuiltInAccessMethod { } } -impl Shadowsocks { - pub fn new<I: Into<SocketAddr>>(peer: I, cipher: String, password: String) -> Self { - Shadowsocks { - peer: peer.into(), - password, - cipher, - } - } -} - -impl Socks5Local { - pub fn new<I: Into<SocketAddr>>(remote_peer: I, local_port: u16) -> Self { - let transport_protocol = TransportProtocol::Tcp; - Self::new_with_transport_protocol(remote_peer, local_port, transport_protocol) - } - - pub fn new_with_transport_protocol<I: Into<SocketAddr>>( - remote_peer: I, - local_port: u16, - transport_protocol: TransportProtocol, - ) -> Self { - let remote_endpoint = Endpoint::from_socket_address(remote_peer.into(), transport_protocol); - Self { - remote_endpoint, - local_port, - } - } -} - -impl Socks5Remote { - pub fn new<I: Into<SocketAddr>>(peer: I) -> Self { - Self { - peer: peer.into(), - authentication: None, - } - } - - pub fn new_with_authentication<I: Into<SocketAddr>>( - peer: I, - authentication: SocksAuth, - ) -> Self { - Self { - peer: peer.into(), - authentication: Some(authentication), - } - } -} - impl From<BuiltInAccessMethod> for AccessMethod { fn from(value: BuiltInAccessMethod) -> Self { AccessMethod::BuiltIn(value) } } -impl From<CustomAccessMethod> for AccessMethod { - fn from(value: CustomAccessMethod) -> Self { +impl From<CustomProxy> for AccessMethod { + fn from(value: CustomProxy) -> Self { AccessMethod::Custom(value) } } -impl From<Shadowsocks> for AccessMethod { - fn from(value: Shadowsocks) -> Self { - CustomAccessMethod::Shadowsocks(value).into() - } -} - -impl From<Socks5> for AccessMethod { - fn from(value: Socks5) -> Self { - AccessMethod::from(CustomAccessMethod::Socks5(value)) - } -} - impl From<Socks5Remote> for AccessMethod { fn from(value: Socks5Remote) -> Self { - Socks5::Remote(value).into() + CustomProxy::Socks5Remote(value).into() } } impl From<Socks5Local> for AccessMethod { fn from(value: Socks5Local) -> Self { - Socks5::Local(value).into() - } -} - -impl From<Socks5Remote> for Socks5 { - fn from(value: Socks5Remote) -> Self { - Socks5::Remote(value) + CustomProxy::Socks5Local(value).into() } } -impl From<Socks5Local> for Socks5 { - fn from(value: Socks5Local) -> Self { - Socks5::Local(value) +impl From<Shadowsocks> for AccessMethod { + fn from(value: Shadowsocks) -> Self { + CustomProxy::Shadowsocks(value).into() } } diff --git a/mullvad-types/src/custom_tunnel.rs b/mullvad-types/src/custom_tunnel.rs index 9b6ab9bff3..8fdf361447 100644 --- a/mullvad-types/src/custom_tunnel.rs +++ b/mullvad-types/src/custom_tunnel.rs @@ -6,7 +6,7 @@ use std::{ fmt, io, net::{IpAddr, SocketAddr, ToSocketAddrs}, }; -use talpid_types::net::{openvpn, wireguard, Endpoint, TunnelParameters}; +use talpid_types::net::{openvpn, proxy::CustomProxy, wireguard, Endpoint, TunnelParameters}; #[derive(err_derive::Error, Debug)] pub enum Error { @@ -42,7 +42,7 @@ impl CustomTunnelEndpoint { pub fn to_tunnel_parameters( &self, tunnel_options: TunnelOptions, - proxy: Option<openvpn::ProxySettings>, + proxy: Option<CustomProxy>, ) -> Result<TunnelParameters, Error> { let ip = resolve_to_ip(&self.host)?; let mut config = self.config.clone(); diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index 6645fb493d..0669d8bd8b 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -16,7 +16,7 @@ use std::{ net::{Ipv4Addr, Ipv6Addr}, str::FromStr, }; -use talpid_types::net::{openvpn::ProxySettings, IpVersion, TransportProtocol, TunnelType}; +use talpid_types::net::{proxy::CustomProxy, IpVersion, TransportProtocol, TunnelType}; pub trait Match<T> { fn matches(&self, other: &T) -> bool; @@ -831,14 +831,53 @@ struct Port { value: i32, } +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum BridgeType { + /// Let the relay selection algorithm decide on bridges, based on the relay list + /// and normal bridge constraints. + #[default] + Normal, + /// Use custom bridge configuration. + Custom, +} + +impl fmt::Display for BridgeType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + BridgeType::Normal => f.write_str("normal"), + BridgeType::Custom => f.write_str("custom"), + } + } +} + +#[derive(err_derive::Error, Debug)] +#[error(display = "Missing custom bridge settings")] +pub struct MissingCustomBridgeSettings(()); + /// Specifies a specific endpoint or [`BridgeConstraints`] to use when `mullvad-daemon` selects a /// bridge server. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] -pub enum BridgeSettings { - /// Let the relay selection algorithm decide on bridges, based on the relay list. - Normal(BridgeConstraints), - Custom(ProxySettings), +pub struct BridgeSettings { + pub bridge_type: BridgeType, + pub normal: BridgeConstraints, + pub custom: Option<CustomProxy>, +} + +pub enum ResolvedBridgeSettings<'a> { + Normal(&'a BridgeConstraints), + Custom(&'a CustomProxy), +} + +impl BridgeSettings { + pub fn resolve(&self) -> Result<ResolvedBridgeSettings<'_>, MissingCustomBridgeSettings> { + match (self.bridge_type, &self.custom) { + (BridgeType::Normal, _) => Ok(ResolvedBridgeSettings::Normal(&self.normal)), + (BridgeType::Custom, Some(custom)) => Ok(ResolvedBridgeSettings::Custom(custom)), + (BridgeType::Custom, None) => Err(MissingCustomBridgeSettings(())), + } + } } #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 0edf4f5752..55649b38c1 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -4,7 +4,7 @@ use jnix::IntoJava; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use talpid_types::net::{ - openvpn::{ProxySettings, ShadowsocksProxySettings}, + proxy::{CustomProxy, Shadowsocks}, wireguard, TransportProtocol, }; @@ -207,17 +207,11 @@ pub struct ShadowsocksEndpointData { } impl ShadowsocksEndpointData { - pub fn to_proxy_settings( - &self, - addr: IpAddr, - #[cfg(target_os = "linux")] fwmark: u32, - ) -> ProxySettings { - ProxySettings::Shadowsocks(ShadowsocksProxySettings { - peer: SocketAddr::new(addr, self.port), + pub fn to_proxy_settings(&self, addr: IpAddr) -> CustomProxy { + CustomProxy::Shadowsocks(Shadowsocks { + endpoint: SocketAddr::new(addr, self.port), password: self.password.clone(), cipher: self.cipher.clone(), - #[cfg(target_os = "linux")] - fwmark: Some(fwmark), }) } } diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index 612e353487..e6886c9e3c 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -2,8 +2,8 @@ use crate::{ access_method, custom_list::CustomListsSettings, relay_constraints::{ - BridgeConstraints, BridgeSettings, BridgeState, Constraint, GeographicLocationConstraint, - LocationConstraint, ObfuscationSettings, RelayConstraints, RelayOverride, RelaySettings, + BridgeSettings, BridgeState, Constraint, GeographicLocationConstraint, LocationConstraint, + ObfuscationSettings, RelayConstraints, RelayOverride, RelaySettings, RelaySettingsFormatter, SelectedObfuscation, WireguardConstraints, }, wireguard, @@ -32,6 +32,7 @@ pub enum SettingsVersion { V5 = 5, V6 = 6, V7 = 7, + V8 = 8, } impl<'de> Deserialize<'de> for SettingsVersion { @@ -46,6 +47,7 @@ impl<'de> Deserialize<'de> for SettingsVersion { v if v == SettingsVersion::V5 as u32 => Ok(SettingsVersion::V5), v if v == SettingsVersion::V6 as u32 => Ok(SettingsVersion::V6), v if v == SettingsVersion::V7 as u32 => Ok(SettingsVersion::V7), + v if v == SettingsVersion::V8 as u32 => Ok(SettingsVersion::V8), v => Err(serde::de::Error::custom(format!( "{v} is not a valid SettingsVersion" ))), @@ -128,7 +130,7 @@ impl Default for Settings { }, ..Default::default() }), - bridge_settings: BridgeSettings::Normal(BridgeConstraints::default()), + bridge_settings: BridgeSettings::default(), obfuscation_settings: ObfuscationSettings { selected_obfuscation: SelectedObfuscation::Off, ..Default::default() diff --git a/talpid-core/src/firewall/linux.rs b/talpid-core/src/firewall/linux.rs index 6cbff79b32..79ac00fc8d 100644 --- a/talpid-core/src/firewall/linux.rs +++ b/talpid-core/src/firewall/linux.rs @@ -567,6 +567,7 @@ impl<'a> PolicyBatch<'a> { self.add_allow_tunnel_endpoint_rules(peer_endpoint, fwmark); self.add_allow_dns_rules(tunnel, dns_servers, TransportProtocol::Udp)?; self.add_allow_dns_rules(tunnel, dns_servers, TransportProtocol::Tcp)?; + // Important to block DNS *before* we allow the tunnel and allow LAN. So DNS // can't leak to the wrong IPs in the tunnel or on the LAN. self.add_drop_dns_rule(); @@ -607,10 +608,10 @@ impl<'a> PolicyBatch<'a> { Ok(()) } - fn add_allow_tunnel_endpoint_rules(&mut self, endpoint: &Endpoint, fwmark: u32) { + fn add_allow_tunnel_endpoint_rules(&mut self, endpoint: &AllowedEndpoint, fwmark: u32) { let mut prerouting_rule = Rule::new(&self.prerouting_chain); // Mark incoming traffic from endpoint with fwmark - check_endpoint(&mut prerouting_rule, End::Src, endpoint); + check_endpoint(&mut prerouting_rule, End::Src, &endpoint.endpoint); prerouting_rule.add_expr(&nft_expr!(immediate data fwmark)); prerouting_rule.add_expr(&nft_expr!(meta mark set)); @@ -621,7 +622,7 @@ impl<'a> PolicyBatch<'a> { self.batch.add(&prerouting_rule, nftnl::MsgType::Add); let mut in_rule = Rule::new(&self.in_chain); - check_endpoint(&mut in_rule, End::Src, endpoint); + check_endpoint(&mut in_rule, End::Src, &endpoint.endpoint); // Allow all incoming traffic from established connections to the endpoint let allowed_states = nftnl::expr::ct::States::ESTABLISHED.bits(); @@ -637,12 +638,24 @@ impl<'a> PolicyBatch<'a> { // Allow any traffic to the endpoint which is marked with fwmark let mut out_rule = Rule::new(&self.out_chain); - check_endpoint(&mut out_rule, End::Dst, endpoint); + check_endpoint(&mut out_rule, End::Dst, &endpoint.endpoint); out_rule.add_expr(&nft_expr!(meta mark)); out_rule.add_expr(&nft_expr!(cmp == fwmark)); add_verdict(&mut out_rule, &Verdict::Accept); self.batch.add(&out_rule, nftnl::MsgType::Add); + + // Used for local custom bridge, allows some local socks5 proxy to send traffic to the + // endpoint + if endpoint.clients.allow_all() { + let mut rule = Rule::new(&self.mangle_chain); + check_endpoint(&mut rule, End::Dst, &endpoint.endpoint); + rule.add_expr(&nft_expr!(immediate data split_tunnel::MARK)); + rule.add_expr(&nft_expr!(ct mark set)); + rule.add_expr(&nft_expr!(immediate data fwmark)); + rule.add_expr(&nft_expr!(meta mark set)); + self.batch.add(&rule, nftnl::MsgType::Add); + } } /// Adds firewall rules allow traffic to flow to the API. Allows the app to reach the API in diff --git a/talpid-core/src/firewall/macos.rs b/talpid-core/src/firewall/macos.rs index 54f72a79c2..27fc6200fa 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos.rs @@ -121,7 +121,7 @@ impl Firewall { allowed_endpoint, allowed_tunnel_traffic, } => { - let mut rules = vec![self.get_allow_relay_rule(*peer_endpoint)?]; + let mut rules = vec![self.get_allow_relay_rule(peer_endpoint)?]; rules.push(self.get_allowed_endpoint_rule(allowed_endpoint)?); // Important to block DNS after allow relay rule (so the relay can operate @@ -151,7 +151,7 @@ impl Firewall { rules.append(&mut self.get_allow_dns_rules_when_connected(tunnel, *server)?); } - rules.push(self.get_allow_relay_rule(*peer_endpoint)?); + rules.push(self.get_allow_relay_rule(peer_endpoint)?); // Important to block DNS *before* we allow the tunnel and allow LAN. So DNS // can't leak to the wrong IPs in the tunnel or on the LAN. @@ -273,18 +273,26 @@ impl Firewall { Ok(rules) } - fn get_allow_relay_rule(&self, relay_endpoint: net::Endpoint) -> Result<pfctl::FilterRule> { - let pfctl_proto = as_pfctl_proto(relay_endpoint.protocol); + fn get_allow_relay_rule( + &self, + relay_endpoint: &net::AllowedEndpoint, + ) -> Result<pfctl::FilterRule> { + let pfctl_proto = as_pfctl_proto(relay_endpoint.endpoint.protocol); - self.create_rule_builder(FilterRuleAction::Pass) + let mut builder = self.create_rule_builder(FilterRuleAction::Pass); + builder .direction(pfctl::Direction::Out) - .to(relay_endpoint.address) + .to(relay_endpoint.endpoint.address) .proto(pfctl_proto) .keep_state(pfctl::StatePolicy::Keep) .tcp_flags(Self::get_tcp_flags()) - .user(Uid::from(super::ROOT_UID)) - .quick(true) - .build() + .quick(true); + + if !relay_endpoint.clients.allow_all() { + builder.user(Uid::from(super::ROOT_UID)); + } + + builder.build() } /// Produces a rule that allows traffic to flow to the API. Allows the app (or other apps if configured) diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 5a35b1a7ae..a0afb39f5d 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -1,12 +1,10 @@ use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; use once_cell::sync::Lazy; -#[cfg(windows)] -use std::path::PathBuf; use std::{ fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; -use talpid_types::net::{AllowedEndpoint, AllowedTunnelTraffic, Endpoint}; +use talpid_types::net::{AllowedEndpoint, AllowedTunnelTraffic}; #[cfg(target_os = "macos")] #[path = "macos.rs"] @@ -116,7 +114,7 @@ pub enum FirewallPolicy { /// Allow traffic only to server Connecting { /// The peer endpoint that should be allowed. - peer_endpoint: Endpoint, + peer_endpoint: AllowedEndpoint, /// Metadata about the tunnel and tunnel interface. tunnel: Option<crate::tunnel::TunnelMetadata>, /// Flag setting if communication with LAN networks should be possible. @@ -125,15 +123,12 @@ pub enum FirewallPolicy { allowed_endpoint: AllowedEndpoint, /// Networks for which to permit in-tunnel traffic. allowed_tunnel_traffic: AllowedTunnelTraffic, - /// A process that is allowed to send packets to the relay. - #[cfg(windows)] - relay_client: PathBuf, }, /// Allow traffic only to server and over tunnel interface Connected { /// The peer endpoint that should be allowed. - peer_endpoint: Endpoint, + peer_endpoint: AllowedEndpoint, /// Metadata about the tunnel and tunnel interface. tunnel: crate::tunnel::TunnelMetadata, /// Flag setting if communication with LAN networks should be possible. @@ -141,9 +136,6 @@ pub enum FirewallPolicy { /// Servers that are allowed to respond to DNS requests. #[cfg(not(target_os = "android"))] dns_servers: Vec<IpAddr>, - /// A process that is allowed to send packets to the relay. - #[cfg(windows)] - relay_client: PathBuf, }, /// Block all network traffic in and out from the computer. diff --git a/talpid-core/src/firewall/windows.rs b/talpid-core/src/firewall/windows.rs index ed3e0a19fe..f26102820a 100644 --- a/talpid-core/src/firewall/windows.rs +++ b/talpid-core/src/firewall/windows.rs @@ -1,11 +1,11 @@ use crate::tunnel::TunnelMetadata; -use std::{ffi::CStr, io, net::IpAddr, path::Path, ptr}; +use std::{ffi::CStr, io, net::IpAddr, ptr}; use self::winfw::*; use super::{FirewallArguments, FirewallPolicy, InitialFirewallState}; use talpid_types::{ - net::{AllowedEndpoint, AllowedTunnelTraffic, Endpoint}, + net::{AllowedEndpoint, AllowedTunnelTraffic}, tunnel::FirewallPolicyError, }; use widestring::WideCString; @@ -99,7 +99,6 @@ impl Firewall { allow_lan, allowed_endpoint, allowed_tunnel_traffic, - relay_client, } => { let cfg = &WinFwSettings::new(allow_lan); @@ -109,7 +108,6 @@ impl Firewall { &tunnel, &WinFwAllowedEndpointContainer::from(allowed_endpoint).as_endpoint(), &allowed_tunnel_traffic, - &relay_client, ) } FirewallPolicy::Connected { @@ -117,10 +115,9 @@ impl Firewall { tunnel, allow_lan, dns_servers, - relay_client, } => { let cfg = &WinFwSettings::new(allow_lan); - self.set_connected_state(&peer_endpoint, cfg, &tunnel, &dns_servers, &relay_client) + self.set_connected_state(&peer_endpoint, cfg, &tunnel, &dns_servers) } FirewallPolicy::Blocked { allow_lan, @@ -142,22 +139,33 @@ impl Firewall { fn set_connecting_state( &mut self, - endpoint: &Endpoint, + endpoint: &AllowedEndpoint, winfw_settings: &WinFwSettings, tunnel_metadata: &Option<TunnelMetadata>, allowed_endpoint: &WinFwAllowedEndpoint<'_>, allowed_tunnel_traffic: &AllowedTunnelTraffic, - relay_client: &Path, ) -> Result<(), Error> { log::trace!("Applying 'connecting' firewall policy"); - let ip_str = widestring_ip(endpoint.address.ip()); + let ip_str = widestring_ip(endpoint.endpoint.address.ip()); let winfw_relay = WinFwEndpoint { ip: ip_str.as_ptr(), - port: endpoint.address.port(), - protocol: WinFwProt::from(endpoint.protocol), + port: endpoint.endpoint.address.port(), + protocol: WinFwProt::from(endpoint.endpoint.protocol), }; - let relay_client = WideCString::from_os_str_truncate(relay_client); + // SAFETY: `endpoint1_ip`, `endpoint2_ip`, `endpoint1`, `endpoint2`, `relay_client_wstrs` must not be dropped + // until `WinFw_ApplyPolicyConnecting` has returned. + + let relay_client_wstrs: Vec<_> = endpoint + .clients + .iter() + .map(WideCString::from_os_str_truncate) + .collect(); + let relay_client_wstr_ptrs: Vec<*const u16> = relay_client_wstrs + .iter() + .map(|wstr| wstr.as_ptr()) + .collect(); + let relay_client_wstr_ptrs_len = relay_client_wstr_ptrs.len(); let interface_wstr = tunnel_metadata .as_ref() @@ -168,8 +176,6 @@ impl Firewall { ptr::null() }; - // SAFETY: `endpoint1_ip`, `endpoint2_ip`, `endpoint1` and `endpoint2` must not be dropped - // until `WinFw_ApplyPolicyConnecting` has returned. let mut endpoint1_ip = WideCString::new(); let mut endpoint2_ip = WideCString::new(); let (endpoint1, endpoint2) = match allowed_tunnel_traffic { @@ -218,7 +224,8 @@ impl Firewall { WinFw_ApplyPolicyConnecting( winfw_settings, &winfw_relay, - relay_client.as_ptr(), + relay_client_wstr_ptrs.as_ptr(), + relay_client_wstr_ptrs_len, interface_wstr_ptr, allowed_endpoint, &allowed_tunnel_traffic, @@ -235,19 +242,19 @@ impl Firewall { drop(endpoint1); #[allow(clippy::drop_non_drop)] drop(endpoint2); + drop(relay_client_wstrs); res } fn set_connected_state( &mut self, - endpoint: &Endpoint, + endpoint: &AllowedEndpoint, winfw_settings: &WinFwSettings, tunnel_metadata: &TunnelMetadata, dns_servers: &[IpAddr], - relay_client: &Path, ) -> Result<(), Error> { log::trace!("Applying 'connected' firewall policy"); - let ip_str = widestring_ip(endpoint.address.ip()); + let ip_str = widestring_ip(endpoint.endpoint.address.ip()); let v4_gateway = widestring_ip(tunnel_metadata.ipv4_gateway.into()); let v6_gateway = tunnel_metadata .ipv6_gateway @@ -258,8 +265,8 @@ impl Firewall { // ip_str, gateway_str and tunnel_alias have to outlive winfw_relay let winfw_relay = WinFwEndpoint { ip: ip_str.as_ptr(), - port: endpoint.address.port(), - protocol: WinFwProt::from(endpoint.protocol), + port: endpoint.endpoint.address.port(), + protocol: WinFwProt::from(endpoint.endpoint.protocol), }; let v6_gateway_ptr = match &v6_gateway { @@ -267,17 +274,28 @@ impl Firewall { None => ptr::null(), }; - let relay_client = WideCString::from_os_str_truncate(relay_client); + // SAFETY: `relay_client_wstrs` must not be dropped until `WinFw_ApplyPolicyConnected` has returned. + let relay_client_wstrs: Vec<_> = endpoint + .clients + .iter() + .map(WideCString::from_os_str_truncate) + .collect(); + let relay_client_wstr_ptrs: Vec<*const u16> = relay_client_wstrs + .iter() + .map(|wstr| wstr.as_ptr()) + .collect(); + let relay_client_wstr_ptrs_len = relay_client_wstr_ptrs.len(); let dns_servers: Vec<WideCString> = dns_servers.iter().cloned().map(widestring_ip).collect(); let dns_servers: Vec<*const u16> = dns_servers.iter().map(|ip| ip.as_ptr()).collect(); - unsafe { + let result = unsafe { WinFw_ApplyPolicyConnected( winfw_settings, &winfw_relay, - relay_client.as_ptr(), + relay_client_wstr_ptrs.as_ptr(), + relay_client_wstr_ptrs_len, tunnel_alias.as_ptr(), v4_gateway.as_ptr(), v6_gateway_ptr, @@ -286,7 +304,12 @@ impl Firewall { ) .into_result() .map_err(Error::ApplyingConnectedPolicy) - } + }; + + // SAFETY: `relay_client_wstrs` holds memory pointed to by pointers used in C++ and must + // not be dropped until after `WinFw_ApplyPolicyConnected` has returned. + drop(relay_client_wstrs); + result } fn set_blocked_state( @@ -598,7 +621,8 @@ mod winfw { pub fn WinFw_ApplyPolicyConnecting( settings: &WinFwSettings, relay: &WinFwEndpoint, - relayClient: *const libc::wchar_t, + relayClient: *const *const libc::wchar_t, + relayClientLen: usize, tunnelIfaceAlias: *const libc::wchar_t, allowedEndpoint: *const WinFwAllowedEndpoint<'_>, allowedTunnelTraffic: &WinFwAllowedTunnelTraffic, @@ -608,7 +632,8 @@ mod winfw { pub fn WinFw_ApplyPolicyConnected( settings: &WinFwSettings, relay: &WinFwEndpoint, - relayClient: *const libc::wchar_t, + relayClient: *const *const libc::wchar_t, + relayClientLen: usize, tunnelIfaceAlias: *const libc::wchar_t, v4Gateway: *const libc::wchar_t, v6Gateway: *const libc::wchar_t, diff --git a/talpid-core/src/tunnel/mod.rs b/talpid-core/src/tunnel/mod.rs index 4ab6bb8d3d..3d953e965a 100644 --- a/talpid-core/src/tunnel/mod.rs +++ b/talpid-core/src/tunnel/mod.rs @@ -4,7 +4,7 @@ use futures::channel::oneshot; use std::path; #[cfg(not(target_os = "android"))] use talpid_openvpn; -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(not(target_os = "android"))] use talpid_routing::RouteManagerHandle; pub use talpid_tunnel::{TunnelArgs, TunnelEvent, TunnelMetadata}; #[cfg(not(target_os = "android"))] @@ -91,7 +91,6 @@ impl TunnelMonitor { args.resource_dir, args.on_event, args.tunnel_close_rx, - #[cfg(target_os = "linux")] args.route_manager, )), #[cfg(target_os = "android")] @@ -104,20 +103,23 @@ impl TunnelMonitor { } /// Returns a path to an executable that communicates with relay servers. + /// Returns `None` if the executable is unknown. #[cfg(windows)] - pub fn get_relay_client(resource_dir: &path::Path, params: &TunnelParameters) -> path::PathBuf { + pub fn get_relay_client( + resource_dir: &path::Path, + params: &TunnelParameters, + ) -> Option<path::PathBuf> { + use talpid_types::net::proxy::CustomProxy; + let resource_dir = resource_dir.to_path_buf(); - let process_string = match params { - TunnelParameters::OpenVpn(params) => { - if let Some(openvpn_types::ProxySettings::Shadowsocks(..)) = ¶ms.proxy { - return std::env::current_exe().unwrap(); - } else { - "openvpn.exe" - } - } - _ => return std::env::current_exe().unwrap(), - }; - resource_dir.join(process_string) + match params { + TunnelParameters::OpenVpn(params) => match ¶ms.proxy { + Some(CustomProxy::Shadowsocks(_)) => Some(std::env::current_exe().unwrap()), + Some(CustomProxy::Socks5Local(_)) => None, + Some(CustomProxy::Socks5Remote(_)) | None => Some(resource_dir.join("openvpn.exe")), + }, + _ => Some(std::env::current_exe().unwrap()), + } } fn start_wireguard_tunnel<L>( @@ -211,7 +213,7 @@ impl TunnelMonitor { resource_dir: &path::Path, on_event: L, tunnel_close_rx: oneshot::Receiver<()>, - #[cfg(target_os = "linux")] route_manager: RouteManagerHandle, + route_manager: RouteManagerHandle, ) -> Result<Self> where L: (Fn(TunnelEvent) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>) @@ -224,7 +226,6 @@ impl TunnelMonitor { config, log, resource_dir, - #[cfg(target_os = "linux")] route_manager, ) .await?; diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 21375f056b..6f32b796c3 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -14,7 +14,7 @@ use futures::{ }; use std::net::IpAddr; use talpid_types::{ - net::TunnelParameters, + net::{AllowedClients, AllowedEndpoint, TunnelParameters}, tunnel::{ErrorStateCause, FirewallPolicyError}, BoxedError, ErrorExt, }; @@ -130,17 +130,34 @@ impl ConnectedState { } fn get_firewall_policy(&self, shared_values: &SharedTunnelStateValues) -> FirewallPolicy { + let endpoint = self.tunnel_parameters.get_next_hop_endpoint(); + + #[cfg(target_os = "windows")] + let clients = AllowedClients::from( + TunnelMonitor::get_relay_client(&shared_values.resource_dir, &self.tunnel_parameters) + .into_iter() + .collect::<Vec<_>>(), + ); + + #[cfg(not(target_os = "windows"))] + let clients = if self + .tunnel_parameters + .get_openvpn_local_proxy_settings() + .is_some() + { + AllowedClients::All + } else { + AllowedClients::Root + }; + + let peer_endpoint = AllowedEndpoint { endpoint, clients }; + FirewallPolicy::Connected { - peer_endpoint: self.tunnel_parameters.get_next_hop_endpoint(), + peer_endpoint, tunnel: self.metadata.clone(), allow_lan: shared_values.allow_lan, #[cfg(not(target_os = "android"))] dns_servers: self.get_dns_servers(shared_values), - #[cfg(windows)] - relay_client: TunnelMonitor::get_relay_client( - &shared_values.resource_dir, - &self.tunnel_parameters, - ), } } diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index d7d93da9d4..e49e5b69d8 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -21,7 +21,7 @@ use std::{ use talpid_routing::RouteManager; use talpid_tunnel::{tun_provider::TunProvider, TunnelArgs, TunnelEvent, TunnelMetadata}; use talpid_types::{ - net::{AllowedTunnelTraffic, TunnelParameters}, + net::{AllowedClients, AllowedEndpoint, AllowedTunnelTraffic, TunnelParameters}, tunnel::{ErrorStateCause, FirewallPolicyError}, ErrorExt, }; @@ -140,7 +140,23 @@ impl ConnectingState { #[cfg(target_os = "linux")] shared_values.disable_connectivity_check(); - let peer_endpoint = params.get_next_hop_endpoint(); + let endpoint = params.get_next_hop_endpoint(); + + #[cfg(target_os = "windows")] + let clients = AllowedClients::from( + TunnelMonitor::get_relay_client(&shared_values.resource_dir, params) + .into_iter() + .collect::<Vec<_>>(), + ); + + #[cfg(not(target_os = "windows"))] + let clients = if params.get_openvpn_local_proxy_settings().is_some() { + AllowedClients::All + } else { + AllowedClients::Root + }; + + let peer_endpoint = AllowedEndpoint { endpoint, clients }; let policy = FirewallPolicy::Connecting { peer_endpoint, @@ -148,8 +164,6 @@ impl ConnectingState { allow_lan: shared_values.allow_lan, allowed_endpoint: shared_values.allowed_endpoint.clone(), allowed_tunnel_traffic, - #[cfg(windows)] - relay_client: TunnelMonitor::get_relay_client(&shared_values.resource_dir, params), }; shared_values .firewall diff --git a/talpid-openvpn/src/lib.rs b/talpid-openvpn/src/lib.rs index 9fd0c317ef..a0c4651d13 100644 --- a/talpid-openvpn/src/lib.rs +++ b/talpid-openvpn/src/lib.rs @@ -3,7 +3,7 @@ #![deny(missing_docs)] #![deny(rust_2018_idioms)] -use crate::proxy::{ProxyMonitor, ProxyResourceData}; +use crate::proxy::ProxyMonitor; #[cfg(windows)] use once_cell::sync::Lazy; use process::openvpn::{OpenVpnCommand, OpenVpnProcHandle}; @@ -21,7 +21,10 @@ use std::{ #[cfg(target_os = "linux")] use talpid_routing::{self, RequiredRoute}; use talpid_tunnel::TunnelEvent; -use talpid_types::{net::openvpn, ErrorExt}; +use talpid_types::{ + net::{openvpn, proxy::CustomProxy}, + ErrorExt, +}; use tokio::task; #[cfg(windows)] @@ -226,7 +229,7 @@ impl OpenVpnMonitor<OpenVpnCommand> { params: &openvpn::TunnelParameters, log_path: Option<PathBuf>, resource_dir: &Path, - #[cfg(target_os = "linux")] route_manager: talpid_routing::RouteManagerHandle, + route_manager: talpid_routing::RouteManagerHandle, ) -> Result<Self> where L: (Fn(TunnelEvent) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>) @@ -242,19 +245,12 @@ impl OpenVpnMonitor<OpenVpnCommand> { let user_pass_file_path = user_pass_file.to_path_buf(); let proxy_auth_file_path = proxy_auth_file.as_ref().map(|file| file.to_path_buf()); - let log_dir = log_path.as_ref().map(|log_path| { - log_path - .parent() - .expect("log_path has no parent") - .to_path_buf() - }); - - let proxy_resources = proxy::ProxyResourceData { - resource_dir: resource_dir.to_path_buf(), - log_dir, - }; - - let proxy_monitor = Self::start_proxy(¶ms.proxy, &proxy_resources).await?; + let proxy_monitor = Self::start_proxy( + ¶ms.proxy, + #[cfg(target_os = "linux")] + params.fwmark, + ) + .await?; #[cfg(windows)] let wintun = Self::new_wintun_context(params, resource_dir)?; @@ -295,7 +291,7 @@ impl OpenVpnMonitor<OpenVpnCommand> { user_pass_file_path: user_pass_file_path.clone(), proxy_auth_file_path: proxy_auth_file_path.clone(), abort_server_tx: event_server_abort_tx, - #[cfg(target_os = "linux")] + proxy: params.proxy.clone(), route_manager_handle: route_manager, #[cfg(target_os = "linux")] ipv6_enabled, @@ -528,9 +524,9 @@ impl<C: OpenVpnBuilder + Send + 'static> OpenVpnMonitor<C> { } fn create_proxy_auth_file( - proxy_settings: &Option<openvpn::ProxySettings>, + proxy_settings: &Option<CustomProxy>, ) -> std::result::Result<Option<mktemp::TempFile>, io::Error> { - if let Some(openvpn::ProxySettings::Remote(ref remote_proxy)) = proxy_settings { + if let Some(CustomProxy::Socks5Remote(ref remote_proxy)) = proxy_settings { if let Some(ref proxy_auth) = remote_proxy.auth { return Ok(Some(Self::create_credentials_file( &proxy_auth.username, @@ -543,13 +539,17 @@ impl<C: OpenVpnBuilder + Send + 'static> OpenVpnMonitor<C> { /// Starts a proxy service, as applicable. async fn start_proxy( - proxy_settings: &Option<openvpn::ProxySettings>, - proxy_resources: &ProxyResourceData, + proxy_settings: &Option<CustomProxy>, + #[cfg(target_os = "linux")] fwmark: u32, ) -> Result<Option<Box<dyn ProxyMonitor>>> { if let Some(ref settings) = proxy_settings { - let proxy_monitor = proxy::start_proxy(settings, proxy_resources) - .await - .map_err(Error::ProxyError)?; + let proxy_monitor = proxy::start_proxy( + settings, + #[cfg(target_os = "linux")] + fwmark, + ) + .await + .map_err(Error::ProxyError)?; return Ok(Some(proxy_monitor)); } Ok(None) @@ -751,12 +751,12 @@ mod event_server { use futures::stream::TryStreamExt; use parity_tokio_ipc::Endpoint as IpcEndpoint; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, pin::Pin, task::{Context, Poll}, }; use talpid_tunnel::TunnelMetadata; - #[cfg(any(target_os = "linux", windows))] + use talpid_types::net::proxy::CustomProxy; use talpid_types::ErrorExt; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tonic::{ @@ -798,7 +798,7 @@ mod event_server { pub user_pass_file_path: super::PathBuf, pub proxy_auth_file_path: Option<super::PathBuf>, pub abort_server_tx: triggered::Trigger, - #[cfg(target_os = "linux")] + pub proxy: Option<CustomProxy>, pub route_manager_handle: talpid_routing::RouteManagerHandle, #[cfg(target_os = "linux")] pub ipv6_enabled: bool, @@ -838,28 +838,33 @@ mod event_server { let _ = tokio::fs::remove_file(file_path).await; } + let mut routes = HashSet::new(); + #[cfg(not(target_os = "linux"))] + if let Some(CustomProxy::Socks5Local(proxy_settings)) = &self.proxy { + let network = proxy_settings.remote_endpoint.address.ip().into(); + let node = talpid_routing::NetNode::DefaultNode; + let route = talpid_routing::RequiredRoute::new(network, node); + routes.insert(route); + } + let route_handle = self.route_manager_handle.clone(); + #[cfg(target_os = "linux")] { - let route_handle = self.route_manager_handle.clone(); let ipv6_enabled = self.ipv6_enabled; - let routes = super::extract_routes(&env) + if let Err(error) = route_handle.create_routing_rules(ipv6_enabled).await { + log::error!("{}", error.display_chain()); + return Err(tonic::Status::failed_precondition("Failed to add routes")); + } + + let extracted_routes = super::extract_routes(&env) .map_err(|err| { log::error!("{}", err.display_chain_with_msg("Failed to obtain routes")); tonic::Status::failed_precondition("Failed to obtain routes") })? .into_iter() - .filter(|route| route.prefix.is_ipv4() || ipv6_enabled) - .collect(); - - if let Err(error) = route_handle.add_routes(routes).await { - log::error!("{}", error.display_chain()); - return Err(tonic::Status::failed_precondition("Failed to add routes")); - } - if let Err(error) = route_handle.create_routing_rules(ipv6_enabled).await { - log::error!("{}", error.display_chain()); - return Err(tonic::Status::failed_precondition("Failed to add routes")); - } + .filter(|route| route.prefix.is_ipv4() || ipv6_enabled); + routes.extend(extracted_routes); } let metadata = Self::get_tunnel_metadata(&env)?; @@ -883,6 +888,11 @@ mod event_server { })?; } + if let Err(error) = route_handle.add_routes(routes).await { + log::error!("{}", error.display_chain()); + return Err(tonic::Status::failed_precondition("Failed to add routes")); + } + (self.on_event)(talpid_tunnel::TunnelEvent::Up(metadata)).await; Ok(Response::new(())) diff --git a/talpid-openvpn/src/process/openvpn.rs b/talpid-openvpn/src/process/openvpn.rs index 8d969d196c..84b9c88534 100644 --- a/talpid-openvpn/src/process/openvpn.rs +++ b/talpid-openvpn/src/process/openvpn.rs @@ -7,7 +7,7 @@ use std::{ process::Stdio, time::Duration, }; -use talpid_types::net; +use talpid_types::net::{self, proxy::CustomProxy}; static BASE_ARGUMENTS: &[&[&str]] = &[ &["--client"], @@ -66,7 +66,7 @@ pub struct OpenVpnCommand { plugin: Option<(PathBuf, Vec<String>)>, log: Option<PathBuf>, tunnel_options: net::openvpn::TunnelOptions, - proxy_settings: Option<net::openvpn::ProxySettings>, + proxy_settings: Option<CustomProxy>, tunnel_alias: Option<OsString>, enable_ipv6: bool, proxy_port: Option<u16>, @@ -182,7 +182,7 @@ impl OpenVpnCommand { } /// Sets the proxy settings. - pub fn proxy_settings(&mut self, proxy_settings: net::openvpn::ProxySettings) -> &mut Self { + pub fn proxy_settings(&mut self, proxy_settings: CustomProxy) -> &mut Self { self.proxy_settings = Some(proxy_settings); self } @@ -302,19 +302,19 @@ impl OpenVpnCommand { fn proxy_arguments(&self) -> Vec<String> { let mut args = vec![]; match self.proxy_settings { - Some(net::openvpn::ProxySettings::Local(ref local_proxy)) => { + Some(CustomProxy::Socks5Local(ref local_proxy)) => { args.push("--socks-proxy".to_owned()); args.push("127.0.0.1".to_owned()); - args.push(local_proxy.port.to_string()); + args.push(local_proxy.local_port.to_string()); args.push("--route".to_owned()); - args.push(local_proxy.peer.ip().to_string()); + args.push(local_proxy.remote_endpoint.address.ip().to_string()); args.push("255.255.255.255".to_owned()); args.push("net_gateway".to_owned()); } - Some(net::openvpn::ProxySettings::Remote(ref remote_proxy)) => { + Some(CustomProxy::Socks5Remote(ref remote_proxy)) => { args.push("--socks-proxy".to_owned()); - args.push(remote_proxy.address.ip().to_string()); - args.push(remote_proxy.address.port().to_string()); + args.push(remote_proxy.endpoint.ip().to_string()); + args.push(remote_proxy.endpoint.port().to_string()); if let Some(ref _auth) = remote_proxy.auth { if let Some(ref auth_file) = self.proxy_auth_path { @@ -325,11 +325,11 @@ impl OpenVpnCommand { } args.push("--route".to_owned()); - args.push(remote_proxy.address.ip().to_string()); + args.push(remote_proxy.endpoint.ip().to_string()); args.push("255.255.255.255".to_owned()); args.push("net_gateway".to_owned()); } - Some(net::openvpn::ProxySettings::Shadowsocks(ref ss)) => { + Some(CustomProxy::Shadowsocks(ref ss)) => { args.push("--socks-proxy".to_owned()); args.push("127.0.0.1".to_owned()); @@ -340,7 +340,7 @@ impl OpenVpnCommand { } args.push("--route".to_owned()); - args.push(ss.peer.ip().to_string()); + args.push(ss.endpoint.ip().to_string()); args.push("255.255.255.255".to_owned()); args.push("net_gateway".to_owned()); } diff --git a/talpid-openvpn/src/proxy/mod.rs b/talpid-openvpn/src/proxy/mod.rs index dd0e0b6cc9..6bc0fe92d5 100644 --- a/talpid-openvpn/src/proxy/mod.rs +++ b/talpid-openvpn/src/proxy/mod.rs @@ -3,8 +3,8 @@ mod shadowsocks; use self::shadowsocks::ShadowsocksProxyMonitor; use async_trait::async_trait; -use std::{fmt, io, path::PathBuf}; -use talpid_types::net::openvpn; +use std::{fmt, io}; +use talpid_types::net::proxy::CustomProxy; #[derive(err_derive::Error, Debug)] pub enum Error { @@ -39,33 +39,30 @@ pub trait ProxyMonitorCloseHandle: Send { fn close(self: Box<Self>) -> Result<()>; } -/// Variables that define the environment to help -/// proxy implementations find their way around. -/// TODO: Move struct to wider scope and use more generic name. -pub struct ProxyResourceData { - pub resource_dir: PathBuf, - pub log_dir: Option<PathBuf>, -} - pub async fn start_proxy( - settings: &openvpn::ProxySettings, - resource_data: &ProxyResourceData, + settings: &CustomProxy, + #[cfg(target_os = "linux")] fwmark: u32, ) -> Result<Box<dyn ProxyMonitor>> { match settings { - openvpn::ProxySettings::Local(local_settings) => { + CustomProxy::Socks5Local(local_settings) => { // These are generic proxy settings with the proxy client not managed by us. Ok(Box::new(noop::NoopProxyMonitor::start( - local_settings.port, + local_settings.local_port, )?)) } - openvpn::ProxySettings::Remote(remote_settings) => { + CustomProxy::Socks5Remote(remote_settings) => { // These are generic proxy settings with the proxy client not managed by us. Ok(Box::new(noop::NoopProxyMonitor::start( - remote_settings.address.port(), + remote_settings.endpoint.port(), )?)) } - openvpn::ProxySettings::Shadowsocks(ss_settings) => Ok(Box::new( - ShadowsocksProxyMonitor::start(ss_settings, resource_data).await?, + CustomProxy::Shadowsocks(ss_settings) => Ok(Box::new( + ShadowsocksProxyMonitor::start( + ss_settings, + #[cfg(target_os = "linux")] + fwmark, + ) + .await?, )), } } diff --git a/talpid-openvpn/src/proxy/shadowsocks.rs b/talpid-openvpn/src/proxy/shadowsocks.rs index 47fa0e939a..2b88f23068 100644 --- a/talpid-openvpn/src/proxy/shadowsocks.rs +++ b/talpid-openvpn/src/proxy/shadowsocks.rs @@ -16,8 +16,8 @@ use shadowsocks_service::{ }, }; -use super::{Error, ProxyMonitor, ProxyMonitorCloseHandle, ProxyResourceData}; -use talpid_types::{net::openvpn::ShadowsocksProxySettings, ErrorExt}; +use super::{Error, ProxyMonitor, ProxyMonitorCloseHandle}; +use talpid_types::{net::proxy::Shadowsocks, ErrorExt}; pub struct ShadowsocksProxyMonitor { port: u16, @@ -27,13 +27,22 @@ pub struct ShadowsocksProxyMonitor { impl ShadowsocksProxyMonitor { pub async fn start( - settings: &ShadowsocksProxySettings, - _resource_data: &ProxyResourceData, + settings: &Shadowsocks, + #[cfg(target_os = "linux")] fwmark: u32, ) -> super::Result<Self> { - Self::start_inner(settings).await.map_err(Error::Io) + Self::start_inner( + settings, + #[cfg(target_os = "linux")] + fwmark, + ) + .await + .map_err(Error::Io) } - async fn start_inner(settings: &ShadowsocksProxySettings) -> io::Result<Self> { + async fn start_inner( + settings: &Shadowsocks, + #[cfg(target_os = "linux")] fwmark: u32, + ) -> io::Result<Self> { let mut config = Config::new(ConfigType::Local); config.fast_open = true; @@ -50,7 +59,7 @@ impl ShadowsocksProxyMonitor { .push(LocalInstanceConfig::with_local_config(local)); let server = ServerConfig::new( - settings.peer, + settings.endpoint, settings.password.clone(), settings.cipher.parse().map_err(|_| { io::Error::new( @@ -66,7 +75,7 @@ impl ShadowsocksProxyMonitor { #[cfg(target_os = "linux")] { - config.outbound_fwmark = settings.fwmark; + config.outbound_fwmark = Some(fwmark); } let srv = local::Server::new(config).await?; diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs index de9c5c765d..c7147d00ce 100644 --- a/talpid-types/src/net/mod.rs +++ b/talpid-types/src/net/mod.rs @@ -10,6 +10,8 @@ use std::{ str::FromStr, }; +use self::proxy::{CustomProxy, Socks5Local}; + pub mod obfuscation; pub mod openvpn; pub mod proxy; @@ -31,7 +33,10 @@ impl TunnelParameters { tunnel_type: TunnelType::OpenVpn, quantum_resistant: false, endpoint: params.config.endpoint, - proxy: params.proxy.as_ref().map(|proxy| proxy.get_endpoint()), + proxy: params + .proxy + .as_ref() + .map(|proxy| proxy.get_remote_endpoint()), obfuscation: None, entry_endpoint: None, tunnel_interface: None, @@ -60,7 +65,7 @@ impl TunnelParameters { TunnelParameters::OpenVpn(params) => params .proxy .as_ref() - .map(|proxy| proxy.get_endpoint().endpoint) + .map(|proxy| proxy.get_remote_endpoint().endpoint) .unwrap_or(params.config.endpoint), TunnelParameters::Wireguard(params) => params .obfuscation @@ -93,6 +98,21 @@ impl TunnelParameters { TunnelParameters::Wireguard(params) => ¶ms.generic_options, } } + + pub fn get_openvpn_local_proxy_settings(&self) -> Option<&Socks5Local> { + match &self { + TunnelParameters::OpenVpn(params) => { + params + .proxy + .as_ref() + .and_then(|proxy_settings| match proxy_settings { + CustomProxy::Socks5Local(local_settings) => Some(local_settings), + _ => None, + }) + } + _ => None, + } + } } impl From<wireguard::TunnelParameters> for TunnelParameters { diff --git a/talpid-types/src/net/openvpn.rs b/talpid-types/src/net/openvpn.rs index 5968331b52..b2989bb851 100644 --- a/talpid-types/src/net/openvpn.rs +++ b/talpid-types/src/net/openvpn.rs @@ -1,9 +1,7 @@ -use crate::net::{ - proxy::{ProxyEndpoint, ProxyType}, - Endpoint, GenericTunnelOptions, TransportProtocol, -}; +use crate::net::{Endpoint, GenericTunnelOptions}; use serde::{Deserialize, Serialize}; -use std::net::SocketAddr; + +use super::proxy::CustomProxy; /// Information needed by `OpenVpnMonitor` to establish a tunnel connection. /// See [`crate::net::TunnelParameters`]. @@ -12,7 +10,7 @@ pub struct TunnelParameters { pub config: ConnectionConfig, pub options: TunnelOptions, pub generic_options: GenericTunnelOptions, - pub proxy: Option<ProxySettings>, + pub proxy: Option<CustomProxy>, #[cfg(target_os = "linux")] pub fwmark: u32, } @@ -45,156 +43,3 @@ pub struct TunnelOptions { /// as discussed [here](https://openvpn.net/archive/openvpn-users/2003-11/msg00154.html) pub mssfix: Option<u16>, } - -/// Proxy server options to be used by `OpenVpnMonitor` when starting a tunnel. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ProxySettings { - Local(LocalProxySettings), - Remote(RemoteProxySettings), - Shadowsocks(ShadowsocksProxySettings), -} - -impl ProxySettings { - pub fn get_endpoint(&self) -> ProxyEndpoint { - match self { - ProxySettings::Local(settings) => ProxyEndpoint { - endpoint: settings.get_endpoint(), - proxy_type: ProxyType::Custom, - }, - ProxySettings::Remote(settings) => ProxyEndpoint { - endpoint: settings.get_endpoint(), - proxy_type: ProxyType::Custom, - }, - ProxySettings::Shadowsocks(settings) => ProxyEndpoint { - endpoint: settings.get_endpoint(), - proxy_type: ProxyType::Shadowsocks, - }, - } - } -} - -/// Options for a generic proxy running on localhost. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct LocalProxySettings { - pub port: u16, - pub peer: SocketAddr, -} - -impl LocalProxySettings { - pub fn get_endpoint(&self) -> Endpoint { - Endpoint { - address: self.peer, - protocol: TransportProtocol::Tcp, - } - } -} - -/// Options for a generic proxy running on remote host. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct RemoteProxySettings { - pub address: SocketAddr, - pub auth: Option<ProxyAuth>, -} - -impl RemoteProxySettings { - pub fn get_endpoint(&self) -> Endpoint { - Endpoint { - address: self.address, - protocol: TransportProtocol::Tcp, - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct ProxyAuth { - pub username: String, - pub password: String, -} - -/// Options for a bundled Shadowsocks proxy. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct ShadowsocksProxySettings { - pub peer: SocketAddr, - /// Password on peer. - pub password: String, - pub cipher: String, - #[cfg(target_os = "linux")] - pub fwmark: Option<u32>, -} - -impl ShadowsocksProxySettings { - pub fn get_endpoint(&self) -> Endpoint { - Endpoint { - address: self.peer, - protocol: TransportProtocol::Tcp, - } - } -} - -/// List of ciphers usable by a Shadowsocks proxy. -/// Cf. [`ShadowsocksProxySettings::cipher`]. -pub const SHADOWSOCKS_CIPHERS: [&str; 19] = [ - // Stream ciphers. - "aes-128-cfb", - "aes-128-cfb1", - "aes-128-cfb8", - "aes-128-cfb128", - "aes-256-cfb", - "aes-256-cfb1", - "aes-256-cfb8", - "aes-256-cfb128", - "rc4", - "rc4-md5", - "chacha20", - "salsa20", - "chacha20-ietf", - // AEAD ciphers. - "aes-128-gcm", - "aes-256-gcm", - "chacha20-ietf-poly1305", - "xchacha20-ietf-poly1305", - "aes-128-pmac-siv", - "aes-256-pmac-siv", -]; - -/// Checks whether the proxy settings to be used by `OpenVpnMonitor` are valid. -pub fn validate_proxy_settings(proxy: &ProxySettings) -> Result<(), String> { - match proxy { - ProxySettings::Local(local) => { - if local.port == 0 { - return Err(String::from("Invalid local port number")); - } - if local.peer.ip().is_loopback() { - return Err(String::from( - "localhost is not a valid peer in this context", - )); - } - if local.peer.port() == 0 { - return Err(String::from("Invalid remote port number")); - } - } - ProxySettings::Remote(remote) => { - if remote.address.port() == 0 { - return Err(String::from("Invalid port number")); - } - if remote.address.ip().is_loopback() { - return Err(String::from("localhost is not a valid remote server")); - } - } - ProxySettings::Shadowsocks(ss) => { - if ss.peer.ip().is_loopback() { - return Err(String::from( - "localhost is not a valid peer in this context", - )); - } - if ss.peer.port() == 0 { - return Err(String::from("Invalid remote port number")); - } - if !SHADOWSOCKS_CIPHERS.contains(&ss.cipher.as_str()) { - return Err(String::from("Invalid cipher")); - } - } - }; - Ok(()) -} diff --git a/talpid-types/src/net/proxy.rs b/talpid-types/src/net/proxy.rs index 046298557a..5c16f19a7f 100644 --- a/talpid-types/src/net/proxy.rs +++ b/talpid-types/src/net/proxy.rs @@ -1,6 +1,8 @@ use crate::net::Endpoint; use serde::{Deserialize, Serialize}; -use std::fmt; +use std::{fmt, net::SocketAddr}; + +use super::TransportProtocol; /// Types of bridges that can be used to proxy a connection to a tunnel #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -27,3 +29,136 @@ pub struct ProxyEndpoint { pub endpoint: Endpoint, pub proxy_type: ProxyType, } + +/// User customized proxy used for obfuscation. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum CustomProxy { + Shadowsocks(Shadowsocks), + Socks5Local(Socks5Local), + Socks5Remote(Socks5Remote), +} + +impl CustomProxy { + pub fn get_remote_endpoint(&self) -> ProxyEndpoint { + match self { + CustomProxy::Socks5Local(settings) => ProxyEndpoint { + endpoint: settings.remote_endpoint, + proxy_type: ProxyType::Custom, + }, + CustomProxy::Socks5Remote(settings) => ProxyEndpoint { + endpoint: Endpoint::from_socket_address(settings.endpoint, TransportProtocol::Tcp), + proxy_type: ProxyType::Custom, + }, + CustomProxy::Shadowsocks(settings) => ProxyEndpoint { + endpoint: Endpoint::from_socket_address(settings.endpoint, TransportProtocol::Tcp), + proxy_type: ProxyType::Shadowsocks, + }, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct Shadowsocks { + pub endpoint: SocketAddr, + pub password: String, + /// One of [`shadowsocks_ciphers`]. + /// Gets validated at a later stage. Is assumed to be valid. + /// + /// shadowsocks_ciphers: talpid_types::net::openvpn::SHADOWSOCKS_CIPHERS + pub cipher: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct Socks5Local { + pub remote_endpoint: Endpoint, + /// Port on localhost where the SOCKS5-proxy listens to. + pub local_port: u16, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct Socks5Remote { + pub endpoint: SocketAddr, + pub auth: Option<SocksAuth>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct SocksAuth { + pub username: String, + pub password: String, +} + +impl Shadowsocks { + pub fn new<I: Into<SocketAddr>>(endpoint: I, cipher: String, password: String) -> Self { + Shadowsocks { + endpoint: endpoint.into(), + password, + cipher, + } + } +} + +impl Socks5Local { + pub fn new<I: Into<SocketAddr>>(remote_endpoint: I, local_port: u16) -> Self { + let transport_protocol = TransportProtocol::Tcp; + Self::new_with_transport_protocol(remote_endpoint, local_port, transport_protocol) + } + + pub fn new_with_transport_protocol<I: Into<SocketAddr>>( + remote_endpoint: I, + local_port: u16, + transport_protocol: TransportProtocol, + ) -> Self { + let remote_endpoint = + Endpoint::from_socket_address(remote_endpoint.into(), transport_protocol); + Self { + remote_endpoint, + local_port, + } + } +} + +impl Socks5Remote { + pub fn new<I: Into<SocketAddr>>(endpoint: I) -> Self { + Self { + endpoint: endpoint.into(), + auth: None, + } + } + + pub fn new_with_authentication<I: Into<SocketAddr>>( + endpoint: I, + authentication: SocksAuth, + ) -> Self { + Self { + endpoint: endpoint.into(), + auth: Some(authentication), + } + } +} + +/// List of ciphers usable by a Shadowsocks proxy. +/// Cf. [`ShadowsocksProxySettings::cipher`]. +pub const SHADOWSOCKS_CIPHERS: [&str; 19] = [ + // Stream ciphers. + "aes-128-cfb", + "aes-128-cfb1", + "aes-128-cfb8", + "aes-128-cfb128", + "aes-256-cfb", + "aes-256-cfb1", + "aes-256-cfb8", + "aes-256-cfb128", + "rc4", + "rc4-md5", + "chacha20", + "salsa20", + "chacha20-ietf", + // AEAD ciphers. + "aes-128-gcm", + "aes-256-gcm", + "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305", + "aes-128-pmac-siv", + "aes-256-pmac-siv", +]; diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index 6f759d7826..d36ba4febe 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -6,8 +6,8 @@ use super::{Error, TestContext}; use crate::network_monitor::{start_packet_monitor, MonitorOptions}; use mullvad_management_interface::{types, ManagementServiceClient}; use mullvad_types::relay_constraints::{ - BridgeConstraints, BridgeSettings, BridgeState, Constraint, ObfuscationSettings, - OpenVpnConstraints, RelayConstraints, RelaySettings, SelectedObfuscation, TransportPort, + BridgeSettings, BridgeState, Constraint, ObfuscationSettings, OpenVpnConstraints, + RelayConstraints, RelaySettings, SelectedObfuscation, TransportPort, Udp2TcpObfuscationSettings, WireguardConstraints, }; use mullvad_types::wireguard; @@ -209,12 +209,9 @@ pub async fn test_bridge( .await .expect("failed to enable bridge mode"); - set_bridge_settings( - &mut mullvad_client, - BridgeSettings::Normal(BridgeConstraints::default()), - ) - .await - .expect("failed to update bridge settings"); + set_bridge_settings(&mut mullvad_client, BridgeSettings::default()) + .await + .expect("failed to update bridge settings"); set_relay_settings( &mut mullvad_client, diff --git a/windows/winfw/src/winfw/fwcontext.cpp b/windows/winfw/src/winfw/fwcontext.cpp index f033830b45..4ed22737fc 100644 --- a/windows/winfw/src/winfw/fwcontext.cpp +++ b/windows/winfw/src/winfw/fwcontext.cpp @@ -81,7 +81,7 @@ void AppendRelayRules ( FwContext::Ruleset &ruleset, const WinFwEndpoint &relay, - const std::wstring &relayClient + const std::vector<std::wstring> &relayClients ) { auto sublayer = @@ -95,7 +95,7 @@ void AppendRelayRules wfp::IpAddress(relay.ip), relay.port, relay.protocol, - relayClient, + relayClients, sublayer )); } @@ -185,7 +185,7 @@ bool FwContext::applyPolicyConnecting ( const WinFwSettings &settings, const WinFwEndpoint &relay, - const std::wstring &relayClient, + const std::vector<std::wstring> &relayClients, const std::optional<std::wstring> &tunnelInterfaceAlias, const std::optional<WinFwAllowedEndpoint> &allowedEndpoint, const WinFwAllowedTunnelTraffic &allowedTunnelTraffic @@ -195,7 +195,7 @@ bool FwContext::applyPolicyConnecting AppendNetBlockedRules(ruleset); AppendSettingsRules(ruleset, settings); - AppendRelayRules(ruleset, relay, relayClient); + AppendRelayRules(ruleset, relay, relayClients); if (allowedEndpoint.has_value()) { @@ -280,7 +280,7 @@ bool FwContext::applyPolicyConnected ( const WinFwSettings &settings, const WinFwEndpoint &relay, - const std::wstring &relayClient, + const std::vector<std::wstring> &relayClient, const std::wstring &tunnelInterfaceAlias, const std::vector<wfp::IpAddress> &tunnelDnsServers, const std::vector<wfp::IpAddress> &nonTunnelDnsServers diff --git a/windows/winfw/src/winfw/fwcontext.h b/windows/winfw/src/winfw/fwcontext.h index 5fc23f09a7..92ecce4f4f 100644 --- a/windows/winfw/src/winfw/fwcontext.h +++ b/windows/winfw/src/winfw/fwcontext.h @@ -28,7 +28,7 @@ public: ( const WinFwSettings &settings, const WinFwEndpoint &relay, - const std::wstring &relayClient, + const std::vector<std::wstring> &relayClients, const std::optional<std::wstring> &tunnelInterfaceAlias, const std::optional<WinFwAllowedEndpoint> &allowedEndpoint, const WinFwAllowedTunnelTraffic &allowedTunnelTraffic @@ -38,7 +38,7 @@ public: ( const WinFwSettings &settings, const WinFwEndpoint &relay, - const std::wstring &relayClient, + const std::vector<std::wstring> &relayClients, const std::wstring &tunnelInterfaceAlias, const std::vector<wfp::IpAddress> &tunnelDnsServers, const std::vector<wfp::IpAddress> &nonTunnelDnsServers diff --git a/windows/winfw/src/winfw/rules/multi/permitvpnrelay.cpp b/windows/winfw/src/winfw/rules/multi/permitvpnrelay.cpp index 3c913cab14..19ce09571b 100644 --- a/windows/winfw/src/winfw/rules/multi/permitvpnrelay.cpp +++ b/windows/winfw/src/winfw/rules/multi/permitvpnrelay.cpp @@ -52,13 +52,13 @@ PermitVpnRelay::PermitVpnRelay const wfp::IpAddress &relay, uint16_t relayPort, WinFwProtocol protocol, - const std::wstring &relayClient, + const std::vector<std::wstring> &relayClients, Sublayer sublayer ) : m_relay(relay) , m_relayPort(relayPort) , m_protocol(protocol) - , m_relayClient(relayClient) + , m_relayClients(relayClients) , m_sublayer(sublayer) { } @@ -86,7 +86,10 @@ bool PermitVpnRelay::apply(IObjectInstaller &objectInstaller) conditionBuilder.add_condition(ConditionIp::Remote(m_relay)); conditionBuilder.add_condition(ConditionPort::Remote(m_relayPort)); conditionBuilder.add_condition(CreateProtocolCondition(m_protocol)); - conditionBuilder.add_condition(std::make_unique<ConditionApplication>(m_relayClient)); + + for(auto relayClient : m_relayClients) { + conditionBuilder.add_condition(std::make_unique<ConditionApplication>(relayClient)); + } return objectInstaller.addFilter(filterBuilder, conditionBuilder); } diff --git a/windows/winfw/src/winfw/rules/multi/permitvpnrelay.h b/windows/winfw/src/winfw/rules/multi/permitvpnrelay.h index d63f27a862..a2bfc16384 100644 --- a/windows/winfw/src/winfw/rules/multi/permitvpnrelay.h +++ b/windows/winfw/src/winfw/rules/multi/permitvpnrelay.h @@ -23,7 +23,7 @@ public: const wfp::IpAddress &relay, uint16_t relayPort, WinFwProtocol protocol, - const std::wstring &relayClient, + const std::vector<std::wstring> &relayClients, Sublayer sublayer ); @@ -34,7 +34,7 @@ private: const wfp::IpAddress m_relay; const uint16_t m_relayPort; const WinFwProtocol m_protocol; - const std::wstring m_relayClient; + const std::vector<std::wstring> m_relayClients; const Sublayer m_sublayer; }; diff --git a/windows/winfw/src/winfw/winfw.cpp b/windows/winfw/src/winfw/winfw.cpp index 4110dcd2f8..352a91c0d1 100644 --- a/windows/winfw/src/winfw/winfw.cpp +++ b/windows/winfw/src/winfw/winfw.cpp @@ -231,7 +231,8 @@ WINFW_API WinFw_ApplyPolicyConnecting( const WinFwSettings *settings, const WinFwEndpoint *relay, - const wchar_t *relayClient, + const wchar_t **relayClients, + size_t relayClientsLen, const wchar_t *tunnelInterfaceAlias, const WinFwAllowedEndpoint *allowedEndpoint, const WinFwAllowedTunnelTraffic *allowedTunnelTraffic @@ -254,20 +255,21 @@ WinFw_ApplyPolicyConnecting( THROW_ERROR("Invalid argument: relay"); } - if (nullptr == relayClient) - { - THROW_ERROR("Invalid argument: relayClient"); - } - if (nullptr == allowedTunnelTraffic) { THROW_ERROR("Invalid argument: allowedTunnelTraffic"); } + std::vector<std::wstring> relayClientWstrings; + relayClientWstrings.reserve(relayClientsLen); + for(int i = 0; i < relayClientsLen; i++) { + relayClientWstrings.push_back(relayClients[i]); + } + return g_fwContext->applyPolicyConnecting( *settings, *relay, - relayClient, + relayClientWstrings, tunnelInterfaceAlias != nullptr ? std::make_optional(tunnelInterfaceAlias) : std::nullopt, MakeOptional(allowedEndpoint), *allowedTunnelTraffic @@ -298,7 +300,8 @@ WINFW_API WinFw_ApplyPolicyConnected( const WinFwSettings *settings, const WinFwEndpoint *relay, - const wchar_t *relayClient, + const wchar_t **relayClients, + size_t relayClientsLen, const wchar_t *tunnelInterfaceAlias, const wchar_t *v4Gateway, const wchar_t *v6Gateway, @@ -323,11 +326,6 @@ WinFw_ApplyPolicyConnected( THROW_ERROR("Invalid argument: relay"); } - if (nullptr == relayClient) - { - THROW_ERROR("Invalid argument: relayClient"); - } - if (nullptr == tunnelInterfaceAlias) { THROW_ERROR("Invalid argument: tunnelInterfaceAlias"); @@ -407,10 +405,16 @@ WinFw_ApplyPolicyConnected( g_logSink(MULLVAD_LOG_LEVEL_DEBUG, ss.str().c_str(), g_logSinkContext); } + std::vector<std::wstring> relayClientWstrings; + relayClientWstrings.reserve(relayClientsLen); + for(int i = 0; i < relayClientsLen; i++) { + relayClientWstrings.push_back(relayClients[i]); + } + return g_fwContext->applyPolicyConnected( *settings, *relay, - relayClient, + relayClientWstrings, tunnelInterfaceAlias, tunnelDnsServers, nonTunnelDnsServers diff --git a/windows/winfw/src/winfw/winfw.h b/windows/winfw/src/winfw/winfw.h index 5d61f1029d..b786d943d3 100644 --- a/windows/winfw/src/winfw/winfw.h +++ b/windows/winfw/src/winfw/winfw.h @@ -164,7 +164,8 @@ WINFW_API WinFw_ApplyPolicyConnecting( const WinFwSettings *settings, const WinFwEndpoint *relay, - const wchar_t *relayClient, + const wchar_t **relayClient, + size_t relayClientLen, const wchar_t *tunnelInterfaceAlias, const WinFwAllowedEndpoint *allowedEndpoint, const WinFwAllowedTunnelTraffic *allowedTunnelTraffic @@ -194,7 +195,8 @@ WINFW_API WinFw_ApplyPolicyConnected( const WinFwSettings *settings, const WinFwEndpoint *relay, - const wchar_t *relayClient, + const wchar_t **relayClient, + size_t relayClientLen, const wchar_t *tunnelInterfaceAlias, const wchar_t *v4Gateway, const wchar_t *v6Gateway, |
