summaryrefslogtreecommitdiffhomepage
path: root/desktop
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-07-23 09:42:15 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-07-23 09:42:15 +0200
commit6ee9f98e90fd8fb7bad4e7670aed3a711ffbf317 (patch)
tree5dd900add396604d3d9e969e5f8387b5c11aedbd /desktop
parentdc2db9cc8a63e8bcbd3cbf70c28c7446d7f83733 (diff)
parentbdd5faa521137753b46c78a6c5778114f0ce20a4 (diff)
downloadmullvadvpn-6ee9f98e90fd8fb7bad4e7670aed3a711ffbf317.tar.xz
mullvadvpn-6ee9f98e90fd8fb7bad4e7670aed3a711ffbf317.zip
Merge branch 'filter-by-quic-in-gui-location-list-des-2266'
Diffstat (limited to 'desktop')
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot7
-rw-r--r--desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts38
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/RelayListContext.tsx19
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx25
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts10
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts9
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/helpers.ts8
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/mock-data.ts16
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/select-location.spec.ts159
10 files changed, 206 insertions, 87 deletions
diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot
index 0f1f997ba8..6283cf0273 100644
--- a/desktop/packages/mullvad-vpn/locales/messages.pot
+++ b/desktop/packages/mullvad-vpn/locales/messages.pot
@@ -1944,6 +1944,13 @@ msgctxt "select-location-view"
msgid "Name is already taken."
msgstr ""
+#. Label for indicator that shows that obfuscation is being used as a filter.
+#. Available placeholders:
+#. %(obfuscation)s - type of obfuscation in use
+msgctxt "select-location-view"
+msgid "Obfuscation: %(obfuscation)s"
+msgstr ""
+
msgctxt "select-location-view"
msgid "Open %(daita)s settings"
msgstr ""
diff --git a/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts b/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts
index 6d8ebcb17b..4b79b0b1d9 100644
--- a/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts
+++ b/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts
@@ -52,7 +52,7 @@ import {
ObfuscationType,
Ownership,
ProxyType,
- RelayEndpointType,
+ Quic,
RelayLocation,
RelayLocationGeographical,
RelayProtocol,
@@ -118,28 +118,36 @@ function convertFromRelayListCity(city: grpcTypes.RelayListCity): IRelayListCity
function convertFromRelayListRelay(relay: grpcTypes.Relay): IRelayListHostname {
const relayObject = relay.toObject();
- let daita = false;
- if (relayObject.endpointType === grpcTypes.Relay.RelayType.WIREGUARD) {
- const endpointDataU8 = relay.getEndpointData()?.getValue_asU8();
- if (endpointDataU8) {
- daita = grpcTypes.WireguardRelayEndpointData.deserializeBinary(endpointDataU8).getDaita();
- }
- }
+ // The relay type is determined by the variant of the extra endpoint data
+ const wireguard = relayObject.endpointData?.wireguard;
+ const openvpn = relayObject.endpointData?.openvpn;
+ const bridge = relayObject.endpointData?.bridge;
+
+ const endpointType = wireguard
+ ? 'wireguard'
+ : openvpn
+ ? 'openvpn'
+ : bridge
+ ? 'bridge'
+ : /*This case should never happen ..*/ 'bridge';
+
+ const daita = wireguard ? wireguard.daita : false;
+ const quic = wireguard?.quic ? quicFromRelayType(wireguard.quic) : undefined;
return {
...relayObject,
- endpointType: convertFromRelayType(relayObject.endpointType),
+ endpointType,
daita,
+ quic,
};
}
-function convertFromRelayType(relayType: grpcTypes.Relay.RelayType): RelayEndpointType {
- const protocolMap: Record<grpcTypes.Relay.RelayType, RelayEndpointType> = {
- [grpcTypes.Relay.RelayType.OPENVPN]: 'openvpn',
- [grpcTypes.Relay.RelayType.BRIDGE]: 'bridge',
- [grpcTypes.Relay.RelayType.WIREGUARD]: 'wireguard',
+function quicFromRelayType(quic: grpcTypes.Relay.RelayData.Wireguard.Quic.AsObject): Quic {
+ return {
+ domain: quic.domain,
+ token: quic.token,
+ addrIn: quic.addrInList,
};
- return protocolMap[relayType];
}
function convertFromWireguardKey(publicKey: Uint8Array | string): string {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/RelayListContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/RelayListContext.tsx
index 07b2aa9c62..d300c56e7f 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/RelayListContext.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/RelayListContext.tsx
@@ -1,11 +1,16 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
-import { compareRelayLocation, RelayLocation } from '../../../shared/daemon-rpc-types';
+import {
+ compareRelayLocation,
+ ObfuscationType,
+ RelayLocation,
+} from '../../../shared/daemon-rpc-types';
import {
EndpointType,
filterLocations,
filterLocationsByDaita,
filterLocationsByEndPointType,
+ filterLocationsByQuic,
getLocationsExpandedBySearch,
searchForLocations,
} from '../../lib/filter-locations';
@@ -68,6 +73,9 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) {
const { locationType, searchTerm } = useSelectLocationContext();
const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false);
const directOnly = useSelector((state) => state.settings.wireguard.daita?.directOnly ?? false);
+ const quic = useSelector(
+ (state) => state.settings.obfuscationSettings.selectedObfuscation === ObfuscationType.quic,
+ );
const fullRelayList = useSelector((state) => state.settings.relayLocations);
const relaySettings = useNormalRelaySettings();
@@ -99,11 +107,16 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) {
relaySettings?.wireguard.useMultihop,
]);
+ // Only show relays that have QUIC endpoints when QUIC obfuscation is enabled.
+ const relayListForQuic = useMemo(() => {
+ return filterLocationsByQuic(relayListForDaita, quic, tunnelProtocol);
+ }, [quic, relayListForDaita, tunnelProtocol]);
+
// Filters the relays to only keep the relays matching the currently selected filters, e.g.
// ownership and providers
const relayListForFilters = useMemo(() => {
- return filterLocations(relayListForDaita, relaySettings?.ownership, relaySettings?.providers);
- }, [relaySettings?.ownership, relaySettings?.providers, relayListForDaita]);
+ return filterLocations(relayListForQuic, relaySettings?.ownership, relaySettings?.providers);
+ }, [relaySettings?.ownership, relaySettings?.providers, relayListForQuic]);
// Filters the relays based on the provided search term
const relayListForSearch = useMemo(() => {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx
index d5be26cbf4..14f9bef50b 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { sprintf } from 'sprintf-js';
import { strings } from '../../../shared/constants';
-import { Ownership } from '../../../shared/daemon-rpc-types';
+import { ObfuscationType, Ownership } from '../../../shared/daemon-rpc-types';
import { messages } from '../../../shared/gettext';
import { RoutePath } from '../../../shared/routes';
import { Button, FilterChip, Flex, IconButton, LabelTiny } from '../../lib/components';
@@ -62,6 +62,9 @@ export default function SelectLocation() {
const filteredProviders = useFilteredProviders(providers, ownership);
const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false);
const directOnly = useSelector((state) => state.settings.wireguard.daita?.directOnly ?? false);
+ const showQuicFilter = useSelector(
+ (state) => state.settings.obfuscationSettings.selectedObfuscation === ObfuscationType.quic,
+ );
const showDaitaFilter = daitaFilterActive(
daita,
directOnly,
@@ -119,7 +122,8 @@ export default function SelectLocation() {
const showOwnershipFilter = ownership !== Ownership.any;
const showProvidersFilter = providers.length > 0;
- const showFilters = showOwnershipFilter || showProvidersFilter || showDaitaFilter;
+ const showFilters =
+ showOwnershipFilter || showProvidersFilter || showDaitaFilter || showQuicFilter;
return (
<BackAction action={onClose}>
<Layout>
@@ -199,6 +203,23 @@ export default function SelectLocation() {
</FilterChip.Text>
</FilterChip>
)}
+
+ {showQuicFilter && (
+ <FilterChip as="div">
+ <FilterChip.Text>
+ {sprintf(
+ // TRANSLATORS: Label for indicator that shows that obfuscation is being used as a filter.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(obfuscation)s - type of obfuscation in use
+ messages.pgettext(
+ 'select-location-view',
+ 'Obfuscation: %(obfuscation)s',
+ ),
+ { obfuscation: 'QUIC' },
+ )}
+ </FilterChip.Text>
+ </FilterChip>
+ )}
</Flex>
)}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts
index f73bd14a9e..29ce00f1ff 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts
@@ -32,6 +32,16 @@ export function filterLocationsByEndPointType(
return filterLocationsImpl(locations, getTunnelProtocolFilter(endpointType, tunnelProtocol));
}
+export function filterLocationsByQuic(
+ locations: IRelayLocationCountryRedux[],
+ quic: boolean,
+ tunnelProtocol: TunnelProtocol,
+): IRelayLocationCountryRedux[] {
+ const quicFilterActive = quic && tunnelProtocol !== 'openvpn';
+ const quickOnRelay = (relay: IRelayLocationRelayRedux) => relay.quic !== undefined;
+ return quicFilterActive ? filterLocationsImpl(locations, quickOnRelay) : locations;
+}
+
export function filterLocationsByDaita(
locations: IRelayLocationCountryRedux[],
daita: boolean,
diff --git a/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts b/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts
index 909004faf6..9ac8bb3a61 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts
@@ -15,6 +15,7 @@ import {
ObfuscationSettings,
ObfuscationType,
Ownership,
+ Quic,
RelayEndpointType,
RelayLocation,
RelayOverride,
@@ -77,6 +78,7 @@ export interface IRelayLocationRelayRedux {
weight: number;
endpointType: RelayEndpointType;
daita: boolean;
+ quic?: Quic;
}
export interface IRelayLocationCityRedux {
diff --git a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
index 63ec8d0b9b..49164a3770 100644
--- a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
@@ -340,6 +340,7 @@ export type ConnectionConfig =
addresses: string[];
endpoint: string;
};
+
ipv4Gateway: string;
ipv6Gateway?: string;
};
@@ -396,8 +397,16 @@ export interface IRelayListHostname {
owned: boolean;
endpointType: RelayEndpointType;
daita: boolean;
+ // The absence of this value signals that the relay does not deploy QUIC.
+ quic?: Quic;
}
+export type Quic = {
+ domain: string;
+ token: string;
+ addrIn: string[];
+};
+
export type RelayEndpointType = 'wireguard' | 'openvpn' | 'bridge';
export interface ITunnelOptions {
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/helpers.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/helpers.ts
index 7490adc6ac..d04dac2f3b 100644
--- a/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/helpers.ts
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/helpers.ts
@@ -51,6 +51,13 @@ export const createHelpers = (page: Page, routes: RoutesObjectModel, utils: Mock
),
);
+ const locateRelaysByObfuscation = (relayList: IRelayList): LocatedRelay[] =>
+ relayList.countries.flatMap((country) =>
+ country.cities.flatMap((city) =>
+ city.relays.filter((relay) => relay.quic).map((relay) => ({ country, city, relay })),
+ ),
+ );
+
const resetOwnership = async () => {
await routes.filter.expandOwnership();
await routes.filter.selectOwnershipOption('Any');
@@ -104,6 +111,7 @@ export const createHelpers = (page: Page, routes: RoutesObjectModel, utils: Mock
expandLocatedRelays,
locateRelaysByProvider,
locateRelaysByOwner,
+ locateRelaysByObfuscation,
resetOwnership,
resetProviders,
resetView,
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/mock-data.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/mock-data.ts
index 6e60e29c8d..f3039c2537 100644
--- a/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/mock-data.ts
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/mock-data.ts
@@ -45,6 +45,22 @@ const relayList: IRelayList = {
endpointType: 'wireguard',
daita: true,
},
+ {
+ hostname: 'se-got-wg-104',
+ provider: 'mullvad',
+ ipv4AddrIn: '10.0.0.4',
+ includeInCountry: true,
+ active: true,
+ weight: 0,
+ owned: true,
+ endpointType: 'wireguard',
+ daita: true,
+ quic: {
+ addrIn: [],
+ domain: '',
+ token: '',
+ },
+ },
],
},
],
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/select-location.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/select-location.spec.ts
index 4065f08b74..ab4cd3736d 100644
--- a/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/select-location.spec.ts
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/select-location.spec.ts
@@ -7,6 +7,7 @@ import {
IRelayListWithEndpointData,
ISettings,
IWireguardEndpointData,
+ ObfuscationType,
Ownership,
} from '../../../../src/shared/daemon-rpc-types';
import { RoutePath } from '../../../../src/shared/routes';
@@ -121,100 +122,124 @@ test.describe('Select location', () => {
});
test.describe('Filter', () => {
- test.beforeEach(async () => {
- await helpers.resetView();
- await helpers.resetProviders();
- await helpers.resetOwnership();
- });
+ test.describe('Applied from filter view', () => {
+ test.beforeEach(async () => {
+ await helpers.resetView();
+ await helpers.resetProviders();
+ await helpers.resetOwnership();
+ });
- test.describe('Filter by provider', () => {
- test('Should deselect all providers when clicking all providers checkbox', async () => {
- await routes.filter.expandProviders();
- await routes.filter.checkAllProvidersCheckbox();
- expect(await helpers.areAllCheckboxesChecked()).toBe(false);
+ test.describe('Filter by provider', () => {
+ test('Should deselect all providers when clicking all providers checkbox', async () => {
+ await routes.filter.expandProviders();
+ await routes.filter.checkAllProvidersCheckbox();
+ expect(await helpers.areAllCheckboxesChecked()).toBe(false);
- await routes.filter.checkAllProvidersCheckbox();
- expect(await helpers.areAllCheckboxesChecked()).toBe(true);
- });
+ await routes.filter.checkAllProvidersCheckbox();
+ expect(await helpers.areAllCheckboxesChecked()).toBe(true);
+ });
- test('Should apply filter when selecting provider', async () => {
- await routes.filter.expandProviders();
- await routes.filter.checkAllProvidersCheckbox();
- expect(await helpers.areAllCheckboxesChecked()).toBe(false);
+ test('Should apply filter when selecting provider', async () => {
+ await routes.filter.expandProviders();
+ await routes.filter.checkAllProvidersCheckbox();
+ expect(await helpers.areAllCheckboxesChecked()).toBe(false);
- // Select one provider
- const provider = relayList.countries[0].cities[0].relays[0].provider;
- await routes.filter.checkProviderCheckbox(provider);
+ // Select one provider
+ const provider = relayList.countries[0].cities[0].relays[0].provider;
+ await routes.filter.checkProviderCheckbox(provider);
- await helpers.updateMockRelayFilter({
- providers: [provider],
- });
+ await helpers.updateMockRelayFilter({
+ providers: [provider],
+ });
- await routes.filter.applyFilter();
- await util.waitForRoute(RoutePath.selectLocation);
- const providerFilterChip = routes.selectLocation.getFilterChip('Providers: 1');
- await expect(providerFilterChip).toBeVisible();
+ await routes.filter.applyFilter();
+ await util.waitForRoute(RoutePath.selectLocation);
+ const providerFilterChip = routes.selectLocation.getFilterChip('Providers: 1');
+ await expect(providerFilterChip).toBeVisible();
- const locatedRelays = helpers.locateRelaysByProvider(relayList, provider);
- const relays = locatedRelays.map((locatedRelay) => locatedRelay.relay);
- const relayNames = relays.map((relay) => relay.hostname);
+ const locatedRelays = helpers.locateRelaysByProvider(relayList, provider);
+ const relays = locatedRelays.map((locatedRelay) => locatedRelay.relay);
+ const relayNames = relays.map((relay) => relay.hostname);
- // Expand all accordions
- await helpers.expandLocatedRelays(locatedRelays);
+ // Expand all accordions
+ await helpers.expandLocatedRelays(locatedRelays);
- const buttons = routes.selectLocation.getRelaysMatching(relayNames);
+ const buttons = routes.selectLocation.getRelaysMatching(relayNames);
- // Expect all filtered relays to have a button
- await expect(buttons).toHaveCount(relays.length);
+ // Expect all filtered relays to have a button
+ await expect(buttons).toHaveCount(relays.length);
- // Clear filter
- await providerFilterChip.click();
+ // Clear filter
+ await providerFilterChip.click();
- // Get all relays and expand accordions
- const allLocatedRelays = helpers.locateRelaysByProvider(relayList);
- await helpers.expandLocatedRelays(allLocatedRelays);
+ // Get all relays and expand accordions
+ const allLocatedRelays = helpers.locateRelaysByProvider(relayList);
+ await helpers.expandLocatedRelays(allLocatedRelays);
- // Should not have same length as all relays
- await expect(buttons).not.toHaveCount(allLocatedRelays.length);
+ // Should not have same length as all relays
+ await expect(buttons).not.toHaveCount(allLocatedRelays.length);
+ });
});
- });
- test.describe('Filter by ownership', () => {
- test('Should apply filter when selecting ownership', async () => {
- // Select rented only
- await routes.filter.expandOwnership();
- await routes.filter.selectOwnershipOption('Rented only');
- await helpers.updateMockRelayFilter({
- ownership: Ownership.rented,
- });
+ test.describe('Filter by ownership', () => {
+ test('Should apply filter when selecting ownership', async () => {
+ // Select rented only
+ await routes.filter.expandOwnership();
+ await routes.filter.selectOwnershipOption('Rented only');
+ await helpers.updateMockRelayFilter({
+ ownership: Ownership.rented,
+ });
+
+ await routes.filter.applyFilter();
+ await util.waitForRoute(RoutePath.selectLocation);
+
+ const ownerFilterChip = routes.selectLocation.getFilterChip('Rented');
+ await expect(ownerFilterChip).toBeVisible();
+
+ const locatedRelays = helpers.locateRelaysByOwner(relayList, false);
+ const relays = locatedRelays.map((locatedRelay) => locatedRelay.relay);
+ const relayNames = relays.map((relay) => relay.hostname);
+
+ // Expand all accordions
+ await helpers.expandLocatedRelays(locatedRelays);
+
+ const buttons = routes.selectLocation.getRelaysMatching(relayNames);
+
+ // Expect all filtered relays to have a button
+ await expect(buttons).toHaveCount(relays.length);
- await routes.filter.applyFilter();
- await util.waitForRoute(RoutePath.selectLocation);
+ // Clear filter
+ await ownerFilterChip.click();
- const ownerFilterChip = routes.selectLocation.getFilterChip('Rented');
- await expect(ownerFilterChip).toBeVisible();
+ // Get all relays and expand accordions
+ const allLocatedRelays = helpers.locateRelaysByOwner(relayList);
+ await helpers.expandLocatedRelays(allLocatedRelays);
- const locatedRelays = helpers.locateRelaysByOwner(relayList, false);
+ // Should not have same length as all relays
+ await expect(buttons).not.toHaveCount(allLocatedRelays.length);
+ });
+ });
+ });
+ test.describe('Filter by obfuscation', () => {
+ test('Should apply filter when QUIC obfuscation is selected', async () => {
+ const settings = getDefaultSettings();
+ if ('normal' in settings.relaySettings) {
+ settings.obfuscationSettings.selectedObfuscation = ObfuscationType.quic;
+ }
+ await util.sendMockIpcResponse<ISettings>({
+ channel: 'settings-',
+ response: settings,
+ });
+ const locatedRelays = helpers.locateRelaysByObfuscation(relayList);
const relays = locatedRelays.map((locatedRelay) => locatedRelay.relay);
const relayNames = relays.map((relay) => relay.hostname);
- // Expand all accordions
await helpers.expandLocatedRelays(locatedRelays);
const buttons = routes.selectLocation.getRelaysMatching(relayNames);
// Expect all filtered relays to have a button
await expect(buttons).toHaveCount(relays.length);
-
- // Clear filter
- await ownerFilterChip.click();
-
- // Get all relays and expand accordions
- const allLocatedRelays = helpers.locateRelaysByOwner(relayList);
- await helpers.expandLocatedRelays(allLocatedRelays);
-
- // Should not have same length as all relays
- await expect(buttons).not.toHaveCount(allLocatedRelays.length);
});
});
});