diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-10-23 15:13:51 +0200 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-10-23 15:13:51 +0200 |
| commit | 7774933e9513204db53c6191af3fa46458454027 (patch) | |
| tree | 4167cdc8361ebf0312b347d014ad209a2e27cb9a | |
| parent | 6e86b6fe22df51725e45133d3467df94815970f4 (diff) | |
| parent | c018d97ac770bfbd1169e544ea63359c9b932afc (diff) | |
| download | mullvadvpn-7774933e9513204db53c6191af3fa46458454027.tar.xz mullvadvpn-7774933e9513204db53c6191af3fa46458454027.zip | |
Merge branch 'add-encrypted-dns-as-an-access-method-in-the-daemon-des-1319'
24 files changed, 237 insertions, 76 deletions
diff --git a/Cargo.lock b/Cargo.lock index acec671f71..08ef9d4897 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2399,6 +2399,7 @@ dependencies = [ "ipnetwork", "libc", "log", + "mullvad-encrypted-dns-proxy", "mullvad-fs", "mullvad-types", "rustls-pemfile 2.1.3", @@ -2454,6 +2455,7 @@ dependencies = [ "log", "log-panics", "mullvad-api", + "mullvad-encrypted-dns-proxy", "mullvad-fs", "mullvad-management-interface", "mullvad-paths", @@ -2490,6 +2492,7 @@ dependencies = [ "hickory-resolver", "log", "rustls 0.21.11", + "serde", "tokio", "webpki-roots", ] diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt index 817418e1fc..622e95d9dd 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt @@ -188,7 +188,11 @@ internal fun ApiAccessMethod.fromDomain(): ManagementInterface.AccessMethod = it.setDirect(ManagementInterface.AccessMethod.Direct.getDefaultInstance()) ApiAccessMethod.Bridges -> it.setBridges(ManagementInterface.AccessMethod.Bridges.getDefaultInstance()) - is ApiAccessMethod.CustomProxy -> it.setCustom(this.fromDomain()) + is ApiAccessMethod.CustomProxy -> it.setCustom(fromDomain()) + is ApiAccessMethod.EncryptedDns -> + it.setEncryptedDnsProxy( + ManagementInterface.AccessMethod.EncryptedDnsProxy.getDefaultInstance() + ) } } .build() @@ -197,8 +201,8 @@ internal fun ApiAccessMethod.CustomProxy.fromDomain(): ManagementInterface.Custo ManagementInterface.CustomProxy.newBuilder() .let { when (this) { - is ApiAccessMethod.CustomProxy.Shadowsocks -> it.setShadowsocks(this.fromDomain()) - is ApiAccessMethod.CustomProxy.Socks5Remote -> it.setSocks5Remote(this.fromDomain()) + is ApiAccessMethod.CustomProxy.Shadowsocks -> it.setShadowsocks(fromDomain()) + is ApiAccessMethod.CustomProxy.Socks5Remote -> it.setSocks5Remote(fromDomain()) } } .build() diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt index bb7fb83fa6..fc4c64942f 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt @@ -557,7 +557,12 @@ internal fun ManagementInterface.PlayPurchasePaymentToken.toDomain(): PlayPurcha PlayPurchasePaymentToken(value = token) internal fun ManagementInterface.ApiAccessMethodSettings.toDomain(): List<ApiAccessMethodSetting> = - listOf(direct.toDomain(), mullvadBridges.toDomain()).plus(customList.map { it.toDomain() }) + buildList { + add(direct.toDomain()) + add(mullvadBridges.toDomain()) + add(encryptedDnsProxy.toDomain()) + addAll(customList.map { it.toDomain() }) + } internal fun ManagementInterface.AccessMethodSetting.toDomain(): ApiAccessMethodSetting = ApiAccessMethodSetting( @@ -571,6 +576,7 @@ internal fun ManagementInterface.AccessMethod.toDomain(): ApiAccessMethod = when { hasDirect() -> ApiAccessMethod.Direct hasBridges() -> ApiAccessMethod.Bridges + hasEncryptedDnsProxy() -> ApiAccessMethod.EncryptedDns hasCustom() -> custom.toDomain() else -> error("Type not found") } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ApiAccessMethod.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ApiAccessMethod.kt index 3fdcf8c730..dde768cfea 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ApiAccessMethod.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ApiAccessMethod.kt @@ -8,6 +8,8 @@ sealed interface ApiAccessMethod : Parcelable { @Parcelize data object Bridges : ApiAccessMethod + @Parcelize data object EncryptedDns : ApiAccessMethod + sealed interface CustomProxy : ApiAccessMethod { @Parcelize data class Socks5Remote(val ip: String, val port: Port, val auth: SocksAuth?) : CustomProxy diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 3850b7f239..ec571e5364 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -467,6 +467,10 @@ msgid "Enter port" msgstr "" msgctxt "api-access-methods-view" +msgid "If you are not connected to our VPN, then the Encrypted DNS proxy will use your own non-VPN IP when connecting. The DoH servers are hosted by one of the following providers: Quad 9, CloudFlare, or Google." +msgstr "" + +msgctxt "api-access-methods-view" msgid "In use" msgstr "" @@ -567,6 +571,10 @@ msgid "With the “Direct” method, the app communicates with a Mullvad API ser msgstr "" msgctxt "api-access-methods-view" +msgid "With the “Encrypted DNS proxy” method, the app will communicate with our Mullvad API through a proxy address. It does this by retrieving an address from a DNS over HTTPS (DoH) server and then using that to reach our API servers." +msgstr "" + +msgctxt "api-access-methods-view" msgid "With the “Mullvad bridges” method, the app communicates with a Mullvad API server via a Mullvad bridge server. It does this by sending the traffic obfuscated by Shadowsocks." msgstr "" diff --git a/gui/src/main/default-settings.ts b/gui/src/main/default-settings.ts index 46355dd439..bebc5b9a4e 100644 --- a/gui/src/main/default-settings.ts +++ b/gui/src/main/default-settings.ts @@ -104,6 +104,12 @@ export function getDefaultApiAccessMethods(): ApiAccessMethodSettings { enabled: false, type: 'bridges', }, + encryptedDnsProxy: { + id: '', + name: 'Encrypted DNS Proxy', + enabled: false, + type: 'encrypted-dns-proxy', + }, custom: [], }; } diff --git a/gui/src/main/grpc-type-convertions.ts b/gui/src/main/grpc-type-convertions.ts index 6511f10c67..37e5c7fcf5 100644 --- a/gui/src/main/grpc-type-convertions.ts +++ b/gui/src/main/grpc-type-convertions.ts @@ -16,6 +16,7 @@ import { DeviceEvent, DeviceState, DirectMethod, + EncryptedDnsProxy, EndpointObfuscationType, ErrorStateCause, ErrorStateDetails, @@ -1097,6 +1098,11 @@ function fillApiAccessMethodSetting<T extends grpcTypes.NewAccessMethodSetting>( accessMethod.setBridges(bridges); break; } + case 'encrypted-dns-proxy': { + const encryptedDnsProxy = new grpcTypes.AccessMethod.EncryptedDnsProxy(); + accessMethod.setEncryptedDnsProxy(encryptedDnsProxy); + break; + } default: accessMethod.setCustom(convertToCustomProxy(method)); } @@ -1160,6 +1166,12 @@ function convertFromApiAccessMethodSettings( const bridges = convertFromApiAccessMethodSetting( ensureExists(accessMethods.getMullvadBridges(), "no 'Mullvad Bridges' access method was found"), ) as AccessMethodSetting<BridgesMethod>; + const encryptedDnsProxy = convertFromApiAccessMethodSetting( + ensureExists( + accessMethods.getEncryptedDnsProxy(), + "no 'Encrypted DNS proxy' access method was found", + ), + ) as AccessMethodSetting<EncryptedDnsProxy>; const custom = accessMethods .getCustomList() .filter((setting) => setting.hasId() && setting.hasAccessMethod()) @@ -1170,6 +1182,7 @@ function convertFromApiAccessMethodSettings( return { direct, mullvadBridges: bridges, + encryptedDnsProxy, custom, }; } @@ -1177,7 +1190,11 @@ function convertFromApiAccessMethodSettings( function isCustomProxy( accessMethod: AccessMethodSetting, ): accessMethod is AccessMethodSetting<CustomProxy> { - return accessMethod.type !== 'direct' && accessMethod.type !== 'bridges'; + return ( + accessMethod.type !== 'direct' && + accessMethod.type !== 'bridges' && + accessMethod.type !== 'encrypted-dns-proxy' + ); } export function convertFromApiAccessMethodSetting( @@ -1200,6 +1217,8 @@ function convertFromAccessMethod(method: grpcTypes.AccessMethod): AccessMethod { return { type: 'direct' }; case grpcTypes.AccessMethod.AccessMethodCase.BRIDGES: return { type: 'bridges' }; + case grpcTypes.AccessMethod.AccessMethodCase.ENCRYPTED_DNS_PROXY: + return { type: 'encrypted-dns-proxy' }; case grpcTypes.AccessMethod.AccessMethodCase.CUSTOM: { return convertFromCustomProxy(method.getCustom()!); } diff --git a/gui/src/renderer/components/ApiAccessMethods.tsx b/gui/src/renderer/components/ApiAccessMethods.tsx index 57668df787..68b873bd64 100644 --- a/gui/src/renderer/components/ApiAccessMethods.tsx +++ b/gui/src/renderer/components/ApiAccessMethods.tsx @@ -36,6 +36,8 @@ import { StyledContent, StyledNavigationScrollbars, StyledSettingsContent } from import { SmallButton, SmallButtonColor, SmallButtonGroup } from './SmallButton'; const StyledContextMenuButton = styled(Cell.Icon)({ + alignItems: 'center', + justifyContent: 'center', marginRight: '8px', }); @@ -50,6 +52,7 @@ const StyledSpinner = styled(ImageView)({ }); const StyledNameLabel = styled(Cell.Label)({ + display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', @@ -133,6 +136,10 @@ export default function ApiAccessMethods() { method={methods.mullvadBridges} inUse={methods.mullvadBridges.id === currentMethod?.id} /> + <ApiAccessMethod + method={methods.encryptedDnsProxy} + inUse={methods.encryptedDnsProxy.id === currentMethod?.id} + /> {methods.custom.map((method) => ( <ApiAccessMethod key={method.id} @@ -211,7 +218,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { }, ]; - // Edit and Delete shouldn't be available for direct and bridges. + // Edit and Delete shouldn't be available for direct, bridges or encrypted DNS proxy. if (props.custom) { items.push( { type: 'separator' as const }, @@ -290,6 +297,20 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { ]} /> )} + {props.method.type === 'encrypted-dns-proxy' && ( + <StyledMethodInfoButton + message={[ + messages.pgettext( + 'api-access-methods-view', + 'With the “Encrypted DNS proxy” method, the app will communicate with our Mullvad API through a proxy address. It does this by retrieving an address from a DNS over HTTPS (DoH) server and then using that to reach our API servers.', + ), + messages.pgettext( + 'api-access-methods-view', + 'If you are not connected to our VPN, then the Encrypted DNS proxy will use your own non-VPN IP when connecting. The DoH servers are hosted by one of the following providers: Quad 9, CloudFlare, or Google.', + ), + ]} + /> + )} <ContextMenuContainer> <ContextMenuTrigger> <StyledContextMenuButton diff --git a/gui/src/renderer/components/ContextMenu.tsx b/gui/src/renderer/components/ContextMenu.tsx index fd84a8f0d9..2e01c9375f 100644 --- a/gui/src/renderer/components/ContextMenu.tsx +++ b/gui/src/renderer/components/ContextMenu.tsx @@ -36,6 +36,8 @@ const menuContext = React.createContext<MenuContext>({ const StyledMenuContainer = styled.div({ position: 'relative', padding: '8px 4px', + display: 'flex', + justifyContent: 'center', }); export function ContextMenuContainer(props: React.PropsWithChildren) { diff --git a/gui/src/renderer/components/InfoButton.tsx b/gui/src/renderer/components/InfoButton.tsx index 4ef2aa3c6e..7f77f115e7 100644 --- a/gui/src/renderer/components/InfoButton.tsx +++ b/gui/src/renderer/components/InfoButton.tsx @@ -8,7 +8,7 @@ import ImageView from './ImageView'; import { ModalAlert, ModalAlertType } from './Modal'; const StyledInfoButton = styled.button({ - margin: '0 16px 0 0', + margin: '0 16px 0 8px', borderWidth: 0, padding: 0, cursor: 'default', diff --git a/gui/src/renderer/components/cell/Label.tsx b/gui/src/renderer/components/cell/Label.tsx index 1115edc829..b2b37c1e4c 100644 --- a/gui/src/renderer/components/cell/Label.tsx +++ b/gui/src/renderer/components/cell/Label.tsx @@ -15,11 +15,15 @@ const StyledLabel = styled.div<{ disabled: boolean }>(buttonText, (props) => ({ textAlign: 'left', [`${LabelContainer} &&`]: { - marginTop: '5px', + marginTop: '0px', marginBottom: 0, height: '20px', lineHeight: '20px', }, + + [`${LabelContainer}:has(${StyledSubLabel}) &&`]: { + marginTop: '5px', + }, })); const StyledSubText = styled.span<{ disabled: boolean }>(tinyText, (props) => ({ diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index bd8f99711b..1522d43b39 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -524,7 +524,8 @@ export type NamedCustomProxy = CustomProxy & { name: string }; export type DirectMethod = { type: 'direct' }; export type BridgesMethod = { type: 'bridges' }; -export type AccessMethod = DirectMethod | BridgesMethod | CustomProxy; +export type EncryptedDnsProxy = { type: 'encrypted-dns-proxy' }; +export type AccessMethod = DirectMethod | BridgesMethod | EncryptedDnsProxy | CustomProxy; export type NamedAccessMethod<T extends AccessMethod> = T & { name: string }; @@ -540,6 +541,7 @@ export type AccessMethodSetting<T extends AccessMethod = AccessMethod> = export type ApiAccessMethodSettings = { direct: AccessMethodSetting<DirectMethod>; mullvadBridges: AccessMethodSetting<BridgesMethod>; + encryptedDnsProxy: AccessMethodSetting<EncryptedDnsProxy>; custom: Array<AccessMethodSetting<CustomProxy>>; }; diff --git a/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts b/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts index d17611261b..662ac4bf2a 100644 --- a/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts +++ b/gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts @@ -15,6 +15,7 @@ import { startInstalledApp } from '../installed-utils'; const DIRECT_NAME = 'Direct'; const BRIDGES_NAME = 'Mullvad Bridges'; +const ENCRYPTED_DNS_PROXY_NAME = 'Encrypted DNS proxy'; const IN_USE_LABEL = 'In use'; const FUNCTIONING_METHOD_NAME = 'Test method'; const NON_FUNCTIONING_METHOD_NAME = 'Non functioning test method'; @@ -42,12 +43,14 @@ test('App should display access methods', async () => { await navigateToAccessMethods(); const accessMethods = page.getByTestId('access-method'); - await expect(accessMethods).toHaveCount(2); + await expect(accessMethods).toHaveCount(3); const direct = accessMethods.first(); - const bridges = accessMethods.last(); + const bridges = accessMethods.nth(1); + const encryptedDnsProxy = accessMethods.nth(2); await expect(direct).toContainText(DIRECT_NAME); await expect(bridges).toContainText(BRIDGES_NAME); + await expect(encryptedDnsProxy).toContainText(ENCRYPTED_DNS_PROXY_NAME); await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1); }); @@ -144,6 +147,7 @@ test('App should use valid method', async () => { const direct = accessMethods.first(); const bridges = accessMethods.nth(1); + const encryptedDnsProxy = accessMethods.nth(2); const functioningTestMethod = accessMethods.last(); await expect(page.getByText(IN_USE_LABEL)).toHaveCount(1); @@ -154,6 +158,7 @@ test('App should use valid method', async () => { await functioningTestMethod.getByText('Use').click(); await expect(direct).not.toContainText(IN_USE_LABEL); await expect(bridges).not.toContainText(IN_USE_LABEL); + await expect(encryptedDnsProxy).not.toContainText(IN_USE_LABEL); await expect(functioningTestMethod).toContainText('API reachable'); await expect(functioningTestMethod).toContainText(IN_USE_LABEL); }); diff --git a/mullvad-api/Cargo.toml b/mullvad-api/Cargo.toml index ebb456ad6f..e617d942b5 100644 --- a/mullvad-api/Cargo.toml +++ b/mullvad-api/Cargo.toml @@ -33,6 +33,7 @@ tokio-rustls = { version = "0.26.0", features = ["logging", "tls12", "ring"], de tokio-socks = "0.5.1" rustls-pemfile = "2.1.3" +mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" } mullvad-fs = { path = "../mullvad-fs" } mullvad-types = { path = "../mullvad-types" } talpid-types = { path = "../talpid-types" } diff --git a/mullvad-api/src/https_client_with_sni.rs b/mullvad-api/src/https_client_with_sni.rs index 574d0a6309..898927513f 100644 --- a/mullvad-api/src/https_client_with_sni.rs +++ b/mullvad-api/src/https_client_with_sni.rs @@ -13,6 +13,9 @@ use hyper_util::{ client::legacy::connect::dns::{GaiResolver, Name}, rt::TokioIo, }; +use mullvad_encrypted_dns_proxy::{ + config::ProxyConfig as EncryptedDNSConfig, Forwarder as EncryptedDNSForwarder, +}; use shadowsocks::{ config::ServerType, context::{Context as SsContext, SharedContext}, @@ -78,6 +81,9 @@ enum InnerConnectionMode { Shadowsocks(ShadowsocksConfig), /// Connect to the destination via a Socks proxy. Socks5(SocksConfig), + /// Connect to the destination via Mullvad Encrypted DNS proxy. + /// See [`mullvad-encrypted-dns-proxy`] for how the proxy works. + EncryptedDnsProxy(EncryptedDNSConfig), } impl InnerConnectionMode { @@ -153,6 +159,20 @@ impl InnerConnectionMode { ) .await } + InnerConnectionMode::EncryptedDnsProxy(proxy_config) => { + let first_hop = SocketAddr::V4(proxy_config.addr); + let make_proxy_stream = |tcp_stream| async { + EncryptedDNSForwarder::from_stream(&proxy_config, tcp_stream) + }; + Self::connect_proxied( + first_hop, + hostname, + make_proxy_stream, + #[cfg(target_os = "android")] + socket_bypass_tx, + ) + .await + } } } @@ -256,6 +276,9 @@ impl TryFrom<ApiConnectionMode> for InnerConnectionMode { peer: config.endpoint, authentication: config.auth, }), + ProxyConfig::EncryptedDnsProxy(config) => { + InnerConnectionMode::EncryptedDnsProxy(config) + } }, }) } diff --git a/mullvad-api/src/proxy.rs b/mullvad-api/src/proxy.rs index 2fb2c4bf35..279a289289 100644 --- a/mullvad-api/src/proxy.rs +++ b/mullvad-api/src/proxy.rs @@ -1,7 +1,8 @@ use hyper_util::client::legacy::connect::{Connected, Connection}; use serde::{Deserialize, Serialize}; use std::{ - fmt, io, + io, + net::SocketAddr, path::Path, pin::Pin, task::{self, Poll}, @@ -60,20 +61,12 @@ pub enum ApiConnectionMode { Proxied(ProxyConfig), } -impl fmt::Display for ApiConnectionMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self { - ApiConnectionMode::Direct => write!(f, "unproxied"), - ApiConnectionMode::Proxied(settings) => settings.fmt(f), - } - } -} - #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum ProxyConfig { Shadowsocks(proxy::Shadowsocks), Socks5Local(proxy::Socks5Local), Socks5Remote(proxy::Socks5Remote), + EncryptedDnsProxy(mullvad_encrypted_dns_proxy::config::ProxyConfig), } impl ProxyConfig { @@ -87,18 +80,9 @@ impl ProxyConfig { ProxyConfig::Socks5Remote(remote) => { Endpoint::from_socket_address(remote.endpoint, TransportProtocol::Tcp) } - } - } -} - -impl fmt::Display for ProxyConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - let endpoint = self.get_endpoint(); - match self { - ProxyConfig::Shadowsocks(_) => write!(f, "Shadowsocks {}", endpoint), - ProxyConfig::Socks5Remote(_) => write!(f, "Socks5 {}", endpoint), - ProxyConfig::Socks5Local(local) => { - write!(f, "Socks5 {} via localhost:{}", endpoint, local.local_port) + ProxyConfig::EncryptedDnsProxy(proxy) => { + let addr = SocketAddr::V4(proxy.addr); + Endpoint::from_socket_address(addr, TransportProtocol::Tcp) } } } diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index 9342062d36..778ef02d7f 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -31,6 +31,7 @@ tokio-stream = "0.1" mullvad-relay-selector = { path = "../mullvad-relay-selector" } mullvad-types = { path = "../mullvad-types" } mullvad-api = { path = "../mullvad-api" } +mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" } mullvad-fs = { path = "../mullvad-fs" } mullvad-paths = { path = "../mullvad-paths" } mullvad-version = { path = "../mullvad-version" } diff --git a/mullvad-daemon/src/api.rs b/mullvad-daemon/src/api.rs index ac54382a57..622c9e8c8d 100644 --- a/mullvad-daemon/src/api.rs +++ b/mullvad-daemon/src/api.rs @@ -15,6 +15,7 @@ use mullvad_api::{ proxy::{ApiConnectionMode, ConnectionModeProvider, ProxyConfig}, AddressCache, }; +use mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState; use mullvad_relay_selector::RelaySelector; use mullvad_types::access_method::{ AccessMethod, AccessMethodSetting, BuiltInAccessMethod, Id, Settings, @@ -239,6 +240,8 @@ pub struct AccessModeSelector { cache_dir: PathBuf, /// Used for selecting a Bridge when the `Mullvad Bridges` access method is used. relay_selector: RelaySelector, + /// Used for selecting a config for the 'Encrypted DNS proxy' access method. + encrypted_dns_proxy_cache: EncryptedDnsProxyState, access_method_settings: Settings, address_cache: AddressCache, access_method_event_sender: DaemonEventSender<(AccessMethodEvent, oneshot::Sender<()>)>, @@ -267,10 +270,18 @@ impl AccessModeSelector { } } + // Initialize the Encrypted DNS cache + let mut encrypted_dns_proxy_cache = EncryptedDnsProxyState::default(); + // Always start looking from the position of `Direct`. let (index, next) = Self::find_next_active(0, &access_method_settings); - let initial_connection_mode = - Self::resolve_inner(next, &relay_selector, &address_cache).await; + let initial_connection_mode = Self::resolve_inner( + next, + &relay_selector, + &mut encrypted_dns_proxy_cache, + &address_cache, + ) + .await; let (change_tx, change_rx) = mpsc::unbounded(); @@ -280,6 +291,7 @@ impl AccessModeSelector { cmd_rx, cache_dir, relay_selector, + encrypted_dns_proxy_cache, access_method_settings, address_cache, access_method_event_sender, @@ -405,13 +417,10 @@ impl AccessModeSelector { // Save the new connection mode to cache! let cache_dir = self.cache_dir.clone(); - let new_connection_mode = resolved.connection_mode.clone(); + let connection_mode = resolved.connection_mode.clone(); tokio::spawn(async move { - if new_connection_mode.save(&cache_dir).await.is_err() { - log::warn!( - "Failed to save {connection_mode} to cache", - connection_mode = new_connection_mode - ) + if connection_mode.save(&cache_dir).await.is_err() { + log::warn!("Failed to save {connection_mode:#?} to cache") } }); @@ -496,16 +505,53 @@ impl AccessModeSelector { } async fn resolve(&mut self, access_method: AccessMethodSetting) -> ResolvedConnectionMode { - Self::resolve_inner(access_method, &self.relay_selector, &self.address_cache).await + Self::resolve_inner( + access_method, + &self.relay_selector, + &mut self.encrypted_dns_proxy_cache, + &self.address_cache, + ) + .await } async fn resolve_inner( access_method: AccessMethodSetting, relay_selector: &RelaySelector, + encrypted_dns_proxy_cache: &mut EncryptedDnsProxyState, address_cache: &AddressCache, ) -> ResolvedConnectionMode { - let connection_mode = - resolve_connection_mode(access_method.access_method.clone(), relay_selector); + let connection_mode = { + let access_method = access_method.access_method.clone(); + match access_method { + AccessMethod::BuiltIn(BuiltInAccessMethod::Direct) => ApiConnectionMode::Direct, + AccessMethod::BuiltIn(BuiltInAccessMethod::Bridge) => relay_selector + .get_bridge_forced() + .map(ProxyConfig::from) + .map(ApiConnectionMode::Proxied) + .unwrap_or_else(|| { + log::warn!( + "Received unexpected proxy settings type. Defaulting to direct API connection" + ); + log::debug!("Defaulting to direct API connection"); + ApiConnectionMode::Direct + }), + AccessMethod::BuiltIn(BuiltInAccessMethod::EncryptedDnsProxy) => { + if let Err(error) = encrypted_dns_proxy_cache.fetch_configs().await { + log::warn!("Failed to fetch new Encrypted DNS Proxy configurations"); + log::debug!("{error:#?}"); + } + encrypted_dns_proxy_cache + .next_configuration() + .map(ProxyConfig::EncryptedDnsProxy) + .map(ApiConnectionMode::Proxied) + .unwrap_or_else(|| { + log::warn!("Could not select next Encrypted DNS proxy config"); + log::debug!("Defaulting to direct API connection"); + ApiConnectionMode::Direct + })}, + AccessMethod::Custom(config) => ApiConnectionMode::Proxied(ProxyConfig::from(config)), + } + }; let endpoint = resolve_allowed_endpoint(&connection_mode, address_cache.get_address().await); ResolvedConnectionMode { @@ -516,30 +562,6 @@ impl AccessModeSelector { } } -/// Ad-hoc version of [`std::convert::From::from`], but since some -/// [`ApiConnectionMode`]s require extra logic/data from [`RelaySelector`] to be -/// instantiated the standard [`std::convert::From`] trait can not be -/// implemented. -fn resolve_connection_mode( - access_method: AccessMethod, - relay_selector: &RelaySelector, -) -> ApiConnectionMode { - match access_method { - AccessMethod::BuiltIn(BuiltInAccessMethod::Direct) => ApiConnectionMode::Direct, - AccessMethod::BuiltIn(BuiltInAccessMethod::Bridge) => relay_selector - .get_bridge_forced() - .map(ProxyConfig::from) - .map(ApiConnectionMode::Proxied) - .unwrap_or_else(|| { - log::error!( - "Received unexpected proxy settings type. Defaulting to direct API connection" - ); - ApiConnectionMode::Direct - }), - AccessMethod::Custom(config) => ApiConnectionMode::Proxied(ProxyConfig::from(config)), - } -} - pub fn resolve_allowed_endpoint( connection_mode: &ApiConnectionMode, fallback: SocketAddr, diff --git a/mullvad-encrypted-dns-proxy/Cargo.toml b/mullvad-encrypted-dns-proxy/Cargo.toml index 4f101e3ed0..da2a9c1552 100644 --- a/mullvad-encrypted-dns-proxy/Cargo.toml +++ b/mullvad-encrypted-dns-proxy/Cargo.toml @@ -14,6 +14,7 @@ workspace = true tokio = { workspace = true, features = [ "macros" ] } log = { workspace = true } hickory-resolver = { version = "0.24.1", features = [ "dns-over-https-rustls" ]} +serde = { workspace = true } webpki-roots = "0.25.0" rustls = "0.21" diff --git a/mullvad-encrypted-dns-proxy/src/config/mod.rs b/mullvad-encrypted-dns-proxy/src/config/mod.rs index bd75fd25eb..70f3b62af4 100644 --- a/mullvad-encrypted-dns-proxy/src/config/mod.rs +++ b/mullvad-encrypted-dns-proxy/src/config/mod.rs @@ -7,6 +7,7 @@ use std::net::{Ipv6Addr, SocketAddrV4}; mod plain; mod xor; +use serde::{Deserialize, Serialize}; pub use xor::XorKey; /// All the errors that can happen during deserialization of a [`ProxyConfig`]. @@ -67,7 +68,7 @@ pub trait Obfuscator: Send { /// Represents a Mullvad Encrypted DNS proxy configuration. Created by parsing /// the config out of an IPv6 address resolved over DoH. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ProxyConfig { /// The remote address to connect to the proxy over. This is the address /// on the internet where the proxy is listening. @@ -77,7 +78,7 @@ pub struct ProxyConfig { pub obfuscation: Option<ObfuscationConfig>, } -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum ObfuscationConfig { XorV2(xor::XorKey), } diff --git a/mullvad-encrypted-dns-proxy/src/config/xor.rs b/mullvad-encrypted-dns-proxy/src/config/xor.rs index b4dd2dbdf2..eb4f314e13 100644 --- a/mullvad-encrypted-dns-proxy/src/config/xor.rs +++ b/mullvad-encrypted-dns-proxy/src/config/xor.rs @@ -1,6 +1,8 @@ use core::fmt; use std::net::{Ipv4Addr, SocketAddrV4}; +use serde::{Deserialize, Serialize}; + /// Parse a proxy config that XORs all traffic with the given key. /// /// A Xor configuration is represented by the proxy type `ProxyType::XorV2`. There used to be a `XorV1`, but it @@ -37,7 +39,7 @@ pub fn parse_xor(data: [u8; 12]) -> Result<super::ProxyConfig, super::Error> { /// A bunch of bytes, representing a "key" Simply meaning a slice of bytes that the data /// will be XORed with. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct XorKey { data: [u8; 6], len: usize, diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index b57d63dcf3..62740438fb 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -411,10 +411,12 @@ message CustomProxy { message AccessMethod { message Direct {} message Bridges {} + message EncryptedDnsProxy {} oneof access_method { Direct direct = 1; Bridges bridges = 2; - CustomProxy custom = 3; + EncryptedDnsProxy encrypted_dns_proxy = 3; + CustomProxy custom = 4; } } @@ -434,7 +436,8 @@ message NewAccessMethodSetting { message ApiAccessMethodSettings { AccessMethodSetting direct = 1; AccessMethodSetting mullvad_bridges = 2; - repeated AccessMethodSetting custom = 3; + AccessMethodSetting encrypted_dns_proxy = 3; + repeated AccessMethodSetting custom = 4; } message Settings { diff --git a/mullvad-management-interface/src/types/conversions/access_method.rs b/mullvad-management-interface/src/types/conversions/access_method.rs index 9f45957db6..be3b4c20a8 100644 --- a/mullvad-management-interface/src/types/conversions/access_method.rs +++ b/mullvad-management-interface/src/types/conversions/access_method.rs @@ -10,6 +10,7 @@ mod settings { Self { direct: Some(settings.direct().clone().into()), mullvad_bridges: Some(settings.mullvad_bridges().clone().into()), + encrypted_dns_proxy: Some(settings.encrypted_dns_proxy().clone().into()), custom: settings .iter_custom() .cloned() @@ -37,6 +38,13 @@ mod settings { )) .and_then(access_method::AccessMethodSetting::try_from)?; + let encrypted_dns_proxy = settings + .encrypted_dns_proxy + .ok_or(FromProtobufTypeError::InvalidArgument( + "Could not deserialize Encrypted DNS proxy Access Method from protobuf", + )) + .and_then(access_method::AccessMethodSetting::try_from)?; + let custom = settings .custom .iter() @@ -46,6 +54,7 @@ mod settings { Ok(access_method::Settings::new( direct, mullvad_bridges, + encrypted_dns_proxy, custom, )) } @@ -118,6 +127,9 @@ mod data { Ok(match access_method { proto::access_method::AccessMethod::Direct(direct) => AccessMethod::from(direct), proto::access_method::AccessMethod::Bridges(bridge) => AccessMethod::from(bridge), + proto::access_method::AccessMethod::EncryptedDnsProxy(proxy) => { + AccessMethod::from(proxy) + } proto::access_method::AccessMethod::Custom(custom) => { CustomProxy::try_from(custom).map(AccessMethod::from)? } @@ -146,6 +158,12 @@ mod data { } } + impl From<proto::access_method::EncryptedDnsProxy> for AccessMethod { + fn from(_value: proto::access_method::EncryptedDnsProxy) -> Self { + AccessMethod::from(BuiltInAccessMethod::EncryptedDnsProxy) + } + } + impl TryFrom<proto::Socks5Local> for AccessMethod { type Error = FromProtobufTypeError; @@ -187,6 +205,11 @@ mod data { mullvad_types::access_method::BuiltInAccessMethod::Bridge => { proto::access_method::AccessMethod::Bridges(proto::access_method::Bridges {}) } + mullvad_types::access_method::BuiltInAccessMethod::EncryptedDnsProxy => { + proto::access_method::AccessMethod::EncryptedDnsProxy( + proto::access_method::EncryptedDnsProxy {}, + ) + } } } } diff --git a/mullvad-types/src/access_method.rs b/mullvad-types/src/access_method.rs index 2b375efb92..4f1229d126 100644 --- a/mullvad-types/src/access_method.rs +++ b/mullvad-types/src/access_method.rs @@ -4,8 +4,12 @@ use talpid_types::net::proxy::{CustomProxy, Shadowsocks, Socks5Local, Socks5Remo /// Settings for API access methods. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Settings { + #[serde(default = "Settings::create_direct")] direct: AccessMethodSetting, + #[serde(default = "Settings::create_mullvad_bridges")] mullvad_bridges: AccessMethodSetting, + #[serde(default = "Settings::create_encrypted_dns_proxy")] + encrypted_dns_proxy: AccessMethodSetting, /// Custom API access methods. custom: Vec<AccessMethodSetting>, } @@ -14,11 +18,13 @@ impl Settings { pub fn new( direct: AccessMethodSetting, mullvad_bridges: AccessMethodSetting, + encrypted_dns_proxy: AccessMethodSetting, custom: Vec<AccessMethodSetting>, ) -> Settings { Settings { direct, mullvad_bridges, + encrypted_dns_proxy, custom, } } @@ -93,6 +99,7 @@ impl Settings { use std::iter::once; once(&self.direct) .chain(once(&self.mullvad_bridges)) + .chain(once(&self.encrypted_dns_proxy)) .chain(&self.custom) } @@ -101,6 +108,7 @@ impl Settings { use std::iter::once; once(&mut self.direct) .chain(once(&mut self.mullvad_bridges)) + .chain(once(&mut self.encrypted_dns_proxy)) .chain(&mut self.custom) } @@ -112,9 +120,7 @@ impl Settings { /// Return the total number of access methods. /// This counts both enabled and disabled [`AccessMethodSetting`]s. pub fn cardinality(&self) -> usize { - 1 + // 'Direct' - 1 + // 'Mullvad bridges' - self.custom.len() + self.iter().count() } pub fn direct(&self) -> &AccessMethodSetting { @@ -125,6 +131,10 @@ impl Settings { &self.mullvad_bridges } + pub fn encrypted_dns_proxy(&self) -> &AccessMethodSetting { + &self.encrypted_dns_proxy + } + fn create_direct() -> AccessMethodSetting { let method = BuiltInAccessMethod::Direct; AccessMethodSetting::new(method.canonical_name(), true, AccessMethod::from(method)) @@ -134,6 +144,11 @@ impl Settings { let method = BuiltInAccessMethod::Bridge; AccessMethodSetting::new(method.canonical_name(), true, AccessMethod::from(method)) } + + fn create_encrypted_dns_proxy() -> AccessMethodSetting { + let method = BuiltInAccessMethod::EncryptedDnsProxy; + AccessMethodSetting::new(method.canonical_name(), true, AccessMethod::from(method)) + } } impl Default for Settings { @@ -141,6 +156,7 @@ impl Default for Settings { Self { direct: Settings::create_direct(), mullvad_bridges: Settings::create_mullvad_bridges(), + encrypted_dns_proxy: Settings::create_encrypted_dns_proxy(), custom: vec![], } } @@ -271,6 +287,7 @@ impl AccessMethodSetting { pub enum BuiltInAccessMethod { Direct, Bridge, + EncryptedDnsProxy, } impl AccessMethod { @@ -287,6 +304,7 @@ impl BuiltInAccessMethod { match self { BuiltInAccessMethod::Direct => "Direct".to_string(), BuiltInAccessMethod::Bridge => "Mullvad Bridges".to_string(), + BuiltInAccessMethod::EncryptedDnsProxy => "Encrypted DNS proxy".to_string(), } } } |
