summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-09-30 16:09:50 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-09-30 16:09:50 +0200
commit30bae1819de1ede5c4b9334dbd2b6cf945e237fd (patch)
tree05e7334b2d134b47b32e1f1c725ffcf490d9a6c2
parent51ede1644ebb79ef2f2d054a03b278ea78e5df63 (diff)
parent6ee119d8e435c91a12392a0aae94303f448c3676 (diff)
downloadmullvadvpn-30bae1819de1ede5c4b9334dbd2b6cf945e237fd.tar.xz
mullvadvpn-30bae1819de1ede5c4b9334dbd2b6cf945e237fd.zip
Merge branch 'add-remaining-lwo'
-rw-r--r--CHANGELOG.md1
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt1
-rw-r--r--desktop/packages/mullvad-vpn/locales/messages.pot4
-rw-r--r--desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts5
-rw-r--r--desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/RelayListContext.tsx12
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/select-location/SelectLocation.tsx28
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/main/components/connection-panel/components/feature-indicators/hooks/use-get-feature-indicator/useGetFeatureIndicator.ts4
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/wireguard-settings/components/obfuscation-settings/ObfuscationSettings.tsx3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts25
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts3
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mock-data.ts7
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/feature-indicators/feature-indicators.spec.ts7
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/helpers.ts9
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/select-location/select-location.spec.ts25
-rw-r--r--mullvad-api/src/relay_list.rs8
-rw-r--r--mullvad-cli/Cargo.toml5
-rw-r--r--mullvad-management-interface/proto/management_interface.proto18
-rw-r--r--mullvad-management-interface/src/types/conversions/features.rs2
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_list.rs2
-rw-r--r--mullvad-relay-selector/src/relay_selector/helpers.rs7
-rw-r--r--mullvad-relay-selector/src/relay_selector/matcher.rs4
-rw-r--r--mullvad-relay-selector/src/relay_selector/query.rs22
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs29
-rw-r--r--mullvad-types/Cargo.toml3
-rw-r--r--mullvad-types/src/features.rs5
-rw-r--r--mullvad-types/src/relay_constraints.rs1
-rw-r--r--mullvad-types/src/relay_list.rs12
-rw-r--r--test/test-manager/src/tests/tunnel.rs39
-rw-r--r--test/test-manager/test_macro/src/lib.rs51
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"),
}
}