Skip to content

Commit

Permalink
Merge branch 'add-encrypted-dns-as-an-access-method-in-the-daemon-des…
Browse files Browse the repository at this point in the history
…-1319'
  • Loading branch information
MarkusPettersson98 committed Oct 23, 2024
2 parents 6e86b6f + c018d97 commit 7774933
Show file tree
Hide file tree
Showing 24 changed files with 237 additions and 76 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions gui/locales/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,10 @@ msgctxt "api-access-methods-view"
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 ""
Expand Down Expand Up @@ -566,6 +570,10 @@ msgctxt "api-access-methods-view"
msgid "With the “Direct” method, the app communicates with a Mullvad API server directly without any intermediate proxies."
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 ""
Expand Down
6 changes: 6 additions & 0 deletions gui/src/main/default-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};
}
21 changes: 20 additions & 1 deletion gui/src/main/grpc-type-convertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DeviceEvent,
DeviceState,
DirectMethod,
EncryptedDnsProxy,
EndpointObfuscationType,
ErrorStateCause,
ErrorStateDetails,
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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())
Expand All @@ -1170,14 +1182,19 @@ function convertFromApiAccessMethodSettings(
return {
direct,
mullvadBridges: bridges,
encryptedDnsProxy,
custom,
};
}

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(
Expand All @@ -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()!);
}
Expand Down
23 changes: 22 additions & 1 deletion gui/src/renderer/components/ApiAccessMethods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand All @@ -50,6 +52,7 @@ const StyledSpinner = styled(ImageView)({
});

const StyledNameLabel = styled(Cell.Label)({
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions gui/src/renderer/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion gui/src/renderer/components/InfoButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 5 additions & 1 deletion gui/src/renderer/components/cell/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down
4 changes: 3 additions & 1 deletion gui/src/shared/daemon-rpc-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand All @@ -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>>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
Expand All @@ -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);
});
Expand Down
1 change: 1 addition & 0 deletions mullvad-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
23 changes: 23 additions & 0 deletions mullvad-api/src/https_client_with_sni.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
}

Expand Down Expand Up @@ -256,6 +276,9 @@ impl TryFrom<ApiConnectionMode> for InnerConnectionMode {
peer: config.endpoint,
authentication: config.auth,
}),
ProxyConfig::EncryptedDnsProxy(config) => {
InnerConnectionMode::EncryptedDnsProxy(config)
}
},
})
}
Expand Down
Loading

0 comments on commit 7774933

Please sign in to comment.