summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2024-10-23 15:13:51 +0200
committerMarkus Pettersson <markus.pettersson@mullvad.net>2024-10-23 15:13:51 +0200
commit7774933e9513204db53c6191af3fa46458454027 (patch)
tree4167cdc8361ebf0312b347d014ad209a2e27cb9a
parent6e86b6fe22df51725e45133d3467df94815970f4 (diff)
parentc018d97ac770bfbd1169e544ea63359c9b932afc (diff)
downloadmullvadvpn-7774933e9513204db53c6191af3fa46458454027.tar.xz
mullvadvpn-7774933e9513204db53c6191af3fa46458454027.zip
Merge branch 'add-encrypted-dns-as-an-access-method-in-the-daemon-des-1319'
-rw-r--r--Cargo.lock3
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt10
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ApiAccessMethod.kt2
-rw-r--r--gui/locales/messages.pot8
-rw-r--r--gui/src/main/default-settings.ts6
-rw-r--r--gui/src/main/grpc-type-convertions.ts21
-rw-r--r--gui/src/renderer/components/ApiAccessMethods.tsx23
-rw-r--r--gui/src/renderer/components/ContextMenu.tsx2
-rw-r--r--gui/src/renderer/components/InfoButton.tsx2
-rw-r--r--gui/src/renderer/components/cell/Label.tsx6
-rw-r--r--gui/src/shared/daemon-rpc-types.ts4
-rw-r--r--gui/test/e2e/installed/state-dependent/api-access-methods.spec.ts9
-rw-r--r--mullvad-api/Cargo.toml1
-rw-r--r--mullvad-api/src/https_client_with_sni.rs23
-rw-r--r--mullvad-api/src/proxy.rs28
-rw-r--r--mullvad-daemon/Cargo.toml1
-rw-r--r--mullvad-daemon/src/api.rs92
-rw-r--r--mullvad-encrypted-dns-proxy/Cargo.toml1
-rw-r--r--mullvad-encrypted-dns-proxy/src/config/mod.rs5
-rw-r--r--mullvad-encrypted-dns-proxy/src/config/xor.rs4
-rw-r--r--mullvad-management-interface/proto/management_interface.proto7
-rw-r--r--mullvad-management-interface/src/types/conversions/access_method.rs23
-rw-r--r--mullvad-types/src/access_method.rs24
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(),
}
}
}