diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-09-30 16:09:50 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-09-30 16:09:50 +0200 |
| commit | 30bae1819de1ede5c4b9334dbd2b6cf945e237fd (patch) | |
| tree | 05e7334b2d134b47b32e1f1c725ffcf490d9a6c2 | |
| parent | 51ede1644ebb79ef2f2d054a03b278ea78e5df63 (diff) | |
| parent | 6ee119d8e435c91a12392a0aae94303f448c3676 (diff) | |
| download | mullvadvpn-30bae1819de1ede5c4b9334dbd2b6cf945e237fd.tar.xz mullvadvpn-30bae1819de1ede5c4b9334dbd2b6cf945e237fd.zip | |
Merge branch 'add-remaining-lwo'
31 files changed, 296 insertions, 54 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8196481230..080b3899ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Line wrap the file at 100 chars. Th ### Added - Add settings reset command to the CLI ('mullvad reset-settings'). - Make feature indicators in connection panel navigate to the relevant setting when clicked. +- Add new LWO obfuscation method for WireGuard. ### Changed - Add validation for API access methods to only allow unique names. Access methods with 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 4601995d78..65ab420be3 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 @@ -730,6 +730,7 @@ internal fun ManagementInterface.FeatureIndicator.toDomain() = ManagementInterface.FeatureIndicator.MULTIHOP -> FeatureIndicator.MULTIHOP ManagementInterface.FeatureIndicator.DAITA_MULTIHOP -> FeatureIndicator.DAITA_MULTIHOP ManagementInterface.FeatureIndicator.QUIC -> FeatureIndicator.QUIC + ManagementInterface.FeatureIndicator.LWO, ManagementInterface.FeatureIndicator.LOCKDOWN_MODE, ManagementInterface.FeatureIndicator.BRIDGE_MODE, ManagementInterface.FeatureIndicator.CUSTOM_MSS_FIX, diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot index 15e32483c0..dc274e6a5f 100644 --- a/desktop/packages/mullvad-vpn/locales/messages.pot +++ b/desktop/packages/mullvad-vpn/locales/messages.pot @@ -2727,6 +2727,10 @@ msgctxt "wireguard-settings-view" msgid "It does this by performing an extra key exchange using a quantum safe algorithm and mixing the result into WireGuard’s regular encryption. This extra step uses approximately 500 kiB of traffic every time a new tunnel is established." msgstr "" +msgctxt "wireguard-settings-view" +msgid "LWO" +msgstr "" + #. The title for the WireGuard MTU setting. MTU stands for Maximum #. Transmission Unit and controls the maximum size of packets sent over #. the VPN tunnel. diff --git a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts index 8db5d1b5f2..b7871f4c76 100644 --- a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts +++ b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts @@ -364,6 +364,11 @@ export class DaemonRpc extends GrpcClient { grpcTypes.ObfuscationSettings.SelectedObfuscation.QUIC, ); break; + case ObfuscationType.lwo: + grpcObfuscationSettings.setSelectedObfuscation( + grpcTypes.ObfuscationSettings.SelectedObfuscation.LWO, + ); + break; } if (obfuscationSettings.udp2tcpSettings) { 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 d691973775..f29979c520 100644 --- a/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts +++ b/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts @@ -133,12 +133,14 @@ function convertFromRelayListRelay(relay: grpcTypes.Relay): IRelayListHostname { const daita = wireguard ? wireguard.daita : false; const quic = wireguard?.quic ? quicFromRelayType(wireguard.quic) : undefined; + const lwo = wireguard ? wireguard.lwo : false; return { ...relayObject, endpointType, daita, quic, + lwo, }; } @@ -412,6 +414,8 @@ function convertFromFeatureIndicator( return FeatureIndicator.shadowsocks; case grpcTypes.FeatureIndicator.QUIC: return FeatureIndicator.quic; + case grpcTypes.FeatureIndicator.LWO: + return FeatureIndicator.lwo; } } @@ -725,6 +729,9 @@ function convertFromObfuscationSettings( case grpcTypes.ObfuscationSettings.SelectedObfuscation.QUIC: selectedObfuscationType = ObfuscationType.quic; break; + case grpcTypes.ObfuscationSettings.SelectedObfuscation.LWO: + selectedObfuscationType = ObfuscationType.lwo; + break; } return { 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 59a48c4130..643b8a6dbb 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 @@ -10,6 +10,7 @@ import { filterLocations, filterLocationsByDaita, filterLocationsByEndPointType, + filterLocationsByLwo, filterLocationsByQuic, getLocationsExpandedBySearch, searchForLocations, @@ -76,6 +77,9 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) { const quic = useSelector( (state) => state.settings.obfuscationSettings.selectedObfuscation === ObfuscationType.quic, ); + const lwo = useSelector( + (state) => state.settings.obfuscationSettings.selectedObfuscation === ObfuscationType.lwo, + ); const fullRelayList = useSelector((state) => state.settings.relayLocations); const relaySettings = useNormalRelaySettings(); @@ -113,12 +117,16 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) { ipVersion, ); }, [quic, relayListForDaita, locationType, tunnelProtocol, multihop, ipVersion]); + // Only show relays that have LWO endpoints when LWO is enabled. + const relayListForLwo = useMemo(() => { + return filterLocationsByLwo(relayListForQuic, lwo, tunnelProtocol, locationType, multihop); + }, [lwo, relayListForQuic, locationType, tunnelProtocol, multihop]); // Filters the relays to only keep the relays matching the currently selected filters, e.g. // ownership and providers const relayListForFilters = useMemo(() => { - return filterLocations(relayListForQuic, relaySettings?.ownership, relaySettings?.providers); - }, [relaySettings?.ownership, relaySettings?.providers, relayListForQuic]); + return filterLocations(relayListForLwo, relaySettings?.ownership, relaySettings?.providers); + }, [relaySettings?.ownership, relaySettings?.providers, relayListForLwo]); // 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 2bcae3428b..5c75cefdaf 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 @@ -11,6 +11,7 @@ import { useRelaySettingsUpdater } from '../../lib/constraint-updater'; import { daitaFilterActive, filterSpecialLocations, + lwoFilterActive, quicFilterActive, } from '../../lib/filter-locations'; import { useHistory } from '../../lib/history'; @@ -70,7 +71,11 @@ export default function SelectLocation() { const quic = useSelector( (state) => state.settings.obfuscationSettings.selectedObfuscation === ObfuscationType.quic, ); + const lwo = useSelector( + (state) => state.settings.obfuscationSettings.selectedObfuscation === ObfuscationType.lwo, + ); const showQuicFilter = quicFilterActive(quic, locationType, tunnelProtocol, multihop); + const showLwoFilter = lwoFilterActive(lwo, locationType, tunnelProtocol, multihop); const showDaitaFilter = daitaFilterActive( daita, directOnly, @@ -129,7 +134,11 @@ export default function SelectLocation() { const showOwnershipFilter = ownership !== Ownership.any; const showProvidersFilter = providers.length > 0; const showFilters = - showOwnershipFilter || showProvidersFilter || showDaitaFilter || showQuicFilter; + showOwnershipFilter || + showProvidersFilter || + showDaitaFilter || + showQuicFilter || + showLwoFilter; return ( <BackAction action={onClose}> <Layout> @@ -226,6 +235,23 @@ export default function SelectLocation() { </FilterChip.Text> </FilterChip> )} + + {showLwoFilter && ( + <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: 'LWO' }, + )} + </FilterChip.Text> + </FilterChip> + )} </Flex> )} diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts index e8cb982c88..3d285439b4 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts @@ -190,6 +190,10 @@ export const useGetFeatureIndicator = () => { label: messages.pgettext('wireguard-settings-view', 'Obfuscation'), onClick: gotoObfuscation, }, + [FeatureIndicator.lwo]: { + label: messages.pgettext('wireguard-settings-view', 'Obfuscation'), + onClick: gotoObfuscation, + }, [FeatureIndicator.multihop]: { label: // TRANSLATORS: This refers to the multihop setting in the VPN settings view. This is diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx index aa9b1e7835..7058548c36 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx @@ -107,6 +107,9 @@ export function ObfuscationSettings() { <SettingsListbox.BaseOption value={ObfuscationType.quic}> {messages.pgettext('wireguard-settings-view', 'QUIC')} </SettingsListbox.BaseOption> + <SettingsListbox.BaseOption value={ObfuscationType.lwo}> + {messages.pgettext('wireguard-settings-view', 'LWO')} + </SettingsListbox.BaseOption> <SettingsListbox.BaseOption value={ObfuscationType.off}> {messages.gettext('Off')} </SettingsListbox.BaseOption> 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 2c125cdb9a..0149f57752 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts @@ -50,6 +50,19 @@ export function filterLocationsByQuic( : locations; } +export function filterLocationsByLwo( + locations: IRelayLocationCountryRedux[], + lwo: boolean, + tunnelProtocol: TunnelProtocol, + locationType: LocationType, + multihop: boolean, +): IRelayLocationCountryRedux[] { + const lwoOnRelay = (relay: IRelayLocationRelayRedux) => relay.lwo; + return lwoFilterActive(lwo, locationType, tunnelProtocol, multihop) + ? filterLocationsImpl(locations, lwoOnRelay) + : locations; +} + export function quicFilterActive( quic: boolean, locationType: LocationType, @@ -62,6 +75,18 @@ export function quicFilterActive( return quic && isEntry && tunnelProtocol !== 'openvpn'; } +export function lwoFilterActive( + lwo: boolean, + locationType: LocationType, + tunnelProtocol: TunnelProtocol, + multihop: boolean, +) { + const isEntry = multihop + ? locationType === LocationType.entry + : locationType === LocationType.exit; + return lwo && isEntry && tunnelProtocol !== 'openvpn'; +} + 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 285a0500e2..0def2b2d98 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts @@ -79,6 +79,7 @@ export interface IRelayLocationRelayRedux { endpointType: RelayEndpointType; daita: boolean; quic?: Quic; + lwo: boolean; } 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 f1c3dd8ac4..f42a9292e9 100644 --- a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts +++ b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts @@ -232,6 +232,7 @@ export enum FeatureIndicator { udp2tcp, shadowsocks, quic, + lwo, lanSharing, dnsContentBlockers, customDns, @@ -398,6 +399,7 @@ export interface IRelayListHostname { daita: boolean; // The absence of this value signals that the relay does not deploy QUIC. quic?: Quic; + lwo: boolean; } export type Quic = { @@ -526,6 +528,7 @@ export enum ObfuscationType { udp2tcp, shadowsocks, quic, + lwo, } export type ObfuscationSettings = { diff --git a/desktop/packages/mullvad-vpn/test/e2e/mock-data.ts b/desktop/packages/mullvad-vpn/test/e2e/mock-data.ts index 4bd4436772..0c21bab06b 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/mock-data.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/mock-data.ts @@ -22,6 +22,7 @@ const relayList: IRelayList = { owned: true, endpointType: 'wireguard', daita: true, + lwo: true, }, { hostname: 'mullvad-wireguard-23', @@ -33,6 +34,7 @@ const relayList: IRelayList = { owned: true, endpointType: 'wireguard', daita: true, + lwo: false, }, { hostname: 'another-provider-wireguard-1', @@ -44,6 +46,7 @@ const relayList: IRelayList = { owned: false, endpointType: 'wireguard', daita: true, + lwo: false, }, { hostname: 'mullvad-wireguard-quic-1', @@ -60,6 +63,7 @@ const relayList: IRelayList = { domain: '', token: '', }, + lwo: false, }, { hostname: 'mullvad-openvpn-1', @@ -70,7 +74,8 @@ const relayList: IRelayList = { weight: 0, owned: true, endpointType: 'openvpn', - daita: true, + daita: false, + lwo: false, }, ], }, diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts index 89135a50b9..72fb1e7d61 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts @@ -77,6 +77,13 @@ const featureIndicatorWithOption: FeatureIndicatorWithOptionTestOption[] = [ option: { name: 'Obfuscation', type: 'listbox' }, }, { + testId: 'LWO', + featureIndicator: FeatureIndicator.lwo, + route: RoutePath.wireguardSettings, + featureIndicatorLabel: 'Obfuscation', + option: { name: 'Obfuscation', type: 'listbox' }, + }, + { testId: 'multihop', featureIndicator: FeatureIndicator.multihop, route: RoutePath.multihopSettings, 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 ea3e033c30..1a30729ac8 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 @@ -62,10 +62,15 @@ export const createHelpers = (page: Page, routes: RoutesObjectModel, utils: Mock ), ); - const locateRelaysByObfuscation = (relayList: IRelayList): LocatedRelay[] => + const locateRelaysByObfuscation = ( + relayList: IRelayList, + relayCondition: (relay: IRelayListHostname) => boolean, + ): LocatedRelay[] => relayList.countries.flatMap((country) => country.cities.flatMap((city) => - city.relays.filter((relay) => relay.quic).map((relay) => ({ country, city, relay })), + city.relays + .filter((relay) => relayCondition(relay)) + .map((relay) => ({ country, city, relay })), ), ); 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 d4ea5343f7..3826d9e27e 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 @@ -289,7 +289,30 @@ test.describe('Select location', () => { } await util.ipc.settings[''].notify(settings); - const locatedRelays = helpers.locateRelaysByObfuscation(relayList); + const locatedRelays = helpers.locateRelaysByObfuscation( + relayList, + (relay) => 'quic' in relay, + ); + const relays = locatedRelays.map((locatedRelay) => locatedRelay.relay); + const relayNames = relays.map((relay) => relay.hostname); + + await helpers.expandLocatedRelays(locatedRelays); + + const buttons = routes.selectLocation.getRelaysMatching(relayNames); + + // Expect all filtered relays to have a button + await expect(buttons).toHaveCount(relays.length); + }); + }); + test.describe('Filter by LWO', () => { + test('Should apply filter when LWO obfuscation is selected', async () => { + const settings = getDefaultSettings(); + if ('normal' in settings.relaySettings) { + settings.obfuscationSettings.selectedObfuscation = ObfuscationType.lwo; + } + await util.ipc.settings[''].notify(settings); + + const locatedRelays = helpers.locateRelaysByObfuscation(relayList, (relay) => relay.lwo); const relays = locatedRelays.map((locatedRelay) => locatedRelay.relay); const relayNames = relays.map((relay) => relay.hostname); diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs index 6fdc073efd..f36a375b85 100644 --- a/mullvad-api/src/relay_list.rs +++ b/mullvad-api/src/relay_list.rs @@ -363,6 +363,7 @@ impl WireGuardRelay { 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), + lwo: self.features.lwo.is_some(), }); into_mullvad_relay(relay, location, endpoint_data) @@ -374,6 +375,7 @@ impl WireGuardRelay { struct Features { daita: Option<Daita>, quic: Option<Quic>, + lwo: Option<Lwo>, } /// DAITA doesn't have any configuration options (exposed by the API). @@ -405,6 +407,12 @@ impl From<Quic> for relay_list::Quic { } } +/// LWO 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 Lwo {} + #[derive(Debug, serde::Deserialize)] struct Bridges { shadowsocks: Vec<relay_list::ShadowsocksEndpointData>, diff --git a/mullvad-cli/Cargo.toml b/mullvad-cli/Cargo.toml index c09fd64d4a..058951bff4 100644 --- a/mullvad-cli/Cargo.toml +++ b/mullvad-cli/Cargo.toml @@ -14,9 +14,6 @@ workspace = true name = "mullvad" path = "src/main.rs" -[features] -lwo = ["mullvad-types/lwo"] - [dependencies] anyhow = { workspace = true } chrono = { workspace = true } @@ -31,7 +28,7 @@ mullvad-version = { path = "../mullvad-version" } talpid-types = { path = "../talpid-types" } mullvad-management-interface = { path = "../mullvad-management-interface" } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 207b977070..21e00573af 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -322,14 +322,15 @@ enum FeatureIndicator { UDP_2_TCP = 5; SHADOWSOCKS = 6; QUIC = 7; - LAN_SHARING = 8; - DNS_CONTENT_BLOCKERS = 9; - CUSTOM_DNS = 10; - SERVER_IP_OVERRIDE = 11; - CUSTOM_MTU = 12; - CUSTOM_MSS_FIX = 13; - DAITA = 14; - DAITA_MULTIHOP = 15; + LWO = 8; + LAN_SHARING = 9; + DNS_CONTENT_BLOCKERS = 10; + CUSTOM_DNS = 11; + SERVER_IP_OVERRIDE = 12; + CUSTOM_MTU = 13; + CUSTOM_MSS_FIX = 14; + DAITA = 15; + DAITA_MULTIHOP = 16; } message ObfuscationInfo { @@ -744,6 +745,7 @@ message Relay { bool daita = 2; Quic quic = 3; repeated string shadowsocks_extra_addr_in = 4; + bool lwo = 5; } oneof data { diff --git a/mullvad-management-interface/src/types/conversions/features.rs b/mullvad-management-interface/src/types/conversions/features.rs index c37f1e37ac..479827c118 100644 --- a/mullvad-management-interface/src/types/conversions/features.rs +++ b/mullvad-management-interface/src/types/conversions/features.rs @@ -12,6 +12,7 @@ impl From<mullvad_types::features::FeatureIndicator> for proto::FeatureIndicator mullvad_types::features::FeatureIndicator::Udp2Tcp => Udp2Tcp, mullvad_types::features::FeatureIndicator::Shadowsocks => Shadowsocks, mullvad_types::features::FeatureIndicator::Quic => Quic, + mullvad_types::features::FeatureIndicator::Lwo => Lwo, mullvad_types::features::FeatureIndicator::LanSharing => LanSharing, mullvad_types::features::FeatureIndicator::DnsContentBlockers => DnsContentBlockers, mullvad_types::features::FeatureIndicator::CustomDns => CustomDns, @@ -35,6 +36,7 @@ impl From<proto::FeatureIndicator> for mullvad_types::features::FeatureIndicator proto::FeatureIndicator::Udp2Tcp => Self::Udp2Tcp, proto::FeatureIndicator::Shadowsocks => Self::Shadowsocks, proto::FeatureIndicator::Quic => Self::Quic, + proto::FeatureIndicator::Lwo => Self::Lwo, proto::FeatureIndicator::LanSharing => Self::LanSharing, proto::FeatureIndicator::DnsContentBlockers => Self::DnsContentBlockers, proto::FeatureIndicator::CustomDns => Self::CustomDns, diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs index f7979725fe..329ddedf24 100644 --- a/mullvad-management-interface/src/types/conversions/relay_list.rs +++ b/mullvad-management-interface/src/types/conversions/relay_list.rs @@ -141,6 +141,7 @@ impl From<mullvad_types::relay_list::Relay> for proto::Relay { daita, shadowsocks_extra_addr_in, quic, + lwo: data.lwo, }) } MullvadEndpointData::Bridge => Data::Bridge(Bridge {}), @@ -306,6 +307,7 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { public_key, daita, quic, + lwo: wireguard.lwo, shadowsocks_extra_addr_in, }; MullvadEndpointData::Wireguard(data) diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs index 977b92b26e..6e0ceacfde 100644 --- a/mullvad-relay-selector/src/relay_selector/helpers.rs +++ b/mullvad-relay-selector/src/relay_selector/helpers.rs @@ -222,10 +222,9 @@ pub fn get_lwo_obfuscator( relay: Relay, endpoint: &MullvadWireguardEndpoint, ) -> Option<(ObfuscatorConfig, Relay)> { - let _wg = relay.wireguard()?; - - // TODO: check if LWO is supported on this relay - + if !relay.wireguard()?.lwo { + return None; + } let ip = match endpoint.peer.endpoint { SocketAddr::V4(_) => IpAddr::V4(relay.ipv4_addr_in), SocketAddr::V6(_) => IpAddr::V6(relay.ipv6_addr_in?), diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs index e0efca03a4..7ccb9a6d68 100644 --- a/mullvad-relay-selector/src/relay_selector/matcher.rs +++ b/mullvad-relay-selector/src/relay_selector/matcher.rs @@ -145,8 +145,8 @@ fn filter_on_obfuscation( }, None => false, }, - // TODO: This is only enabled on some relays - ObfuscationQuery::Lwo => true, + // LWO is only enabled on some relays + ObfuscationQuery::Lwo => endpoint_data.lwo, // Other relays are compatible with this query ObfuscationQuery::Off | ObfuscationQuery::Auto | ObfuscationQuery::Udp2tcp(_) => true, } diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs index a3fe4a24f9..4e4813a9dd 100644 --- a/mullvad-relay-selector/src/relay_selector/query.rs +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -652,6 +652,12 @@ pub mod builder { /// in the mullvad-types crate. pub struct Quic; + /// LWO obfuscation. + /// + /// LWO does not have any user-configurable parameters, so there is no type defined + /// in the mullvad-types crate. + pub struct Lwo; + // This impl-block is quantified over all configurations impl<Multihop, Obfuscation, Daita, QuantumResistant> RelayQueryBuilder<Wireguard<Multihop, Obfuscation, Daita, QuantumResistant>> @@ -825,6 +831,22 @@ pub mod builder { }, } } + + /// Enable LWO obfuscation. + pub fn lwo( + mut self, + ) -> RelayQueryBuilder<Wireguard<Multihop, Lwo, Daita, QuantumResistant>> { + self.query.wireguard_constraints.obfuscation = ObfuscationQuery::Lwo; + RelayQueryBuilder { + query: self.query, + protocol: Wireguard { + multihop: self.protocol.multihop, + obfuscation: Lwo, + daita: self.protocol.daita, + quantum_resistant: self.protocol.quantum_resistant, + }, + } + } } impl<Multihop, Daita, QuantumResistant> diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 3e60e39e96..30115d136d 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -79,7 +79,8 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList { ], "Bearer test".to_owned(), "se9-wireguard.blockerad.eu".to_owned(), - )), + )) + .set_lwo(true), ), location: DUMMY_LOCATION.clone(), }, @@ -925,6 +926,32 @@ fn test_selecting_openvpn_and_quic() { .expect("OpenVPN should not be affected by QUIC"); } +/// Test LWO relay selection +#[test] +fn test_selecting_wireguard_over_lwo() { + let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); + + let query = RelayQueryBuilder::wireguard().lwo().build(); + assert!(!query.wireguard_constraints().multihop()); + + let relay = relay_selector.get_relay_by_query(query).unwrap(); + match relay { + GetRelay::Wireguard { + obfuscator, + inner: WireguardConfig::Singlehop { .. }, + .. + } => { + assert!(obfuscator.is_some_and(|obfuscator| matches!( + obfuscator.config, + Obfuscators::Single(ObfuscatorConfig::Lwo { .. }), + ))) + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay with LWO, instead chose {wrong_relay:?}" + ), + } +} + /// Selecting WG IPv6 should not affect OpenVPN #[test] fn test_selecting_openvpn_and_wg_ipv6() { diff --git a/mullvad-types/Cargo.toml b/mullvad-types/Cargo.toml index fcad67d35a..36efa91eda 100644 --- a/mullvad-types/Cargo.toml +++ b/mullvad-types/Cargo.toml @@ -10,9 +10,6 @@ rust-version.workspace = true [lints] workspace = true -[features] -lwo = [] - [dependencies] either = "1.11" chrono = { workspace = true, features = ["clock", "serde"] } diff --git a/mullvad-types/src/features.rs b/mullvad-types/src/features.rs index 8dc8f5cad5..67c83580bd 100644 --- a/mullvad-types/src/features.rs +++ b/mullvad-types/src/features.rs @@ -74,6 +74,7 @@ pub enum FeatureIndicator { Udp2Tcp, Shadowsocks, Quic, + Lwo, LanSharing, DnsContentBlockers, CustomDns, @@ -101,6 +102,7 @@ impl FeatureIndicator { FeatureIndicator::Udp2Tcp => "Udp2Tcp", FeatureIndicator::Shadowsocks => "Shadowsocks", FeatureIndicator::Quic => "Quic", + FeatureIndicator::Lwo => "LWO", FeatureIndicator::LanSharing => "LAN Sharing", FeatureIndicator::DnsContentBlockers => "Dns Content Blocker", FeatureIndicator::CustomDns => "Custom Dns", @@ -181,6 +183,7 @@ pub fn compute_feature_indicators( let udp_tcp = has_obfuscation(ObfuscationType::Udp2Tcp); let shadowsocks = has_obfuscation(ObfuscationType::Shadowsocks); let quic = has_obfuscation(ObfuscationType::Quic); + let lwo = has_obfuscation(ObfuscationType::Lwo); let mtu = settings.tunnel_options.wireguard.mtu.is_some(); @@ -212,6 +215,7 @@ pub fn compute_feature_indicators( (udp_tcp, FeatureIndicator::Udp2Tcp), (shadowsocks, FeatureIndicator::Shadowsocks), (quic, FeatureIndicator::Quic), + (lwo, FeatureIndicator::Lwo), (mtu, FeatureIndicator::CustomMtu), #[cfg(daita)] (daita, FeatureIndicator::Daita), @@ -459,6 +463,7 @@ mod tests { FeatureIndicator::Udp2Tcp => {} FeatureIndicator::Shadowsocks => {} FeatureIndicator::Quic => {} + FeatureIndicator::Lwo => {} FeatureIndicator::LanSharing => {} FeatureIndicator::DnsContentBlockers => {} FeatureIndicator::CustomDns => {} diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index 39a8f58e63..013153524e 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -640,7 +640,6 @@ pub enum SelectedObfuscation { Udp2Tcp, Shadowsocks, Quic, - #[cfg_attr(all(feature = "clap", not(feature = "lwo")), clap(skip))] Lwo, } diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index d28ed88e74..0d22e64f46 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -255,6 +255,7 @@ impl PartialEq for Relay { /// # daita: false, /// # shadowsocks_extra_addr_in: Default::default(), /// # quic: None, + /// # lwo: false, /// # }), /// # location: mullvad_types::location::Location { /// # country: "Sweden".to_string(), @@ -351,6 +352,9 @@ pub struct WireguardRelayEndpointData { /// Parameters for connecting to the masque-proxy running on the relay. #[serde(default)] pub quic: Option<Quic>, + /// Whether the relay supports LWO + #[serde(default)] + pub lwo: bool, /// Optional IP addresses used by Shadowsocks #[serde(default)] pub shadowsocks_extra_addr_in: HashSet<IpAddr>, @@ -362,6 +366,7 @@ impl WireguardRelayEndpointData { public_key, daita: Default::default(), quic: Default::default(), + lwo: Default::default(), shadowsocks_extra_addr_in: Default::default(), } } @@ -380,6 +385,13 @@ impl WireguardRelayEndpointData { } } + pub fn set_lwo(self, enabled: bool) -> Self { + Self { + lwo: enabled, + ..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); diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index 3d6ac6432e..7cf23c1ef2 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -322,6 +322,45 @@ pub async fn test_wireguard_over_quic_ipvx( Ok(()) } +/// Use LWO obfuscation. This tests whether the daemon can connect using LWO. +/// Note that this doesn't verify that the outgoing traffic does not look like WG +#[duplicate_item( + VX test_wireguard_over_lwo_ipvx; + [ V4 ] [ test_wireguard_over_lwo_ipv4 ]; + [ V6 ] [ test_wireguard_over_lwo_ipv6 ]; +)] +#[test_function(skip)] +pub async fn test_wireguard_over_lwo_ipvx( + _: TestContext, + rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> anyhow::Result<()> { + let ip_version = IpVersion::VX; + + log::info!("Enable LWO as obfuscation method"); + let query = RelayQueryBuilder::wireguard() + .ip_version(ip_version) + .lwo() + .build(); + apply_settings_from_relay_query(&mut mullvad_client, query).await?; + + log::info!("Connect to WireGuard via LWO endpoint"); + connect_and_wait(&mut mullvad_client).await?; + + // Verify that the device has a Mullvad exit IP + let conncheck = geoip_lookup_with_retries(&rpc).await; + let mullvad_exit_ip = conncheck + .as_ref() + .is_ok_and(|am_i_mullvad| am_i_mullvad.mullvad_exit_ip); + ensure!( + mullvad_exit_ip, + "Device is either blocked ❌ or leaking 💦 - {:?}", + conncheck, + ); + + Ok(()) +} + /// Test whether bridge mode works. This fails if: /// * No outgoing traffic to the bridge/entry relay is observed from the SUT. /// * The conncheck reports an unexpected exit relay. diff --git a/test/test-manager/test_macro/src/lib.rs b/test/test-manager/test_macro/src/lib.rs index 88b1f4f1df..8fe3c32916 100644 --- a/test/test-manager/test_macro/src/lib.rs +++ b/test/test-manager/test_macro/src/lib.rs @@ -106,35 +106,38 @@ fn get_test_macro_parameters(attributes: &syn::AttributeArgs) -> Result<MacroPar let mut skip = false; for attribute in attributes { - // we only use name-value attributes - let NestedMeta::Meta(Meta::NameValue(nv)) = attribute else { - bail!(attribute, "unknown attribute"); - }; - let lit = &nv.lit; + match attribute { + NestedMeta::Meta(Meta::Path(path)) if path.is_ident("skip") => { + skip = true; + } + NestedMeta::Meta(Meta::NameValue(nv)) => { + let lit = &nv.lit; - match &nv.path { - path if path.is_ident("priority") => match lit { - Lit::Int(lit_int) => priority = Some(lit_int.base10_parse().unwrap()), - _ => bail!(nv, "'priority' should have an integer value"), - }, - path if path.is_ident("target_os") => { - let Lit::Str(lit_str) = lit else { - bail!(nv, "'target_os' should have a string value"); - }; + match &nv.path { + path if path.is_ident("priority") => match lit { + Lit::Int(lit_int) => priority = Some(lit_int.base10_parse().unwrap()), + _ => bail!(nv, "'priority' should have an integer value"), + }, + path if path.is_ident("target_os") => { + let Lit::Str(lit_str) = lit else { + bail!(nv, "'target_os' should have a string value"); + }; - let target = match lit_str.value().parse() { - Ok(os) => os, - Err(e) => bail!(lit_str, "{e}"), - }; + let target = match lit_str.value().parse() { + Ok(os) => os, + Err(e) => bail!(lit_str, "{e}"), + }; - if targets.contains(&target) { - bail!(nv, "Duplicate target"); - } + if targets.contains(&target) { + bail!(nv, "Duplicate target"); + } - targets.push(target); + targets.push(target); + } + _ => bail!(nv, "unknown attribute"), + } } - path if path.is_ident("skip") => skip = true, - _ => bail!(nv, "unknown attribute"), + _ => bail!(attribute, "unknown attribute"), } } |
