diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-07-23 09:42:15 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-07-23 09:42:15 +0200 |
| commit | 6ee9f98e90fd8fb7bad4e7670aed3a711ffbf317 (patch) | |
| tree | 5dd900add396604d3d9e969e5f8387b5c11aedbd /desktop | |
| parent | dc2db9cc8a63e8bcbd3cbf70c28c7446d7f83733 (diff) | |
| parent | bdd5faa521137753b46c78a6c5778114f0ce20a4 (diff) | |
| download | mullvadvpn-6ee9f98e90fd8fb7bad4e7670aed3a711ffbf317.tar.xz mullvadvpn-6ee9f98e90fd8fb7bad4e7670aed3a711ffbf317.zip | |
Merge branch 'filter-by-quic-in-gui-location-list-des-2266'
Diffstat (limited to 'desktop')
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); }); }); }); |
