Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement encrypted dns proxy in Electron GUI #7013

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading