diff options
25 files changed, 540 insertions, 394 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt index 8e4024a4d5..27b5951cea 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt @@ -22,6 +22,7 @@ private val DUMMY_RELAY_1 = provider = ProviderId("PROVIDER RENTED"), ownership = Ownership.Rented, daita = false, + quic = false, ) private val DUMMY_RELAY_2 = RelayItem.Location.Relay( @@ -34,6 +35,7 @@ private val DUMMY_RELAY_2 = provider = ProviderId("PROVIDER OWNED"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) private val DUMMY_RELAY_CITY_1 = RelayItem.Location.City( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt index 6fe027249e..4803b966a9 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt @@ -95,23 +95,16 @@ private fun RelayItem.Location.City.filter( } } -private fun RelayItem.Location.Relay.hasMatchingDaitaSetting(filterDaita: Boolean): Boolean { - return if (filterDaita) daita else true -} +private fun RelayItem.Location.Relay.hasMatchingDaitaSetting(filterDaita: Boolean): Boolean = + if (filterDaita) daita else true private fun RelayItem.Location.Relay.filter( ownership: Constraint<Ownership>, providers: Constraint<Providers>, daita: Boolean, -): RelayItem.Location.Relay? { - return if ( - hasMatchingDaitaSetting(daita) && hasOwnership(ownership) && hasProvider(providers) - ) { - this - } else { - null - } -} +): RelayItem.Location.Relay? = + if (hasMatchingDaitaSetting(daita) && hasOwnership(ownership) && hasProvider(providers)) this + else null fun List<RelayItem.Location.Country>.findByGeoLocationId( geoLocationId: GeoLocationId diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt index 9b31f8bf24..a3257f04d9 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt @@ -364,6 +364,7 @@ class CustomListLocationsViewModelTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) ), ) @@ -381,6 +382,7 @@ class CustomListLocationsViewModelTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) } } 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 02609da2f8..140cf5aafb 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 @@ -79,7 +79,6 @@ import net.mullvad.mullvadvpn.lib.model.TunnelState import net.mullvad.mullvadvpn.lib.model.Udp2TcpObfuscationSettings import net.mullvad.mullvadvpn.lib.model.WireguardConstraints import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData -import net.mullvad.mullvadvpn.lib.model.WireguardRelayEndpointData import net.mullvad.mullvadvpn.lib.model.WireguardTunnelOptions internal fun ManagementInterface.TunnelState.toDomain(): TunnelState = @@ -544,9 +543,6 @@ internal fun ManagementInterface.WireguardEndpointData.toDomain(): WireguardEndp shadowsocksPortRangesList.map { it.toDomain() }, ) -internal fun ManagementInterface.WireguardRelayEndpointData.toDomain(): WireguardRelayEndpointData = - WireguardRelayEndpointData(daita) - internal fun ManagementInterface.PortRange.toDomain(): PortRange = PortRange(first..last) /** @@ -581,7 +577,7 @@ internal fun ManagementInterface.RelayListCity.toDomain( id = cityCode, relays = relaysList - .filter { it.endpointType == ManagementInterface.Relay.RelayType.WIREGUARD } + .filter { it.endpointData.hasWireguard() } .map { it.toDomain(cityCode) } .sortedWith(RelayNameComparator), ) @@ -595,12 +591,8 @@ internal fun ManagementInterface.Relay.toDomain( active = active, provider = ProviderId(provider), ownership = if (owned) Ownership.MullvadOwned else Ownership.Rented, - daita = - if ( - hasEndpointData() && endpointType == ManagementInterface.Relay.RelayType.WIREGUARD - ) { - ManagementInterface.WireguardRelayEndpointData.parseFrom(endpointData.value).daita - } else false, + daita = endpointData.wireguard.daita, + quic = endpointData.wireguard.hasQuic(), ) private fun Instant.atDefaultZone() = atZone(ZoneId.systemDefault()) diff --git a/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt index df710649c5..e45c8087fd 100644 --- a/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt +++ b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt @@ -26,6 +26,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay10 = RelayItem.Location.Relay( @@ -34,6 +35,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) relay9 assertOrderBothDirection relay10 @@ -48,6 +50,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay9b = RelayItem.Location.Relay( @@ -56,6 +59,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) @@ -71,6 +75,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay1 = RelayItem.Location.Relay( @@ -79,6 +84,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay3 = RelayItem.Location.Relay( @@ -87,6 +93,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay100 = RelayItem.Location.Relay( @@ -95,6 +102,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) relay001 assertOrderBothDirection relay1 @@ -112,6 +120,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay9b = RelayItem.Location.Relay( @@ -120,6 +129,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) @@ -135,6 +145,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay005 = RelayItem.Location.Relay( @@ -143,6 +154,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) relay001 assertOrderBothDirection relay005 @@ -157,6 +169,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relayAr8 = RelayItem.Location.Relay( @@ -165,6 +178,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relaySe5 = RelayItem.Location.Relay( @@ -173,6 +187,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relaySe10 = RelayItem.Location.Relay( @@ -181,6 +196,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) relayAr2 assertOrderBothDirection relayAr8 @@ -197,6 +213,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay2w = RelayItem.Location.Relay( @@ -205,6 +222,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) relay2c assertOrderBothDirection relay2w @@ -219,6 +237,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) val relay22b = RelayItem.Location.Relay( @@ -227,6 +246,7 @@ class RelayNameComparatorTest { provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = false, + quic = false, ) relay22a assertOrderBothDirection relay22b diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt index 27ce80c016..b1df67fea6 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt @@ -85,6 +85,7 @@ sealed interface RelayItem { val ownership: Ownership, override val active: Boolean, val daita: Boolean, + val quic: Boolean, ) : Location { override val name: String = id.code override val hasChildren: Boolean = false diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt index 35397a6a27..724c567595 100644 --- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/RelayItemPreviewData.kt @@ -51,6 +51,7 @@ private fun generateRelayItemRelay( provider = ProviderId("Provider"), ownership = Ownership.MullvadOwned, daita = daita, + quic = false, ) private fun String.generateCountryCode() = 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); }); }); }); diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs index abca6dd2b7..28ce37aec0 100644 --- a/mullvad-api/src/relay_list.rs +++ b/mullvad-api/src/relay_list.rs @@ -7,7 +7,7 @@ use mullvad_types::{location, relay_list}; use talpid_types::net::wireguard; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashSet}, future::Future, net::{IpAddr, Ipv4Addr, Ipv6Addr}, ops::RangeInclusive, @@ -167,7 +167,6 @@ fn into_mullvad_relay( relay: Relay, location: location::Location, endpoint_data: relay_list::RelayEndpointData, - features: relay_list::Features, ) -> relay_list::Relay { relay_list::Relay { hostname: relay.hostname, @@ -182,7 +181,6 @@ fn into_mullvad_relay( weight: relay.weight, endpoint_data, location, - features, } } @@ -247,21 +245,11 @@ struct Relay { impl Relay { fn into_openvpn_mullvad_relay(self, location: location::Location) -> relay_list::Relay { - into_mullvad_relay( - self, - location, - relay_list::RelayEndpointData::Openvpn, - relay_list::Features::empty(), - ) + into_mullvad_relay(self, location, relay_list::RelayEndpointData::Openvpn) } fn into_bridge_mullvad_relay(self, location: location::Location) -> relay_list::Relay { - into_mullvad_relay( - self, - location, - relay_list::RelayEndpointData::Bridge, - relay_list::Features::empty(), - ) + into_mullvad_relay(self, location, relay_list::RelayEndpointData::Bridge) } fn convert_to_lowercase(&mut self) { @@ -353,25 +341,63 @@ struct WireGuardRelay { #[serde(default)] shadowsocks_extra_addr_in: Vec<IpAddr>, #[serde(default)] - features: relay_list::Features, + features: Features, } impl WireGuardRelay { fn into_mullvad_relay(self, location: location::Location) -> relay_list::Relay { - // Sanity check that new 'features' key is in sync with the old Relay keys. - if self.features.daita() { + // Sanity check that new 'features' key is in sync with the old, superceded keys. + // TODO: Remove `self.daita` (and this check 👇) when `features` key has been completely + // rolled out to production. + if self.features.daita.is_some() { debug_assert!(self.daita) } - into_mullvad_relay( - self.relay, - location, + + let relay = self.relay; + let endpoint_data = relay_list::RelayEndpointData::Wireguard(relay_list::WireguardRelayEndpointData { public_key: self.public_key, - daita: self.daita, - shadowsocks_extra_addr_in: self.shadowsocks_extra_addr_in, - }), - self.features, - ) + // FIXME: This hack is forward-compatible with 'features' being rolled out. + // Should unwrap to 'false' once 'daita' field is removed. + daita: self.features.daita.map(|_| true).unwrap_or(self.daita), + shadowsocks_extra_addr_in: HashSet::from_iter(self.shadowsocks_extra_addr_in), + quic: self.features.quic.map(relay_list::Quic::from), + }); + + into_mullvad_relay(relay, location, endpoint_data) + } +} + +/// Extra features enabled on some (Wireguard) relay, such as obfuscation daemons or Daita. +#[derive(Debug, Default, Clone, serde::Deserialize)] +struct Features { + daita: Option<Daita>, + quic: Option<Quic>, +} + +/// DAITA doesn't have any configuration options (exposed by the API). +/// +/// Note, an empty struct is not the same as an empty tuple struct according to serde_json! +#[derive(Debug, Clone, serde::Deserialize)] +struct Daita {} + +/// Parameters for setting up a QUIC obfuscator (connecting to a masque-proxy running on a relay). +#[derive(Debug, Clone, serde::Deserialize)] +struct Quic { + /// In-addresses for the QUIC obfuscator. + /// + /// There may be 0, 1 or 2 in IPs, depending on how many masque-proxy daemons running on the + /// relay. Hopefully the API will tell use the correct amount🤞. + addr_in: Vec<IpAddr>, + /// Authorization token + token: String, + /// Hostname where masque proxy is hosted + domain: String, +} + +impl From<Quic> for relay_list::Quic { + fn from(value: Quic) -> Self { + Self::new(value.addr_in, value.token, value.domain) } } diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 199f4f2000..4d405bcde2 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -6,7 +6,6 @@ import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; import "google/protobuf/duration.proto"; -import "google/protobuf/any.proto"; service ManagementService { // Control and get tunnel state @@ -717,10 +716,27 @@ message RelayListCity { } message Relay { - enum RelayType { - OPENVPN = 0; - BRIDGE = 1; - WIREGUARD = 2; + message RelayData { + message OpenVPN {} + message Bridge {} + message Wireguard { + message Quic { + string domain = 1; + string token = 2; + repeated string addr_in = 3; + } + + bytes public_key = 1; + bool daita = 2; + Quic quic = 3; + repeated string shadowsocks_extra_addr_in = 4; + } + + oneof data { + Wireguard wireguard = 1; + OpenVPN openvpn = 2; + Bridge bridge = 3; + } } string hostname = 1; @@ -731,15 +747,8 @@ message Relay { bool owned = 6; string provider = 7; fixed64 weight = 8; - RelayType endpoint_type = 9; - google.protobuf.Any endpoint_data = 10; - Location location = 11; -} - -message WireguardRelayEndpointData { - bytes public_key = 1; - bool daita = 2; - repeated string shadowsocks_extra_addr_in = 3; + RelayData endpoint_data = 9; + Location location = 10; } message Location { diff --git a/mullvad-management-interface/src/types/conversions/mod.rs b/mullvad-management-interface/src/types/conversions/mod.rs index 0654cbb641..3c610b8d1b 100644 --- a/mullvad-management-interface/src/types/conversions/mod.rs +++ b/mullvad-management-interface/src/types/conversions/mod.rs @@ -60,22 +60,3 @@ impl From<FromProtobufTypeError> for crate::Status { } } } - -/// Converts any message to `google.protobuf.Any`. -fn to_proto_any<T: prost::Message>(type_name: &str, message: T) -> prost_types::Any { - prost_types::Any { - type_url: format!("type.googleapis.com/{type_name}"), - value: message.encode_to_vec(), - } -} - -/// Tries to convert a message from `google.protobuf.Any` to `T`. -fn try_from_proto_any<T: prost::Message + Default>( - type_name: &str, - any_value: prost_types::Any, -) -> Option<T> { - if any_value.type_url != format!("type.googleapis.com/{type_name}") { - return None; - } - T::decode(any_value.value.as_slice()).ok() -} diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs index b002634177..08ec379ba9 100644 --- a/mullvad-management-interface/src/types/conversions/relay_list.rs +++ b/mullvad-management-interface/src/types/conversions/relay_list.rs @@ -1,14 +1,11 @@ use std::{ - net::{Ipv4Addr, Ipv6Addr}, + collections::HashSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, ops::RangeInclusive, str::FromStr, }; -use crate::types::{ - FromProtobufTypeError, - conversions::{bytes_to_pubkey, to_proto_any, try_from_proto_any}, - proto, -}; +use crate::types::{FromProtobufTypeError, conversions::bytes_to_pubkey, proto}; use super::net::try_transport_protocol_from_i32; @@ -125,25 +122,29 @@ impl From<mullvad_types::relay_list::Relay> for proto::Relay { owned: relay.owned, provider: relay.provider, weight: relay.weight, - endpoint_type: match &relay.endpoint_data { - MullvadEndpointData::Openvpn => proto::relay::RelayType::Openvpn as i32, - MullvadEndpointData::Bridge => proto::relay::RelayType::Bridge as i32, - MullvadEndpointData::Wireguard(_) => proto::relay::RelayType::Wireguard as i32, - }, - endpoint_data: match relay.endpoint_data { - MullvadEndpointData::Wireguard(data) => Some(to_proto_any( - "mullvad_daemon.management_interface/WireguardRelayEndpointData", - proto::WireguardRelayEndpointData { - public_key: data.public_key.as_bytes().to_vec(), - daita: data.daita, - shadowsocks_extra_addr_in: data - .shadowsocks_extra_addr_in - .iter() + endpoint_data: { + use proto::relay::RelayData; + use proto::relay::relay_data::{Bridge, Data, OpenVpn, Wireguard, wireguard}; + let data = match relay.endpoint_data { + MullvadEndpointData::Wireguard(data) => { + let shadowsocks_extra_addr_in = data + .shadowsocks_extra_in_addrs() .map(|addr| addr.to_string()) - .collect(), - }, - )), - _ => None, + .collect(); + let public_key = data.public_key.as_bytes().to_vec(); + let daita = data.daita; + let quic = data.quic.map(wireguard::Quic::from); + Data::Wireguard(Wireguard { + public_key, + daita, + shadowsocks_extra_addr_in, + quic, + }) + } + MullvadEndpointData::Bridge => Data::Bridge(Bridge {}), + MullvadEndpointData::Openvpn => Data::Openvpn(OpenVpn {}), + }; + Some(RelayData { data: Some(data) }) }, location: Some(proto::Location { country: relay.location.country, @@ -157,6 +158,38 @@ impl From<mullvad_types::relay_list::Relay> for proto::Relay { } } +impl From<mullvad_types::relay_list::Quic> for proto::relay::relay_data::wireguard::Quic { + fn from(quic: mullvad_types::relay_list::Quic) -> Self { + let domain = quic.hostname().to_owned(); + let token = quic.auth_token().to_owned(); + let addr_in = quic.in_addr().map(|ip| ip.to_string()).collect(); + Self { + domain, + token, + addr_in, + } + } +} + +impl TryFrom<proto::relay::relay_data::wireguard::Quic> for mullvad_types::relay_list::Quic { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::relay::relay_data::wireguard::Quic) -> Result<Self, Self::Error> { + let domain = value.domain; + let token = value.token; + fn parse_addr(addr: String) -> Result<IpAddr, FromProtobufTypeError> { + addr.parse() + .map_err(|_err| FromProtobufTypeError::InvalidArgument("Invalid IP address")) + } + let addr_in = value + .addr_in + .into_iter() + .map(parse_addr) + .collect::<Result<Vec<IpAddr>, FromProtobufTypeError>>()?; + Ok(Self::new(addr_in, token, domain)) + } +} + impl TryFrom<proto::RelayList> for mullvad_types::relay_list::RelayList { type Error = FromProtobufTypeError; @@ -236,44 +269,43 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { relay_list::{Relay as MullvadRelay, RelayEndpointData as MullvadEndpointData}, }; - let endpoint_data = match relay.endpoint_type { - i if i == proto::relay::RelayType::Openvpn as i32 => MullvadEndpointData::Openvpn, - i if i == proto::relay::RelayType::Bridge as i32 => MullvadEndpointData::Bridge, - i if i == proto::relay::RelayType::Wireguard as i32 => { - let data = relay - .endpoint_data - .ok_or(FromProtobufTypeError::InvalidArgument( - "missing endpoint wg data", - ))?; - let data: proto::WireguardRelayEndpointData = try_from_proto_any( - "mullvad_daemon.management_interface/WireguardRelayEndpointData", - data, - ) + let endpoint_data = { + let data = relay + .endpoint_data + .and_then(|endpoint| endpoint.data) .ok_or(FromProtobufTypeError::InvalidArgument( - "invalid endpoint wg data", - ))?; - MullvadEndpointData::Wireguard( - mullvad_types::relay_list::WireguardRelayEndpointData { - public_key: bytes_to_pubkey(&data.public_key)?, - daita: data.daita, - shadowsocks_extra_addr_in: data - .shadowsocks_extra_addr_in - .iter() - .map(|addr| { - addr.parse().map_err(|_err| { - FromProtobufTypeError::InvalidArgument( - "invalid relay IPv6 address", - ) - }) - }) - .collect::<Result<_, FromProtobufTypeError>>()?, - }, - ) - } - _ => { - return Err(FromProtobufTypeError::InvalidArgument( "invalid relay endpoint type", - )); + ))?; + match data { + proto::relay::relay_data::Data::Openvpn(_openvpn) => MullvadEndpointData::Openvpn, + proto::relay::relay_data::Data::Bridge(_bridge) => MullvadEndpointData::Bridge, + proto::relay::relay_data::Data::Wireguard(wireguard) => { + fn parse_addr(addr: &str) -> Result<IpAddr, FromProtobufTypeError> { + addr.parse().map_err(|_err| { + FromProtobufTypeError::InvalidArgument("Invalid IP address") + }) + } + + let public_key = bytes_to_pubkey(&wireguard.public_key)?; + let daita = wireguard.daita; + let quic = wireguard + .quic + .map(mullvad_types::relay_list::Quic::try_from) + .transpose()?; + let shadowsocks_extra_addr_in = wireguard + .shadowsocks_extra_addr_in + .iter() + .map(String::as_ref) + .map(parse_addr) + .collect::<Result<HashSet<IpAddr>, FromProtobufTypeError>>()?; + let data = mullvad_types::relay_list::WireguardRelayEndpointData { + public_key, + daita, + quic, + shadowsocks_extra_addr_in, + }; + MullvadEndpointData::Wireguard(data) + } } }; @@ -286,10 +318,6 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { }) .transpose()?; - // TODO: Eventually, we will need to decide how to represent extra relay features in the - // protobuf message. - let features = mullvad_types::relay_list::Features::default(); - let relay = MullvadRelay { hostname: relay.hostname, ipv4_addr_in: relay.ipv4_addr_in.parse().map_err(|_err| { @@ -316,7 +344,6 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { }) .ok_or("missing relay location") .map_err(FromProtobufTypeError::InvalidArgument)?, - features, }; Ok(relay) diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs index 7135d41fd0..1a5a11e69c 100644 --- a/mullvad-relay-selector/src/relay_selector/helpers.rs +++ b/mullvad-relay-selector/src/relay_selector/helpers.rs @@ -124,7 +124,7 @@ pub fn get_shadowsocks_obfuscator( let port = settings.port; let extra_addrs = match &relay.endpoint_data { mullvad_types::relay_list::RelayEndpointData::Wireguard(wg) => { - &wg.shadowsocks_extra_addr_in + wg.shadowsocks_extra_in_addrs() } _ => panic!("expected wireguard relay"), }; @@ -132,7 +132,7 @@ pub fn get_shadowsocks_obfuscator( let endpoint = get_shadowsocks_obfuscator_inner( endpoint.peer.endpoint.ip(), non_extra_port_ranges, - extra_addrs, + extra_addrs.copied(), port, )?; @@ -143,7 +143,7 @@ pub fn get_shadowsocks_obfuscator( } pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<SelectedObfuscator> { - let quic = relay.features.quic()?; + let quic = relay.wireguard()?.quic()?; let config = { let hostname = quic.hostname().to_string(); let endpoint = match ip_version { @@ -168,14 +168,13 @@ pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<Select fn get_shadowsocks_obfuscator_inner<R: RangeBounds<u16> + Iterator<Item = u16> + Clone>( wg_in_addr: IpAddr, wg_in_addr_port_ranges: &[R], - extra_in_addrs: &[IpAddr], + extra_in_addrs: impl IntoIterator<Item = IpAddr>, desired_port: Constraint<u16>, ) -> Result<SocketAddr, Error> { // Filter out addresses for the wrong address family let extra_in_addrs: Vec<_> = extra_in_addrs - .iter() + .into_iter() .filter(|addr| addr.is_ipv4() == wg_in_addr.is_ipv4()) - .copied() .collect(); let in_ip = extra_in_addrs @@ -244,7 +243,7 @@ mod tests { SHADOWSOCKS_EXTRA_PORT_RANGES, get_shadowsocks_obfuscator_inner, port_if_in_range, }; use mullvad_types::constraints::Constraint; - use std::{net::IpAddr, ops::RangeInclusive}; + use std::{iter, net::IpAddr, ops::RangeInclusive}; /// Test whether select ports are available when relay has no extra IPs #[test] @@ -255,7 +254,7 @@ mod tests { let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap(); let selected_addr = - get_shadowsocks_obfuscator_inner(wg_in_ip, PORT_RANGES, &[], Constraint::Any) + get_shadowsocks_obfuscator_inner(wg_in_ip, PORT_RANGES, iter::empty(), Constraint::Any) .expect("should find valid port without constraint"); assert_eq!(selected_addr.ip(), wg_in_ip); @@ -267,7 +266,7 @@ mod tests { let selected_addr = get_shadowsocks_obfuscator_inner( wg_in_ip, PORT_RANGES, - &[], + iter::empty(), Constraint::Only(WITHIN_RANGE_PORT), ) .expect("should find within-range port"); @@ -281,7 +280,7 @@ mod tests { let selected_addr = get_shadowsocks_obfuscator_inner( wg_in_ip, PORT_RANGES, - &[], + iter::empty(), Constraint::Only(OUT_OF_RANGE_PORT), ); assert!( @@ -297,13 +296,13 @@ mod tests { const OUT_OF_RANGE_PORT: u16 = 1; let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap(); - let extra_in_addrs: &[IpAddr] = - &["1.3.3.7".parse().unwrap(), "192.0.2.123".parse().unwrap()]; + let extra_in_addrs: Vec<IpAddr> = + vec!["1.3.3.7".parse().unwrap(), "192.0.2.123".parse().unwrap()]; let selected_addr = get_shadowsocks_obfuscator_inner( wg_in_ip, PORT_RANGES, - extra_in_addrs, + extra_in_addrs.clone(), Constraint::Any, ) .expect("should find valid port without constraint"); @@ -317,7 +316,7 @@ mod tests { let selected_addr = get_shadowsocks_obfuscator_inner( wg_in_ip, PORT_RANGES, - extra_in_addrs, + extra_in_addrs.clone(), Constraint::Only(OUT_OF_RANGE_PORT), ) .expect("expected selected address to be returned"); @@ -340,12 +339,12 @@ mod tests { const OUT_OF_RANGE_PORT: u16 = 1; let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap(); - let extra_in_addrs: &[IpAddr] = &["::2".parse().unwrap()]; + let extra_in_addrs: Vec<IpAddr> = vec!["::2".parse().unwrap()]; let selected_addr = get_shadowsocks_obfuscator_inner( wg_in_ip, PORT_RANGES, - extra_in_addrs, + extra_in_addrs.clone(), Constraint::Any, ) .expect("should find valid port without constraint"); @@ -359,7 +358,7 @@ mod tests { let selected_addr = get_shadowsocks_obfuscator_inner( wg_in_ip, PORT_RANGES, - extra_in_addrs, + extra_in_addrs.clone(), Constraint::Only(OUT_OF_RANGE_PORT), ); assert!( diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs index 8133677b87..52f0cc600a 100644 --- a/mullvad-relay-selector/src/relay_selector/matcher.rs +++ b/mullvad-relay-selector/src/relay_selector/matcher.rs @@ -145,7 +145,7 @@ fn filter_on_obfuscation( ) } // QUIC is only enabled on some relays - ObfuscationQuery::Quic => relay.features.quic().is_some(), + ObfuscationQuery::Quic => relay.wireguard().is_some_and(|wg| wg.quic().is_some()), // Other relays are compatible with this query ObfuscationQuery::Off | ObfuscationQuery::Auto | ObfuscationQuery::Udp2tcp(_) => true, } diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 532e78a076..ad91c192d5 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -27,9 +27,9 @@ use mullvad_types::{ RelayConstraints, RelayOverride, RelaySettings, TransportPort, }, relay_list::{ - BridgeEndpointData, Features, OpenVpnEndpoint, OpenVpnEndpointData, Quic, Relay, - RelayEndpointData, RelayList, RelayListCity, RelayListCountry, ShadowsocksEndpointData, - WireguardEndpointData, WireguardRelayEndpointData, + BridgeEndpointData, OpenVpnEndpoint, OpenVpnEndpointData, Quic, Relay, RelayEndpointData, + RelayList, RelayListCity, RelayListCountry, ShadowsocksEndpointData, WireguardEndpointData, + WireguardRelayEndpointData, }, }; @@ -42,6 +42,10 @@ static DUMMY_LOCATION: LazyLock<Location> = LazyLock::new(|| Location { longitude: 11.97, }); +static WIREGUARD_PUBKEY: LazyLock<PublicKey> = LazyLock::new(|| { + PublicKey::from_base64("BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=").unwrap() +}); + static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { etag: None, countries: vec![RelayListCountry { @@ -64,25 +68,19 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { owned: true, provider: "provider0".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - daita: true, - shadowsocks_extra_addr_in: vec![], - }), + endpoint_data: RelayEndpointData::Wireguard( + WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()) + .set_daita(true) + .set_quic(Quic::new( + vec![ + "185.213.154.68".parse().unwrap(), + "2a03:1b20:5:f011::a09f".parse().unwrap(), + ], + "Bearer test".to_owned(), + "se9-wireguard.blockerad.eu".to_owned(), + )), + ), location: DUMMY_LOCATION.clone(), - features: Features::default() - .configure_daita() - .configure_quic(Quic::new( - vec![ - "185.213.154.68".parse().unwrap(), - "2a03:1b20:5:f011::a09f".parse().unwrap(), - ], - "Bearer test".to_owned(), - "se9-wireguard.blockerad.eu".to_owned(), - )), }, Relay { hostname: "se10-wireguard".to_string(), @@ -95,16 +93,11 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { owned: false, provider: "provider1".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - daita: false, - shadowsocks_extra_addr_in: vec![], - }), + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData::new( + PublicKey::from_base64("BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=") + .unwrap(), + )), location: DUMMY_LOCATION.clone(), - features: Features::default(), }, Relay { hostname: "se11-wireguard".to_string(), @@ -117,16 +110,14 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { owned: false, provider: "provider2".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + endpoint_data: RelayEndpointData::Wireguard( + WireguardRelayEndpointData::new( + PublicKey::from_base64("BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=") + .unwrap(), ) - .unwrap(), - daita: true, - shadowsocks_extra_addr_in: vec![], - }), + .set_daita(true), + ), location: DUMMY_LOCATION.clone(), - features: Features::default().configure_daita(), }, Relay { hostname: "se-got-001".to_string(), @@ -141,7 +132,6 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { weight: 1, endpoint_data: RelayEndpointData::Openvpn, location: DUMMY_LOCATION.clone(), - features: Features::default(), }, Relay { hostname: "se-got-002".to_string(), @@ -156,7 +146,6 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { weight: 1, endpoint_data: RelayEndpointData::Openvpn, location: DUMMY_LOCATION.clone(), - features: Features::default(), }, Relay { hostname: "se-got-br-001".to_string(), @@ -171,7 +160,6 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { weight: 1, endpoint_data: RelayEndpointData::Bridge, location: DUMMY_LOCATION.clone(), - features: Features::default(), }, SHADOWSOCKS_RELAY.clone(), ], @@ -250,13 +238,13 @@ static SHADOWSOCKS_RELAY: LazyLock<Relay> = LazyLock::new(|| Relay { owned: true, provider: "provider0".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64("eaNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=").unwrap(), - daita: false, - shadowsocks_extra_addr_in: SHADOWSOCKS_RELAY_EXTRA_ADDRS.to_vec(), - }), + endpoint_data: RelayEndpointData::Wireguard( + WireguardRelayEndpointData::new( + PublicKey::from_base64("eaNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=").unwrap(), + ) + .add_shadowsocks_extra_in_addrs(SHADOWSOCKS_RELAY_EXTRA_ADDRS.iter().copied()), + ), location: DUMMY_LOCATION.clone(), - features: Features::default(), }); const SHADOWSOCKS_RELAY_IPV4: Ipv4Addr = Ipv4Addr::new(123, 123, 123, 1); const SHADOWSOCKS_RELAY_IPV6: Ipv6Addr = Ipv6Addr::new(0x123, 0, 0, 0, 0, 0, 0, 2); @@ -586,16 +574,10 @@ fn test_wireguard_entry() { owned: true, provider: "provider0".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - daita: false, - shadowsocks_extra_addr_in: vec![], - }), + endpoint_data: RelayEndpointData::Wireguard( + WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()), + ), location: DUMMY_LOCATION.clone(), - features: Features::default(), }, Relay { hostname: "se10-wireguard".to_string(), @@ -608,16 +590,10 @@ fn test_wireguard_entry() { owned: false, provider: "provider1".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - daita: false, - shadowsocks_extra_addr_in: vec![], - }), + endpoint_data: RelayEndpointData::Wireguard( + WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()), + ), location: DUMMY_LOCATION.clone(), - features: Features::default(), }, ], }], @@ -1280,16 +1256,10 @@ fn test_include_in_country() { owned: true, provider: "31173".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - shadowsocks_extra_addr_in: vec![], - daita: false, - }), + endpoint_data: RelayEndpointData::Wireguard( + WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()), + ), location: DUMMY_LOCATION.clone(), - features: Features::default(), }, Relay { hostname: "se10-wireguard".to_string(), @@ -1302,16 +1272,10 @@ fn test_include_in_country() { owned: false, provider: "31173".to_string(), weight: 1, - endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { - public_key: PublicKey::from_base64( - "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", - ) - .unwrap(), - shadowsocks_extra_addr_in: vec![], - daita: false, - }), + endpoint_data: RelayEndpointData::Wireguard( + WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()), + ), location: DUMMY_LOCATION.clone(), - features: Features::default(), }, ], }], diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 99aaf60059..c6f1693c14 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -1,6 +1,7 @@ use crate::location::{CityCode, CountryCode, Location}; use serde::{Deserialize, Serialize}; use std::{ + collections::HashSet, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, ops::RangeInclusive, }; @@ -88,69 +89,28 @@ pub struct Relay { pub weight: u64, pub endpoint_data: RelayEndpointData, pub location: Location, - #[serde(default)] - pub features: Features, -} - -/// Extra features enabled on some (Wireguard) relay, such as obfuscation daemons or Daita. -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Features { - daita: Option<Daita>, - quic: Option<Quic>, } -impl Features { - /// Equivalent to a relay without any additional features. - pub fn empty() -> Features { - Features { - daita: None, - quic: None, +impl Relay { + /// If self is a Wireguard relay, we sometimes want to peek on its extra data. + pub fn wireguard(&self) -> Option<&WireguardRelayEndpointData> { + match &self.endpoint_data { + RelayEndpointData::Wireguard(wireguard_relay_endpoint_data) => { + Some(wireguard_relay_endpoint_data) + } + RelayEndpointData::Openvpn | RelayEndpointData::Bridge => None, } } - - /// Whether Daita is enabled - pub fn daita(&self) -> bool { - self.daita.is_some() - } - - /// Whether Quic is enabled and its config - pub fn quic(&self) -> Option<&Quic> { - self.quic.as_ref() - } - - /// Enable Daita for this relay - pub fn configure_daita(self) -> Self { - let daita = Some(Daita {}); - Self { daita, ..self } - } - - /// Configure QUIC for this relay - pub fn configure_quic(self, options: Quic) -> Self { - let quic = Some(options); - Self { quic, ..self } - } -} - -impl Default for Features { - fn default() -> Self { - Features::empty() - } } -/// DAITA doesn't have any configuration options (exposed by the API). -/// -/// Note, an empty struct is not the same as an empty tuple struct according to serde_json! -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Daita {} - /// Parameters for setting up a QUIC obfuscator (connecting to a masque-proxy running on a relay). -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct Quic { /// In-addresses for the QUIC obfuscator. /// /// There may be 0, 1 or 2 in IPs, depending on how many masque-proxy daemons running on the /// relay. Hopefully the API will tell use the correct amount🤞. - addr_in: Vec<IpAddr>, + addr_in: HashSet<IpAddr>, /// Authorization token token: String, /// Hostname where masque proxy is hosted @@ -158,7 +118,8 @@ pub struct Quic { } impl Quic { - pub fn new(addr_in: Vec<IpAddr>, token: String, domain: String) -> Self { + pub fn new(addr_in: impl IntoIterator<Item = IpAddr>, token: String, domain: String) -> Self { + let addr_in = HashSet::from_iter(addr_in); Self { addr_in, token, @@ -201,6 +162,10 @@ impl Quic { pub fn auth_token(&self) -> &str { &self.token } + + pub fn in_addr(&self) -> impl Iterator<Item = IpAddr> { + self.addr_in.iter().copied() + } } impl Relay { @@ -230,7 +195,7 @@ impl PartialEq for Relay { /// # Example /// /// ```rust - /// # use mullvad_types::{relay_list::{Relay, Features}, relay_list::{RelayEndpointData, WireguardRelayEndpointData}}; + /// # use mullvad_types::{relay_list::Relay, relay_list::{RelayEndpointData, WireguardRelayEndpointData}}; /// # use talpid_types::net::wireguard::PublicKey; /// /// let relay = Relay { @@ -250,7 +215,8 @@ impl PartialEq for Relay { /// # ) /// # .unwrap(), /// # daita: false, - /// # shadowsocks_extra_addr_in: vec![], + /// # shadowsocks_extra_addr_in: Default::default(), + /// # quic: None, /// # }), /// # location: mullvad_types::location::Location { /// # country: "Sweden".to_string(), @@ -260,7 +226,6 @@ impl PartialEq for Relay { /// # latitude: 57.71, /// # longitude: 11.97, /// # }, - /// # features: Features::default(), /// }; /// /// let mut different_relay = relay.clone(); @@ -338,17 +303,62 @@ impl Default for WireguardEndpointData { } /// Contains data about specific WireGuard endpoints, i.e. their public keys. -#[derive(Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Debug)] +#[derive(Clone, Eq, PartialEq, Deserialize, Serialize, Debug)] pub struct WireguardRelayEndpointData { /// Public key used by the relay peer pub public_key: wireguard::PublicKey, - /// Whether the server supports DAITA - /// FIXME: This has been superceded by [Features] + [Daita]. + /// Whether the relay supports DAITA #[serde(default)] pub daita: bool, + /// Parameters for connecting to the masque-proxy running on the relay. + #[serde(default)] + pub quic: Option<Quic>, /// Optional IP addresses used by Shadowsocks #[serde(default)] - pub shadowsocks_extra_addr_in: Vec<IpAddr>, + pub shadowsocks_extra_addr_in: HashSet<IpAddr>, +} + +impl WireguardRelayEndpointData { + pub fn new(public_key: wireguard::PublicKey) -> Self { + Self { + public_key, + daita: Default::default(), + quic: Default::default(), + shadowsocks_extra_addr_in: Default::default(), + } + } + + pub fn set_daita(self, enabled: bool) -> Self { + Self { + daita: enabled, + ..self + } + } + + pub fn set_quic(self, quic: Quic) -> Self { + Self { + quic: Some(quic), + ..self + } + } + + /// Add `in_addrs` to the existing shadowsocks extra in addressess. + pub fn add_shadowsocks_extra_in_addrs(self, in_addrs: impl Iterator<Item = IpAddr>) -> Self { + let in_addrs = self.shadowsocks_extra_in_addrs().copied().chain(in_addrs); + Self { + shadowsocks_extra_addr_in: HashSet::from_iter(in_addrs), + ..self + } + } + + pub fn shadowsocks_extra_in_addrs(&self) -> impl Iterator<Item = &IpAddr> { + self.shadowsocks_extra_addr_in.iter() + } + + // Is this really needed if `self.quic` is pub? + pub fn quic(&self) -> Option<&Quic> { + self.quic.as_ref() + } } #[derive(Debug, Default, Clone, Deserialize, Serialize)] |
