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 1ff297312bec..a1020b71d0d8 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 @@ -609,6 +609,7 @@ internal fun ManagementInterface.FeatureIndicator.toDomain() = FeatureIndicator.SERVER_IP_OVERRIDE ManagementInterface.FeatureIndicator.CUSTOM_MTU -> FeatureIndicator.CUSTOM_MTU ManagementInterface.FeatureIndicator.DAITA -> FeatureIndicator.DAITA + ManagementInterface.FeatureIndicator.DAITA_SMART_ROUTING, ManagementInterface.FeatureIndicator.LOCKDOWN_MODE, ManagementInterface.FeatureIndicator.SHADOWSOCKS, ManagementInterface.FeatureIndicator.MULTIHOP, diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 5969e4ffd36b..730b75aacaa7 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -5,6 +5,11 @@ msgstr "" msgid "%(amount)d more..." msgstr "" +#. This refers to the Smart Routing setting in the VPN settings view. +#. This is displayed when both Smart Routing and DAITA features are on. +msgid "%(daita)s: Smart routing" +msgstr "" + msgid "%(duration)s was added, account paid until %(expiry)s." msgstr "" @@ -125,6 +130,9 @@ msgstr "" msgid "Disable" msgstr "" +msgid "Disable anyway" +msgstr "" + msgid "Disconnect" msgstr "" @@ -245,6 +253,9 @@ msgstr "" msgid "Settings" msgstr "" +msgid "Smart routing" +msgstr "" + msgid "System default" msgstr "" @@ -1934,6 +1945,10 @@ msgctxt "vpn-settings-view" msgid "IPv4 is always enabled and the majority of websites and applications use this protocol. We do not recommend enabling IPv6 unless you know you need it." msgstr "" +msgctxt "vpn-settings-view" +msgid "Is automatically enabled with %(daita)s, makes it possible to use %(daita)s with any server by using multihop. This might increase latency." +msgstr "" + msgctxt "vpn-settings-view" msgid "It does this by allowing network communication outside the tunnel to local multicast and broadcast ranges as well as to and from these private IP ranges:" msgstr "" @@ -2065,7 +2080,7 @@ msgid "%(wireguard)s settings" msgstr "" msgctxt "wireguard-settings-view" -msgid "Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %(daita)s." +msgid "Hides patterns in your encrypted VPN traffic. Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed and battery usage." msgstr "" msgctxt "wireguard-settings-view" @@ -2080,6 +2095,15 @@ msgctxt "wireguard-settings-view" msgid "MTU" msgstr "" +#. Warning text in a dialog that is displayed after a setting is toggled. +msgctxt "wireguard-settings-view" +msgid "Not all our servers are %(daita)s-enabled. In order to use the internet, you might have to select a new location after disabling, or you can continue using %(daita)s with Smart routing." +msgstr "" + +msgctxt "wireguard-settings-view" +msgid "Not all our servers are %(daita)s-enabled. Smart routing allows %(daita)s to be used at any location. It does this by using multihop in the background to route your traffic via the closest %(daita)s-enabled server first." +msgstr "" + msgctxt "wireguard-settings-view" msgid "Obfuscation" msgstr "" @@ -2134,11 +2158,6 @@ msgctxt "wireguard-settings-view" msgid "This allows access to %(wireguard)s for devices that only support IPv6." msgstr "" -#. Warning text in a dialog that is displayed after a setting is toggled. -msgctxt "wireguard-settings-view" -msgid "This feature isn’t available on all servers. You might need to change location after enabling." -msgstr "" - msgctxt "wireguard-settings-view" msgid "This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers." msgstr "" @@ -2151,6 +2170,10 @@ msgctxt "wireguard-settings-view" msgid "UDP-over-TCP port" msgstr "" +msgctxt "wireguard-settings-view" +msgid "Use Smart routing" +msgstr "" + #. Text describing the valid port range for a port selector. msgctxt "wireguard-settings-view" msgid "Valid range: %(min)s - %(max)s" @@ -2541,6 +2564,9 @@ msgstr "" msgid "This address has already been entered." msgstr "" +msgid "This feature isn’t available on all servers. You might need to change location after enabling." +msgstr "" + msgid "This field is required" msgstr "" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c85189cea740..31f6ddcfabfb 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -38,7 +38,6 @@ import { IAppVersionInfo, IBridgeConstraints, ICustomList, - IDaitaSettings, IDevice, IDeviceRemoval, IDnsOptions, @@ -586,13 +585,12 @@ export class DaemonRpc { await this.callBool(this.client.prepareRestartV2, quit); } - public async setDaitaSettings(daitaSettings: IDaitaSettings): Promise { - const grpcDaitaSettings = new grpcTypes.DaitaSettings(); - grpcDaitaSettings.setEnabled(daitaSettings.enabled); - await this.call( - this.client.setDaitaSettings, - grpcDaitaSettings, - ); + public async setEnableDaita(value: boolean): Promise { + await this.callBool(this.client.setEnableDaita, value); + } + + public async setDaitaSmartRouting(value: boolean): Promise { + await this.callBool(this.client.setDaitaSmartRouting, value); } public async listDevices(accountToken: AccountToken): Promise> { @@ -1182,6 +1180,8 @@ function convertFromFeatureIndicator( return FeatureIndicator.customMssFix; case grpcTypes.FeatureIndicator.DAITA: return FeatureIndicator.daita; + case grpcTypes.FeatureIndicator.DAITA_SMART_ROUTING: + return FeatureIndicator.daitaSmartRouting; case grpcTypes.FeatureIndicator.SHADOWSOCKS: return FeatureIndicator.shadowsocks; } diff --git a/gui/src/main/settings.ts b/gui/src/main/settings.ts index 22238c72c459..03537ba581a6 100644 --- a/gui/src/main/settings.ts +++ b/gui/src/main/settings.ts @@ -107,8 +107,11 @@ export default class Settings implements Readonly { const settings = await fs.readFile(path); return this.daemonRpc.applyJsonSettings(settings.toString()); }); - IpcMainEventChannel.settings.handleSetDaitaSettings((daitaSettings) => { - return this.daemonRpc.setDaitaSettings(daitaSettings); + IpcMainEventChannel.settings.handleSetEnableDaita((value) => { + return this.daemonRpc.setEnableDaita(value); + }); + IpcMainEventChannel.settings.handleSetDaitaSmartRouting((value) => { + return this.daemonRpc.setDaitaSmartRouting(value); }); IpcMainEventChannel.guiSettings.handleSetEnableSystemNotifications((flag: boolean) => { diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index a4c76aa2d1a4..a4d5fc2fada4 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -19,7 +19,6 @@ import { IAccountData, IAppVersionInfo, ICustomList, - IDaitaSettings, IDevice, IDeviceRemoval, IDnsOptions, @@ -345,8 +344,10 @@ export default class AppRenderer { IpcRendererEventChannel.splitTunneling.forgetManuallyAddedApplication(application); public setObfuscationSettings = (obfuscationSettings: ObfuscationSettings) => IpcRendererEventChannel.settings.setObfuscationSettings(obfuscationSettings); - public setDaitaSettings = (daitaSettings: IDaitaSettings) => - IpcRendererEventChannel.settings.setDaitaSettings(daitaSettings); + public setEnableDaita = (value: boolean) => + IpcRendererEventChannel.settings.setEnableDaita(value); + public setDaitaSmartRouting = (value: boolean) => + IpcRendererEventChannel.settings.setDaitaSmartRouting(value); public collectProblemReport = (toRedact: string | undefined) => IpcRendererEventChannel.problemReport.collectLogs(toRedact); public viewLog = (path: string) => IpcRendererEventChannel.problemReport.viewLog(path); diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index e90729507c16..75b8df936c3b 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -8,6 +8,7 @@ import { ITransitionSpecification, transitions, useHistory } from '../lib/histor import { RoutePath } from '../lib/routes'; import Account from './Account'; import ApiAccessMethods from './ApiAccessMethods'; +import DaitaSettings from './DaitaSettings'; import Debug from './Debug'; import { DeviceRevokedView } from './DeviceRevokedView'; import { EditApiAccessMethod } from './EditApiAccessMethod'; @@ -85,6 +86,7 @@ export default function AppRouter() { + diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx new file mode 100644 index 000000000000..3176192ad10d --- /dev/null +++ b/gui/src/renderer/components/DaitaSettings.tsx @@ -0,0 +1,193 @@ +import { useCallback } from 'react'; +import { sprintf } from 'sprintf-js'; +import styled from 'styled-components'; + +import { strings } from '../../config.json'; +import { messages } from '../../shared/gettext'; +import { useAppContext } from '../context'; +import { useHistory } from '../lib/history'; +import { useBoolean } from '../lib/utilityHooks'; +import { useSelector } from '../redux/store'; +import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; +import * as Cell from './cell'; +import InfoButton from './InfoButton'; +import { BackAction } from './KeyboardNavigation'; +import { Layout, SettingsContainer } from './Layout'; +import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; +import { + NavigationBar, + NavigationContainer, + NavigationInfoButton, + NavigationItems, + NavigationScrollbars, + TitleBarItem, +} from './NavigationBar'; +import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; +import { SmallButton, SmallButtonColor } from './SmallButton'; + +const StyledContent = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 1, + marginBottom: '2px', +}); + +export default function DaitaSettings() { + const { pop } = useHistory(); + + return ( + + + + + + + {strings.daita} + + + + {sprintf( + messages.pgettext( + 'wireguard-settings-view', + '%(daita)s (%(daitaFull)s) hides patterns in your encrypted VPN traffic. If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting. It does this by carefully adding network noise and making all network packets the same size.', + ), + { daita: strings.daita, daitaFull: strings.daitaFull }, + )} + + + + + + + + {strings.daita} + + {messages.pgettext( + 'wireguard-settings-view', + 'Hides patterns in your encrypted VPN traffic. Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed and battery usage.', + )} + + + + + + + + + + + + + + ); +} + +function DaitaToggle() { + const { setEnableDaita, setDaitaSmartRouting } = useAppContext(); + const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); + const smartRouting = useSelector( + (state) => state.settings.wireguard.daita?.smartRouting ?? false, + ); + + const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(); + + const setDaita = useCallback((value: boolean) => { + void setEnableDaita(value); + }, []); + + const setSmartRouting = useCallback((value: boolean) => { + if (value) { + void setDaitaSmartRouting(value); + } else { + showConfirmationDialog(); + } + }, []); + + const confirmDisableSmartRouting = useCallback(() => { + void setDaitaSmartRouting(false); + hideConfirmationDialog(); + }, []); + + return ( + <> + + + + {messages.gettext('Enable')} + + + + + + + + + + {messages.gettext('Smart routing')} + + + + + + + + + + + + {sprintf( + messages.pgettext( + 'vpn-settings-view', + 'Is automatically enabled with %(daita)s, makes it possible to use %(daita)s with any server by using multihop. This might increase latency.', + ), + { daita: strings.daita }, + )} + + + + + + {messages.gettext('Disable anyway')} + , + + {messages.pgettext('wireguard-settings-view', 'Use Smart routing')} + , + ]} + close={hideConfirmationDialog}> + + {sprintf( + // TRANSLATORS: Warning text in a dialog that is displayed after a setting is toggled. + messages.pgettext( + 'wireguard-settings-view', + 'Not all our servers are %(daita)s-enabled. In order to use the internet, you might have to select a new location after disabling, or you can continue using %(daita)s with Smart routing.', + ), + { daita: strings.daita }, + )} + + + + ); +} + +export function SmartRoutingModalMessage() { + return ( + + {sprintf( + messages.pgettext( + 'wireguard-settings-view', + 'Not all our servers are %(daita)s-enabled. Smart routing allows %(daita)s to be used at any location. It does this by using multihop in the background to route your traffic via the closest %(daita)s-enabled server first.', + ), + { + daita: strings.daita, + }, + )} + + ); +} diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx index 1f4709159fd0..8f2329d77f05 100644 --- a/gui/src/renderer/components/WireguardSettings.tsx +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -22,7 +22,6 @@ import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import Selector, { SelectorItem, SelectorWithCustomItem } from './cell/Selector'; -import InfoButton from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; @@ -98,7 +97,7 @@ export default function WireguardSettings() { - + @@ -528,89 +527,16 @@ function MtuSetting() { ); } -function DaitaSettings() { - const { setDaitaSettings } = useAppContext(); +function DaitaButton() { + const history = useHistory(); + const navigate = useCallback(() => history.push(RoutePath.daitaSettings), [history]); const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); - const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(); - - const setDaita = useCallback((value: boolean) => { - if (value) { - showConfirmationDialog(); - } else { - void setDaitaSettings({ enabled: value }); - } - }, []); - - const confirmDaita = useCallback(() => { - void setDaitaSettings({ enabled: true }); - hideConfirmationDialog(); - }, []); - return ( - <> - - - - {strings.daita} - - - - {sprintf( - messages.pgettext( - 'wireguard-settings-view', - '%(daita)s (%(daitaFull)s) hides patterns in your encrypted VPN traffic. If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting. It does this by carefully adding network noise and making all network packets the same size.', - ), - { daita: strings.daita, daitaFull: strings.daitaFull }, - )} - - - {sprintf( - messages.pgettext( - 'wireguard-settings-view', - 'Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %(daita)s.', - ), - { daita: strings.daita }, - )} - - - - - - - - - {messages.gettext('Enable anyway')} - , - - {messages.gettext('Back')} - , - ]} - close={hideConfirmationDialog}> - - { - // TRANSLATORS: Warning text in a dialog that is displayed after a setting is toggled. - messages.pgettext( - 'wireguard-settings-view', - 'This feature isn’t available on all servers. You might need to change location after enabling.', - ) - } - - - {sprintf( - messages.pgettext( - 'wireguard-settings-view', - 'Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %(daita)s.', - ), - { daita: strings.daita }, - )} - - - + + {strings.daita} + {daita ? messages.gettext('On') : messages.gettext('Off')} + ); } diff --git a/gui/src/renderer/components/main-view/FeatureIndicators.tsx b/gui/src/renderer/components/main-view/FeatureIndicators.tsx index 68ee0ae5ae81..4d189e712c5d 100644 --- a/gui/src/renderer/components/main-view/FeatureIndicators.tsx +++ b/gui/src/renderer/components/main-view/FeatureIndicators.tsx @@ -5,9 +5,13 @@ import styled from 'styled-components'; import { colors, strings } from '../../../config.json'; import { FeatureIndicator } from '../../../shared/daemon-rpc-types'; import { messages } from '../../../shared/gettext'; -import { useStyledRef } from '../../lib/utilityHooks'; +import { useBoolean, useStyledRef } from '../../lib/utilityHooks'; import { useSelector } from '../../redux/store'; import { tinyText } from '../common-styles'; +import { SmartRoutingModalMessage } from '../DaitaSettings'; +import { InfoIcon } from '../InfoButton'; +import { ModalAlert, ModalAlertType } from '../Modal'; +import { SmallButton, SmallButtonColor } from '../SmallButton'; import { ConnectionPanelAccordion } from './styles'; const LINE_HEIGHT = 22; @@ -43,8 +47,9 @@ const StyledFeatureIndicatorsWrapper = styled.div<{ $expanded: boolean }>((props })); const StyledFeatureIndicatorLabel = styled.span<{ $expanded: boolean }>(tinyText, (props) => ({ - display: 'inline', - padding: '2px 8px', + display: 'flex', + gap: '4px', + padding: '1px 7px', justifyContent: 'center', alignItems: 'center', borderRadius: '4px', @@ -53,6 +58,15 @@ const StyledFeatureIndicatorLabel = styled.span<{ $expanded: boolean }>(tinyText fontWeight: 400, whiteSpace: 'nowrap', visibility: props.$expanded ? 'visible' : 'hidden', + + // Style clickable feature indicators with a border and on-hover effect + boxSizing: 'border-box', // make border act as padding rather than margin + border: 'solid 1px', + borderColor: props.onClick ? colors.blue : colors.darkerBlue, + transition: 'background ease-in-out 300ms', + '&&:hover': { + background: props.onClick ? colors.blue60 : undefined, + }, })); const StyledBaseEllipsis = styled.span<{ $display: boolean }>(tinyText, (props) => ({ @@ -88,6 +102,11 @@ interface FeatureIndicatorsProps { // we can count those and add another ellipsis element which is visible and place it after the last // visible indicator. export default function FeatureIndicators(props: FeatureIndicatorsProps) { + const [ + daitaSmartRoutingDialogueVisible, + showDaitaSmartRoutingDialogue, + hideDaitaSmartRoutingDialogue, + ] = useBoolean(); const tunnelState = useSelector((state) => state.connection.status); const ellipsisRef = useStyledRef(); const ellipsisSpacerRef = useStyledRef(); @@ -106,6 +125,16 @@ export default function FeatureIndicators(props: FeatureIndicatorsProps) { const ellipsis = messages.gettext('%(amount)d more...'); + // Returns an optional callback for clickable feature indicators, or undefined. + const getFeatureIndicatorOnClick = (indicator: FeatureIndicator) => { + switch (indicator) { + case FeatureIndicator.daitaSmartRouting: + return showDaitaSmartRoutingDialogue; + default: + return undefined; + } + }; + useEffect(() => { // We need to defer the visibility logic one painting cycle to make sure the elements are // rendered and available. @@ -164,7 +193,7 @@ export default function FeatureIndicators(props: FeatureIndicatorsProps) { return ( 0}> - + {messages.pgettext('connect-view', 'Active features')} @@ -172,16 +201,20 @@ export default function FeatureIndicators(props: FeatureIndicatorsProps) { - {sortedIndicators.map((indicator) => ( - - {getFeatureIndicatorLabel(indicator)} - - ))} + {sortedIndicators.map((indicator) => { + const onClick = getFeatureIndicatorOnClick(indicator); + return ( + + {getFeatureIndicatorLabel(indicator)} + {onClick ? : null} + + ); + })} - { // Mock amount for the spacer ellipsis. This needs to be wider than the real @@ -189,8 +222,28 @@ export default function FeatureIndicators(props: FeatureIndicatorsProps) { sprintf(ellipsis, { amount: 222 }) } + + + + {messages.gettext('Got it!')} + , + ]} + close={hideDaitaSmartRoutingDialogue}> + + ); } @@ -216,6 +269,13 @@ function getFeatureIndicatorLabel(indicator: FeatureIndicator) { switch (indicator) { case FeatureIndicator.daita: return strings.daita; + case FeatureIndicator.daitaSmartRouting: + return sprintf( + // TRANSLATORS: This refers to the Smart Routing setting in the VPN settings view. + // TRANSLATORS: This is displayed when both Smart Routing and DAITA features are on. + messages.gettext('%(daita)s: Smart routing'), + { daita: strings.daita }, + ); case FeatureIndicator.udp2tcp: case FeatureIndicator.shadowsocks: return messages.pgettext('wireguard-settings-view', 'Obfuscation'); diff --git a/gui/src/renderer/components/select-location/RelayListContext.tsx b/gui/src/renderer/components/select-location/RelayListContext.tsx index 13e2dc28e8d1..241d11fbdcf0 100644 --- a/gui/src/renderer/components/select-location/RelayListContext.tsx +++ b/gui/src/renderer/components/select-location/RelayListContext.tsx @@ -62,6 +62,10 @@ interface RelayListContextProviderProps { export function RelayListContextProvider(props: RelayListContextProviderProps) { const { locationType, searchTerm } = useSelectLocationContext(); const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); + const smartRouting = useSelector( + (state) => state.settings.wireguard.daita?.smartRouting ?? false, + ); + const fullRelayList = useSelector((state) => state.settings.relayLocations); const relaySettings = useNormalRelaySettings(); @@ -77,6 +81,7 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) { return filterLocationsByDaita( relayListForEndpointType, daita, + smartRouting, locationType, relaySettings?.tunnelProtocol ?? 'any', relaySettings?.wireguard.useMultihop ?? false, diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx index f6cf772019a2..9cd66f98f435 100644 --- a/gui/src/renderer/components/select-location/SelectLocation.tsx +++ b/gui/src/renderer/components/select-location/SelectLocation.tsx @@ -68,8 +68,12 @@ export default function SelectLocation() { const providers = relaySettings?.providers ?? []; const filteredProviders = useFilteredProviders(providers, ownership); const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); + const smartRouting = useSelector( + (state) => state.settings.wireguard.daita?.smartRouting ?? false, + ); const showDaitaFilter = daitaFilterActive( daita, + smartRouting, locationType, relaySettings?.tunnelProtocol ?? 'any', relaySettings?.wireguard.useMultihop ?? false, diff --git a/gui/src/renderer/lib/filter-locations.ts b/gui/src/renderer/lib/filter-locations.ts index 78126f1fb417..661d8ecc2950 100644 --- a/gui/src/renderer/lib/filter-locations.ts +++ b/gui/src/renderer/lib/filter-locations.ts @@ -37,17 +37,19 @@ export function filterLocationsByEndPointType( export function filterLocationsByDaita( locations: IRelayLocationCountryRedux[], daita: boolean, + smartRouting: boolean, locationType: LocationType, tunnelProtocol: LiftedConstraint, multihop: boolean, ): IRelayLocationCountryRedux[] { - return daitaFilterActive(daita, locationType, tunnelProtocol, multihop) + return daitaFilterActive(daita, smartRouting, locationType, tunnelProtocol, multihop) ? filterLocationsImpl(locations, (relay: IRelayLocationRelayRedux) => relay.daita) : locations; } export function daitaFilterActive( daita: boolean, + smartRouting: boolean, locationType: LocationType, tunnelProtocol: LiftedConstraint, multihop: boolean, @@ -55,7 +57,7 @@ export function daitaFilterActive( const isEntry = multihop ? locationType === LocationType.entry : locationType === LocationType.exit; - return daita && isEntry && tunnelProtocol !== 'openvpn'; + return daita && (!smartRouting || multihop) && isEntry && tunnelProtocol !== 'openvpn'; } export function filterLocations( diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts index 0ccc3679ff81..5204dc666c61 100644 --- a/gui/src/renderer/lib/routes.ts +++ b/gui/src/renderer/lib/routes.ts @@ -15,6 +15,7 @@ export enum RoutePath { userInterfaceSettings = '/settings/interface', vpnSettings = '/settings/vpn', wireguardSettings = '/settings/advanced/wireguard', + daitaSettings = '/settings/advanced/wireguard/daita', udpOverTcp = '/settings/advanced/wireguard/udp-over-tcp', shadowsocks = '/settings/advanced/shadowsocks', openVpnSettings = '/settings/advanced/openvpn', diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index b84ee8a72ed4..33ff6bb8ce3d 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -183,6 +183,7 @@ export interface ITunnelStateRelayInfo { // The order of the variants match the priority order and can be sorted on. export enum FeatureIndicator { daita, + daitaSmartRouting, quantumResistance, multihop, bridgeMode, @@ -551,6 +552,7 @@ export interface RelayOverride { export interface IDaitaSettings { enabled: boolean; + smartRouting: boolean; } export function parseSocketAddress(socketAddrStr: string): ISocketAddress { diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index acbb6366d262..4e08e7109d47 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -14,7 +14,6 @@ import { IAccountData, IAppVersionInfo, ICustomList, - IDaitaSettings, IDevice, IDeviceRemoval, IDnsOptions, @@ -195,7 +194,8 @@ export const ipcSchema = { testApiAccessMethodById: invoke(), testCustomApiAccessMethod: invoke(), clearAllRelayOverrides: invoke(), - setDaitaSettings: invoke(), + setEnableDaita: invoke(), + setDaitaSmartRouting: invoke(), }, guiSettings: { '': notifyRenderer(), diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs index afe71c829d1a..5f2b2d6d8180 100644 --- a/mullvad-api/src/relay_list.rs +++ b/mullvad-api/src/relay_list.rs @@ -162,7 +162,7 @@ fn into_mullvad_relay( provider: relay.provider, weight: relay.weight, endpoint_data, - location: Some(location), + location, } } diff --git a/mullvad-cli/build.rs b/mullvad-cli/build.rs index ee547da265b5..de110d3a76b0 100644 --- a/mullvad-cli/build.rs +++ b/mullvad-cli/build.rs @@ -15,11 +15,4 @@ fn main() { )); res.compile().expect("Unable to generate windows resources"); } - let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); - - // Enable DAITA by default on desktop - println!("cargo::rustc-check-cfg=cfg(daita)"); - if let "linux" | "windows" | "macos" = target_os.as_str() { - println!(r#"cargo::rustc-cfg=daita"#); - } } diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 68292c0ad1b8..4fe11b2a8f95 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -4,7 +4,7 @@ use itertools::Itertools; use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ constraints::{Constraint, Match}, - location::{CountryCode, Location}, + location::CountryCode, relay_constraints::{ GeographicLocationConstraint, LocationConstraint, LocationConstraintFormatter, OpenVpnConstraints, Ownership, Provider, Providers, RelayConstraints, RelayOverride, @@ -542,7 +542,6 @@ impl Relay { allowed_ips: all_of_the_internet(), endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), psk: None, - #[cfg(daita)] constant_packet_size: false, }, exit_peer: None, @@ -921,15 +920,11 @@ fn parse_transport_port( fn relay_to_geographical_constraint( relay: mullvad_types::relay_list::Relay, -) -> Option { - relay.location.map( - |Location { - country_code, - city_code, - .. - }| { - GeographicLocationConstraint::Hostname(country_code, city_code, relay.hostname) - }, +) -> GeographicLocationConstraint { + GeographicLocationConstraint::Hostname( + relay.location.country_code, + relay.location.city_code, + relay.hostname, ) } @@ -952,10 +947,9 @@ pub async fn resolve_location_constraint( .find(|relay| relay.hostname.to_lowercase() == location_constraint_args.country) { if relay_filter(&matching_relay) { - Ok(Constraint::Only( - relay_to_geographical_constraint(matching_relay) - .context("Selected relay did not contain a valid location")?, - )) + Ok(Constraint::Only(relay_to_geographical_constraint( + matching_relay, + ))) } else { bail!( "The relay `{}` is not valid for this operation", diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 0937cc82299e..da66cac5d505 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -1,8 +1,6 @@ use anyhow::Result; use clap::Subcommand; use mullvad_management_interface::MullvadProxyClient; -#[cfg(daita)] -use mullvad_types::wireguard::DaitaSettings; use mullvad_types::{ constraints::Constraint, wireguard::{QuantumResistantState, RotationInterval, DEFAULT_ROTATION_INTERVAL}, @@ -41,9 +39,11 @@ pub enum TunnelOptions { #[arg(long)] quantum_resistant: Option, /// Configure whether to enable DAITA - #[cfg(daita)] #[arg(long)] daita: Option, + /// Configure whether to enable DAITA smart routing + #[arg(long)] + daita_smart_routing: Option, /// The key rotation interval. Number of hours, or 'any' #[arg(long)] rotation_interval: Option>, @@ -101,7 +101,6 @@ impl Tunnel { tunnel_options.wireguard.quantum_resistant, ); - #[cfg(daita)] print_option!("DAITA", tunnel_options.wireguard.daita.enabled); let key = rpc.get_wireguard_key().await?; @@ -138,16 +137,16 @@ impl Tunnel { TunnelOptions::Wireguard { mtu, quantum_resistant, - #[cfg(daita)] daita, + daita_smart_routing, rotation_interval, rotate_key, } => { Self::handle_wireguard( mtu, quantum_resistant, - #[cfg(daita)] daita, + daita_smart_routing, rotation_interval, rotate_key, ) @@ -178,7 +177,8 @@ impl Tunnel { async fn handle_wireguard( mtu: Option>, quantum_resistant: Option, - #[cfg(daita)] daita: Option, + daita: Option, + daita_smart_routing: Option, rotation_interval: Option>, rotate_key: Option, ) -> Result<()> { @@ -194,11 +194,15 @@ impl Tunnel { println!("Quantum resistant setting has been updated"); } - #[cfg(daita)] - if let Some(daita) = daita { - rpc.set_daita_settings(DaitaSettings { enabled: *daita }) - .await?; + if let Some(enable_daita) = daita { + rpc.set_enable_daita(*enable_daita).await?; println!("DAITA setting has been updated"); + println!("Smart routing setting has been updated"); + } + + if let Some(daita_smart_routing) = daita_smart_routing { + rpc.set_daita_smart_routing(*daita_smart_routing).await?; + println!("Smart routing setting has been updated"); } if let Some(interval) = rotation_interval { diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index a25a83522791..097dcc6bf65d 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -264,6 +264,10 @@ pub enum DaemonCommand { SetQuantumResistantTunnel(ResponseTx<(), settings::Error>, QuantumResistantState), /// Set DAITA settings for the tunnel #[cfg(daita)] + SetEnableDaita(ResponseTx<(), settings::Error>, bool), + #[cfg(daita)] + SetDaitaSmartRouting(ResponseTx<(), settings::Error>, bool), + #[cfg(daita)] SetDaitaSettings(ResponseTx<(), settings::Error>, DaitaSettings), /// Set DNS options or servers to use SetDnsOptions(ResponseTx<(), settings::Error>, DnsOptions), @@ -1255,6 +1259,10 @@ impl Daemon { .await } #[cfg(daita)] + SetEnableDaita(tx, value) => self.on_set_daita_enabled(tx, value).await, + #[cfg(daita)] + SetDaitaSmartRouting(tx, value) => self.on_set_daita_smart_routing(tx, value).await, + #[cfg(daita)] SetDaitaSettings(tx, daita_settings) => { self.on_set_daita_settings(tx, daita_settings).await } @@ -2323,6 +2331,86 @@ impl Daemon { } } + #[cfg(daita)] + async fn on_set_daita_enabled(&mut self, tx: ResponseTx<(), settings::Error>, value: bool) { + use mullvad_types::{constraints::Constraint, Intersection}; + + let result = self + .settings + .update(|settings| { + settings.tunnel_options.wireguard.daita.enabled = value; + + // enable smart-routing automatically with daita + if cfg!(not(target_os = "android")) { + settings.tunnel_options.wireguard.daita.smart_routing = value + } + }) + .await; + + match result { + Ok(settings_changed) => { + Self::oneshot_send(tx, Ok(()), "set_daita_enabled response"); + let RelaySettings::Normal(constraints) = &self.settings.relay_settings else { + return; // DAITA is not supported for custom relays + }; + + let wireguard_enabled = constraints + .tunnel_protocol + .intersection(Constraint::Only(TunnelType::Wireguard)) + .is_some(); + + if settings_changed && wireguard_enabled { + log::info!("Reconnecting because DAITA settings changed"); + self.reconnect_tunnel(); + } + } + Err(e) => { + log::error!("{}", e.display_chain_with_msg("Unable to save settings")); + Self::oneshot_send(tx, Err(e), "set_daita_enabled response"); + } + } + } + + #[cfg(daita)] + async fn on_set_daita_smart_routing( + &mut self, + tx: ResponseTx<(), settings::Error>, + value: bool, + ) { + use mullvad_types::{constraints::Constraint, Intersection}; + + match self + .settings + .update(|settings| settings.tunnel_options.wireguard.daita.smart_routing = value) + .await + { + Ok(settings_changed) => { + Self::oneshot_send(tx, Ok(()), "set_daita_smart_routing response"); + + let RelaySettings::Normal(constraints) = &self.settings.relay_settings else { + return; // DAITA is not supported for custom relays + }; + + let wireguard_enabled = constraints + .tunnel_protocol + .intersection(Constraint::Only(TunnelType::Wireguard)) + .is_some(); + + let multihop_enabled = constraints.wireguard_constraints.use_multihop; + let daita_enabled = self.settings.tunnel_options.wireguard.daita.enabled; + + if settings_changed && wireguard_enabled && daita_enabled && !multihop_enabled { + log::info!("Reconnecting because DAITA settings changed"); + self.reconnect_tunnel(); + } + } + Err(e) => { + log::error!("{}", e.display_chain_with_msg("Unable to save settings")); + Self::oneshot_send(tx, Err(e), "set_daita_smart_routing response"); + } + } + } + #[cfg(daita)] async fn on_set_daita_settings( &mut self, @@ -2931,8 +3019,14 @@ fn new_selector_config(settings: &Settings) -> SelectorConfig { wireguard: AdditionalWireguardConstraints { #[cfg(daita)] daita: settings.tunnel_options.wireguard.daita.enabled, + #[cfg(daita)] + daita_smart_routing: settings.tunnel_options.wireguard.daita.smart_routing, + #[cfg(not(daita))] daita: false, + #[cfg(not(daita))] + daita_smart_routing: false, + quantum_resistant: settings.tunnel_options.wireguard.quantum_resistant, }, }; diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 4d0f558a97f4..331754d1f25f 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -341,6 +341,26 @@ impl ManagementService for ManagementServiceImpl { Ok(Response::new(())) } + #[cfg(daita)] + async fn set_enable_daita(&self, request: Request) -> ServiceResult<()> { + let value = request.into_inner(); + log::debug!("set_enable_daita({value})"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetEnableDaita(tx, value))?; + self.wait_for_result(rx).await?.map(Response::new)?; + Ok(Response::new(())) + } + + #[cfg(daita)] + async fn set_daita_smart_routing(&self, request: Request) -> ServiceResult<()> { + let value = request.into_inner(); + log::debug!("set_daita_smart_routing({value})"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetDaitaSmartRouting(tx, value))?; + self.wait_for_result(rx).await?.map(Response::new)?; + Ok(Response::new(())) + } + #[cfg(daita)] async fn set_daita_settings( &self, @@ -355,6 +375,16 @@ impl ManagementService for ManagementServiceImpl { Ok(Response::new(())) } + #[cfg(not(daita))] + async fn set_enable_daita(&self, _: Request) -> ServiceResult<()> { + Ok(Response::new(())) + } + + #[cfg(not(daita))] + async fn set_daita_smart_routing(&self, _: Request) -> ServiceResult<()> { + Ok(Response::new(())) + } + #[cfg(not(daita))] async fn set_daita_settings(&self, _: Request) -> ServiceResult<()> { Ok(Response::new(())) diff --git a/mullvad-daemon/src/tunnel.rs b/mullvad-daemon/src/tunnel.rs index 72ec5087fc8d..73bda59635c7 100644 --- a/mullvad-daemon/src/tunnel.rs +++ b/mullvad-daemon/src/tunnel.rs @@ -128,7 +128,7 @@ impl ParametersGenerator { hostname = exit.hostname.clone(); obfuscator_hostname = take_hostname(obfuscator); bridge_hostname = None; - location = exit.location.as_ref().cloned().unwrap(); + location = exit.location.clone(); } #[cfg(not(target_os = "android"))] LastSelectedRelays::OpenVpn { relay, bridge, .. } => { @@ -136,7 +136,7 @@ impl ParametersGenerator { bridge_hostname = take_hostname(bridge); entry_hostname = None; obfuscator_hostname = None; - location = relay.location.as_ref().cloned().unwrap(); + location = relay.location.clone(); } }; diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index e705c6378898..d701fc8e4f9b 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -48,6 +48,8 @@ service ManagementService { rpc SetWireguardMtu(google.protobuf.UInt32Value) returns (google.protobuf.Empty) {} rpc SetEnableIpv6(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetQuantumResistantTunnel(QuantumResistantState) returns (google.protobuf.Empty) {} + rpc SetEnableDaita(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + rpc SetDaitaSmartRouting(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetDaitaSettings(DaitaSettings) returns (google.protobuf.Empty) {} rpc SetDnsOptions(DnsOptions) returns (google.protobuf.Empty) {} rpc SetRelayOverride(RelayOverride) returns (google.protobuf.Empty) {} @@ -262,6 +264,7 @@ enum FeatureIndicator { CUSTOM_MTU = 11; CUSTOM_MSS_FIX = 12; DAITA = 13; + DAITA_SMART_ROUTING = 14; } message ObfuscationEndpoint { @@ -541,7 +544,10 @@ message QuantumResistantState { State state = 1; } -message DaitaSettings { bool enabled = 1; } +message DaitaSettings { + bool enabled = 1; + bool smart_routing = 2; +} message TunnelOptions { message OpenvpnOptions { optional uint32 mssfix = 1; } diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs index 3b2cabc33ea7..f782d3ab3242 100644 --- a/mullvad-management-interface/src/client.rs +++ b/mullvad-management-interface/src/client.rs @@ -378,6 +378,21 @@ impl MullvadProxyClient { Ok(()) } + #[cfg(daita)] + pub async fn set_enable_daita(&mut self, value: bool) -> Result<()> { + self.0.set_enable_daita(value).await.map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(daita)] + pub async fn set_daita_smart_routing(&mut self, value: bool) -> Result<()> { + self.0 + .set_daita_smart_routing(value) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + #[cfg(daita)] pub async fn set_daita_settings(&mut self, settings: DaitaSettings) -> Result<()> { let settings = types::DaitaSettings::from(settings); diff --git a/mullvad-management-interface/src/types/conversions/features.rs b/mullvad-management-interface/src/types/conversions/features.rs index ac235d81636c..85c85b8b7786 100644 --- a/mullvad-management-interface/src/types/conversions/features.rs +++ b/mullvad-management-interface/src/types/conversions/features.rs @@ -18,6 +18,7 @@ impl From for proto::FeatureIndicator mullvad_types::features::FeatureIndicator::CustomMtu => CustomMtu, mullvad_types::features::FeatureIndicator::CustomMssFix => CustomMssFix, mullvad_types::features::FeatureIndicator::Daita => Daita, + mullvad_types::features::FeatureIndicator::DaitaSmartRouting => DaitaSmartRouting, } } } @@ -39,6 +40,7 @@ impl From for mullvad_types::features::FeatureIndicator proto::FeatureIndicator::CustomMtu => Self::CustomMtu, proto::FeatureIndicator::CustomMssFix => Self::CustomMssFix, proto::FeatureIndicator::Daita => Self::Daita, + proto::FeatureIndicator::DaitaSmartRouting => Self::DaitaSmartRouting, } } } diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs index a8b69d87fafe..11718973c138 100644 --- a/mullvad-management-interface/src/types/conversions/relay_list.rs +++ b/mullvad-management-interface/src/types/conversions/relay_list.rs @@ -144,13 +144,13 @@ impl From for proto::Relay { )), _ => None, }, - location: relay.location.map(|location| proto::Location { - country: location.country, - country_code: location.country_code, - city: location.city, - city_code: location.city_code, - latitude: location.latitude, - longitude: location.longitude, + location: Some(proto::Location { + country: relay.location.country, + country_code: relay.location.country_code, + city: relay.location.city, + city_code: relay.location.city_code, + latitude: relay.location.latitude, + longitude: relay.location.longitude, }), } } @@ -299,14 +299,18 @@ impl TryFrom for mullvad_types::relay_list::Relay { provider: relay.provider, weight: relay.weight, endpoint_data, - location: relay.location.map(|location| MullvadLocation { - country: location.country, - country_code: location.country_code, - city: location.city, - city_code: location.city_code, - latitude: location.latitude, - longitude: location.longitude, - }), + location: relay + .location + .map(|location| MullvadLocation { + country: location.country, + country_code: location.country_code, + city: location.city, + city_code: location.city_code, + latitude: location.latitude, + longitude: location.longitude, + }) + .ok_or("missing relay location") + .map_err(FromProtobufTypeError::InvalidArgument)?, }) } } diff --git a/mullvad-management-interface/src/types/conversions/wireguard.rs b/mullvad-management-interface/src/types/conversions/wireguard.rs index b4e3bcaef722..9e40b4b52600 100644 --- a/mullvad-management-interface/src/types/conversions/wireguard.rs +++ b/mullvad-management-interface/src/types/conversions/wireguard.rs @@ -78,6 +78,7 @@ impl From for proto::DaitaSettings { fn from(settings: mullvad_types::wireguard::DaitaSettings) -> Self { proto::DaitaSettings { enabled: settings.enabled, + smart_routing: settings.smart_routing, } } } @@ -87,6 +88,7 @@ impl From for mullvad_types::wireguard::DaitaSettings { fn from(settings: proto::DaitaSettings) -> Self { mullvad_types::wireguard::DaitaSettings { enabled: settings.enabled, + smart_routing: settings.smart_routing, } } } diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index dc495b05aa1b..fb3c37064c52 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -124,6 +124,11 @@ pub struct AdditionalWireguardConstraints { /// If true, select WireGuard relays that support DAITA. If false, select any /// server. pub daita: bool, + + /// If true and multihop is disabled, will set up multihop with an automatic entry relay if + /// DAITA is enabled. + pub daita_smart_routing: bool, + /// If enabled, select relays that support PQ. pub quantum_resistant: QuantumResistantState, } @@ -345,6 +350,7 @@ impl<'a> TryFrom> for RelayQuery { } = wireguard_constraints; let AdditionalWireguardConstraints { daita, + daita_smart_routing, quantum_resistant, } = additional_constraints; WireguardRelayQuery { @@ -354,6 +360,7 @@ impl<'a> TryFrom> for RelayQuery { entry_location, obfuscation: ObfuscationQuery::from(obfuscation_settings), daita: Constraint::Only(daita), + daita_smart_routing: Constraint::Only(daita_smart_routing), quantum_resistant, } } @@ -718,12 +725,89 @@ impl RelaySelector { parsed_relays: &ParsedRelays, ) -> Result { let candidates = filter_matching_relay_list(query, parsed_relays, custom_lists); + + // are we using daita? + let using_daita = || query.wireguard_constraints().daita == Constraint::Only(true); + + // is the `candidates` list empty because DAITA is enabled? + let no_relay_because_daita = || { + let mut query = query.clone(); + let mut wireguard_constraints = query.wireguard_constraints().clone(); + wireguard_constraints.daita = Constraint::Any; + query.set_wireguard_constraints(wireguard_constraints)?; + let candidates = filter_matching_relay_list(&query, parsed_relays, custom_lists); + Result::<_, Error>::Ok(!candidates.is_empty()) + }; + + // is `smart_routing` enabled? + let smart_routing = || { + query + .wireguard_constraints() + .daita_smart_routing + .intersection(Constraint::Only(true)) + .is_some() + }; + + // if we found no matching relays because DAITA was enabled, and `smart_routing` is enabled, + // try enabling multihop and connecting using an automatically selected entry relay. + if candidates.is_empty() && using_daita() && no_relay_because_daita()? && smart_routing() { + return Self::get_wireguard_auto_multihop_config(query, custom_lists, parsed_relays); + } + helpers::pick_random_relay(&candidates) .cloned() .map(WireguardConfig::singlehop) .ok_or(Error::NoRelay) } + /// Select a valid Wireguard exit relay, together with with an automatically chosen entry relay. + /// + /// # Returns + /// * An `Err` if no entry/exit relay can be chosen + /// * `Ok(WireguardInner::Multihop)` otherwise + fn get_wireguard_auto_multihop_config( + query: &RelayQuery, + custom_lists: &CustomListsSettings, + parsed_relays: &ParsedRelays, + ) -> Result { + let mut exit_relay_query = query.clone(); + + // DAITA should only be enabled for the entry relay + let mut wireguard_constraints = exit_relay_query.wireguard_constraints().clone(); + wireguard_constraints.daita = Constraint::Only(false); + exit_relay_query.set_wireguard_constraints(wireguard_constraints)?; + + let exit_candidates = + filter_matching_relay_list(&exit_relay_query, parsed_relays, custom_lists); + let exit = helpers::pick_random_relay(&exit_candidates).ok_or(Error::NoRelay)?; + + // generate a list of potential entry relays, disregarding any location constraint + let mut entry_query = query.clone(); + entry_query.set_location(Constraint::Any)?; + let mut entry_candidates = + filter_matching_relay_list(&entry_query, parsed_relays, custom_lists) + .into_iter() + .map(|entry| RelayWithDistance::new_with_distance_from(entry, &exit.location)) + .collect_vec(); + + // sort entry relay candidates by distance, and pick one from those that are closest + entry_candidates.sort_unstable_by(|a, b| a.distance.total_cmp(&b.distance)); + let smallest_distance = entry_candidates.first().map(|relay| relay.distance); + let smallest_distance = smallest_distance.unwrap_or_default(); + let entry_candidates = entry_candidates + .into_iter() + // only consider the relay(s) with the smallest distance. note that the list is sorted. + // NOTE: we could relax this requirement, but since so few relays support DAITA + // (and this function is only used for daita) we might end up picking relays that are + // needlessly far away. Consider making this closure configurable if needed. + .take_while(|relay| relay.distance <= smallest_distance) + .map(|relay_with_distance| relay_with_distance.relay) + .collect_vec(); + let entry = pick_random_excluding(&entry_candidates, exit).ok_or(Error::NoRelay)?; + + Ok(WireguardConfig::multihop(exit.clone(), entry.clone())) + } + /// This function selects a valid entry and exit relay to be used in a multihop configuration. /// /// # Returns @@ -757,11 +841,6 @@ impl RelaySelector { let entry_candidates = filter_matching_relay_list(&entry_relay_query, parsed_relays, custom_lists); - fn pick_random_excluding<'a>(list: &'a [Relay], exclude: &'a Relay) -> Option<&'a Relay> { - list.iter() - .filter(|&a| a != exclude) - .choose(&mut thread_rng()) - } // We avoid picking the same relay for entry and exit by choosing one and excluding it when // choosing the other. let (exit, entry) = match (exit_candidates.as_slice(), entry_candidates.as_slice()) { @@ -938,10 +1017,9 @@ impl RelaySelector { Err(Error::NoBridge) } TransportProtocol::Tcp => { - let location = relay.location.as_ref().ok_or(Error::NoRelay)?; Self::get_bridge_for( bridge_query, - location, + &relay.location, // FIXME: This is temporary while talpid-core only supports TCP proxies TransportProtocol::Tcp, parsed_relays, @@ -1011,19 +1089,10 @@ impl RelaySelector { const MIN_BRIDGE_COUNT: usize = 5; let location = location.into(); - #[derive(Clone)] - struct RelayWithDistance { - relay: Relay, - distance: f64, - } - // Filter out all candidate bridges. let matching_bridges: Vec = relays .into_iter() - .map(|relay| RelayWithDistance { - distance: relay.location.as_ref().unwrap().distance_from(&location), - relay, - }) + .map(|relay| RelayWithDistance::new_with_distance_from(relay, location)) .sorted_unstable_by_key(|relay| relay.distance as usize) .take(MIN_BRIDGE_COUNT) .collect(); @@ -1059,7 +1128,7 @@ impl RelaySelector { let matching_locations: Vec = filter_matching_relay_list(query, parsed_relays, custom_lists) .into_iter() - .filter_map(|relay| relay.location) + .map(|relay| relay.location) .unique_by(|location| location.city.clone()) .collect(); @@ -1084,3 +1153,22 @@ impl RelaySelector { helpers::pick_random_relay(&candidates).cloned() } } + +fn pick_random_excluding<'a>(list: &'a [Relay], exclude: &'a Relay) -> Option<&'a Relay> { + list.iter() + .filter(|&a| a != exclude) + .choose(&mut thread_rng()) +} + +#[derive(Clone)] +struct RelayWithDistance { + distance: f64, + relay: Relay, +} + +impl RelayWithDistance { + fn new_with_distance_from(relay: Relay, from: impl Into) -> Self { + let distance = relay.location.distance_from(from); + RelayWithDistance { relay, distance } + } +} diff --git a/mullvad-relay-selector/src/relay_selector/parsed_relays.rs b/mullvad-relay-selector/src/relay_selector/parsed_relays.rs index 35c78a52aaa8..3a1663d9dd47 100644 --- a/mullvad-relay-selector/src/relay_selector/parsed_relays.rs +++ b/mullvad-relay-selector/src/relay_selector/parsed_relays.rs @@ -168,14 +168,14 @@ impl ParsedRelays { for city in &mut country.cities { for relay in &mut city.relays { // Append location data - relay.location = Some(Location { + relay.location = Location { country: country.name.clone(), country_code: country.code.clone(), city: city.name.clone(), city_code: city.code.clone(), latitude: city.latitude, longitude: city.longitude, - }); + }; // Append overrides if let Some(overrides) = remaining_overrides.remove(&relay.hostname) { diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs index 67fe43e2a354..2dc81b083386 100644 --- a/mullvad-relay-selector/src/relay_selector/query.rs +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -28,14 +28,14 @@ //! queries and ensure that queries are built in a type-safe manner, reducing the risk //! of runtime errors and improving code readability. -use crate::{AdditionalWireguardConstraints, Error}; +use crate::Error; use mullvad_types::{ constraints::Constraint, relay_constraints::{ BridgeConstraints, BridgeSettings, BridgeState, BridgeType, LocationConstraint, ObfuscationSettings, OpenVpnConstraints, Ownership, Providers, RelayConstraints, - SelectedObfuscation, ShadowsocksSettings, TransportPort, Udp2TcpObfuscationSettings, - WireguardConstraints, + RelaySettings, SelectedObfuscation, ShadowsocksSettings, TransportPort, + Udp2TcpObfuscationSettings, WireguardConstraints, }, wireguard::QuantumResistantState, Intersection, @@ -244,6 +244,13 @@ impl Default for RelayQuery { } } +impl From for RelaySettings { + fn from(query: RelayQuery) -> Self { + let (relay_constraints, ..) = query.into_settings(); + RelaySettings::from(relay_constraints) + } +} + /// A query for a relay with Wireguard-specific properties, such as `multihop` and [wireguard /// obfuscation][`SelectedObfuscation`]. /// @@ -261,6 +268,7 @@ pub struct WireguardRelayQuery { pub entry_location: Constraint, pub obfuscation: ObfuscationQuery, pub daita: Constraint, + pub daita_smart_routing: Constraint, pub quantum_resistant: QuantumResistantState, } @@ -346,6 +354,7 @@ impl WireguardRelayQuery { entry_location: Constraint::Any, obfuscation: ObfuscationQuery::Auto, daita: Constraint::Any, + daita_smart_routing: Constraint::Any, quantum_resistant: QuantumResistantState::Auto, } } @@ -367,14 +376,14 @@ impl Default for WireguardRelayQuery { } } -impl From for AdditionalWireguardConstraints { - /// The mapping from [`WireguardRelayQuery`] to [`AdditionalWireguardConstraints`]. +impl From for WireguardConstraints { + /// The mapping from [`WireguardRelayQuery`] to [`WireguardConstraints`]. fn from(value: WireguardRelayQuery) -> Self { - AdditionalWireguardConstraints { - daita: value - .daita - .unwrap_or(AdditionalWireguardConstraints::default().daita), - quantum_resistant: value.quantum_resistant, + WireguardConstraints { + port: value.port, + ip_version: value.ip_version, + entry_location: value.entry_location, + use_multihop: value.use_multihop.unwrap_or(false), } } } @@ -660,6 +669,17 @@ pub mod builder { } } + // impl-block for after DAITA is set + impl + RelayQueryBuilder> + { + /// Enable DAITA smart routing. + pub fn daita_smart_routing(mut self, constraint: impl Into>) -> Self { + self.query.wireguard_constraints.daita_smart_routing = constraint.into(); + self + } + } + impl RelayQueryBuilder> { /// Enable PQ support. pub fn quantum_resistant( diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index bdfd8e19d0ed..dc05d04eb2aa 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -21,6 +21,7 @@ use mullvad_relay_selector::{ use mullvad_types::{ constraints::Constraint, endpoint::MullvadEndpoint, + location::Location, relay_constraints::{ BridgeConstraints, BridgeState, GeographicLocationConstraint, Ownership, Providers, RelayOverride, TransportPort, @@ -32,6 +33,15 @@ use mullvad_types::{ }, }; +static DUMMY_LOCATION: LazyLock = LazyLock::new(|| Location { + country: "Sweden".to_string(), + country_code: "se".to_string(), + city: "Gothenburg".to_string(), + city_code: "got".to_string(), + latitude: 57.71, + longitude: 11.97, +}); + static RELAYS: LazyLock = LazyLock::new(|| RelayList { etag: None, countries: vec![RelayListCountry { @@ -62,7 +72,7 @@ static RELAYS: LazyLock = LazyLock::new(|| RelayList { daita: true, shadowsocks_extra_addr_in: vec![], }), - location: None, + location: DUMMY_LOCATION.clone(), }, Relay { hostname: "se10-wireguard".to_string(), @@ -83,7 +93,7 @@ static RELAYS: LazyLock = LazyLock::new(|| RelayList { daita: false, shadowsocks_extra_addr_in: vec![], }), - location: None, + location: DUMMY_LOCATION.clone(), }, Relay { hostname: "se-got-001".to_string(), @@ -97,7 +107,7 @@ static RELAYS: LazyLock = LazyLock::new(|| RelayList { provider: "provider2".to_string(), weight: 1, endpoint_data: RelayEndpointData::Openvpn, - location: None, + location: DUMMY_LOCATION.clone(), }, Relay { hostname: "se-got-002".to_string(), @@ -111,7 +121,7 @@ static RELAYS: LazyLock = LazyLock::new(|| RelayList { provider: "provider0".to_string(), weight: 1, endpoint_data: RelayEndpointData::Openvpn, - location: None, + location: DUMMY_LOCATION.clone(), }, Relay { hostname: "se-got-br-001".to_string(), @@ -125,7 +135,7 @@ static RELAYS: LazyLock = LazyLock::new(|| RelayList { provider: "provider3".to_string(), weight: 1, endpoint_data: RelayEndpointData::Bridge, - location: None, + location: DUMMY_LOCATION.clone(), }, SHADOWSOCKS_RELAY.clone(), ], @@ -209,7 +219,7 @@ static SHADOWSOCKS_RELAY: LazyLock = LazyLock::new(|| Relay { daita: false, shadowsocks_extra_addr_in: SHADOWSOCKS_RELAY_EXTRA_ADDRS.to_vec(), }), - location: None, + location: DUMMY_LOCATION.clone(), }); 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); @@ -497,7 +507,7 @@ fn test_wireguard_entry() { daita: false, shadowsocks_extra_addr_in: vec![], }), - location: None, + location: DUMMY_LOCATION.clone(), }, Relay { hostname: "se10-wireguard".to_string(), @@ -518,7 +528,7 @@ fn test_wireguard_entry() { daita: false, shadowsocks_extra_addr_in: vec![], }), - location: None, + location: DUMMY_LOCATION.clone(), }, ], }], @@ -1169,7 +1179,7 @@ fn test_include_in_country() { shadowsocks_extra_addr_in: vec![], daita: false, }), - location: None, + location: DUMMY_LOCATION.clone(), }, Relay { hostname: "se10-wireguard".to_string(), @@ -1190,7 +1200,7 @@ fn test_include_in_country() { shadowsocks_extra_addr_in: vec![], daita: false, }), - location: None, + location: DUMMY_LOCATION.clone(), }, ], }], @@ -1438,7 +1448,11 @@ fn test_daita() { let relay_selector = RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()); // Only pick relays that support DAITA - let query = RelayQueryBuilder::new().wireguard().daita().build(); + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .daita_smart_routing(false) + .build(); let relay = unwrap_entry_relay(relay_selector.get_relay_by_query(query).unwrap()); assert!( supports_daita(&relay), @@ -1449,12 +1463,58 @@ fn test_daita() { let query = RelayQueryBuilder::new() .wireguard() .daita() + .daita_smart_routing(false) .location(NON_DAITA_RELAY_LOCATION.clone()) .build(); relay_selector .get_relay_by_query(query) .expect_err("Expected to find no matching relay"); + // Should be able to connect to non-DAITA relay with smart_routing + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .daita_smart_routing(true) + .location(NON_DAITA_RELAY_LOCATION.clone()) + .build(); + let relay = relay_selector + .get_relay_by_query(query) + .expect("Expected to find a relay with daita_smart_routing"); + match relay { + GetRelay::Wireguard { + inner: WireguardConfig::Multihop { exit, entry }, + .. + } => { + assert!(supports_daita(&entry), "entry relay must support DAITA"); + assert!(!supports_daita(&exit), "exit relay must not support DAITA"); + } + wrong_relay => panic!( + "Relay selector should have picked two Wireguard relays, instead chose {wrong_relay:?}" + ), + } + + // Should be able to connect to DAITA relay with smart_routing + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .daita_smart_routing(true) + .location(DAITA_RELAY_LOCATION.clone()) + .build(); + let relay = relay_selector + .get_relay_by_query(query) + .expect("Expected to find a relay with daita_smart_routing"); + match relay { + GetRelay::Wireguard { + inner: WireguardConfig::Singlehop { exit }, + .. + } => { + assert!(supports_daita(&exit), "entry relay must support DAITA"); + } + wrong_relay => panic!( + "Relay selector should have picked a single Wireguard relay, instead chose {wrong_relay:?}" + ), + } + // DAITA-supporting relays can be picked even when it is disabled let query = RelayQueryBuilder::new() .wireguard() @@ -1477,6 +1537,7 @@ fn test_daita() { let query = RelayQueryBuilder::new() .wireguard() .daita() + .daita_smart_routing(false) .multihop() .build(); let relay = relay_selector.get_relay_by_query(query).unwrap(); @@ -1496,6 +1557,7 @@ fn test_daita() { let query = RelayQueryBuilder::new() .wireguard() .daita() + .daita_smart_routing(false) .multihop() .location(NON_DAITA_RELAY_LOCATION.clone()) .build(); diff --git a/mullvad-types/src/features.rs b/mullvad-types/src/features.rs index f847c94af057..ba40e42d1800 100644 --- a/mullvad-types/src/features.rs +++ b/mullvad-types/src/features.rs @@ -65,7 +65,14 @@ pub enum FeatureIndicator { ServerIpOverride, CustomMtu, CustomMssFix, + + /// Whether DAITA (without smart routing) is in use. + /// Mutually exclusive with [FeatureIndicator::DaitaSmartRouting]. Daita, + + /// Whether DAITA (with smart routing) is in use. + /// Mutually exclusive with [FeatureIndicator::Daita]. + DaitaSmartRouting, } impl FeatureIndicator { @@ -85,6 +92,7 @@ impl FeatureIndicator { FeatureIndicator::CustomMtu => "Custom MTU", FeatureIndicator::CustomMssFix => "Custom MSS", FeatureIndicator::Daita => "DAITA", + FeatureIndicator::DaitaSmartRouting => "DAITA: Smart Routing", } } } @@ -144,7 +152,6 @@ pub fn compute_feature_indicators( } TunnelType::Wireguard => { let quantum_resistant = endpoint.quantum_resistant; - let multihop = endpoint.entry_endpoint.is_some(); let udp_tcp = endpoint .obfuscation .as_ref() @@ -158,8 +165,28 @@ pub fn compute_feature_indicators( let mtu = settings.tunnel_options.wireguard.mtu.is_some(); + let mut daita_smart_routing = false; + let mut multihop = false; + + if let crate::relay_constraints::RelaySettings::Normal(constraints) = + &settings.relay_settings + { + multihop = endpoint.entry_endpoint.is_some() + && constraints.wireguard_constraints.use_multihop; + + #[cfg(daita)] + { + // Detect whether we're using "smart_routing" by checking if multihop is + // in use but not explicitly enabled. + daita_smart_routing = endpoint.daita + && endpoint.entry_endpoint.is_some() + && !constraints.wireguard_constraints.use_multihop + } + }; + + // Daita is mutually exclusive with DaitaSmartRouting #[cfg(daita)] - let daita = endpoint.daita; + let daita = endpoint.daita && !daita_smart_routing; vec![ (quantum_resistant, FeatureIndicator::QuantumResistance), @@ -169,6 +196,7 @@ pub fn compute_feature_indicators( (mtu, FeatureIndicator::CustomMtu), #[cfg(daita)] (daita, FeatureIndicator::Daita), + (daita_smart_routing, FeatureIndicator::DaitaSmartRouting), ] } }; @@ -190,6 +218,8 @@ mod tests { Endpoint, ObfuscationEndpoint, TransportProtocol, }; + use crate::relay_constraints::RelaySettings; + use super::*; #[test] @@ -302,6 +332,9 @@ mod tests { address: SocketAddr::from(([1, 2, 3, 4], 443)), protocol: TransportProtocol::Tcp, }); + if let RelaySettings::Normal(constraints) = &mut settings.relay_settings { + constraints.wireguard_constraints.use_multihop = true; + }; expected_indicators.0.insert(FeatureIndicator::Multihop); assert_eq!( compute_feature_indicators(&settings, &endpoint, false), @@ -343,6 +376,20 @@ mod tests { compute_feature_indicators(&settings, &endpoint, false), expected_indicators ); + + if let RelaySettings::Normal(constraints) = &mut settings.relay_settings { + constraints.wireguard_constraints.use_multihop = false; + }; + expected_indicators + .0 + .insert(FeatureIndicator::DaitaSmartRouting); + expected_indicators.0.remove(&FeatureIndicator::Daita); + expected_indicators.0.remove(&FeatureIndicator::Multihop); + assert_eq!( + compute_feature_indicators(&settings, &endpoint, false), + expected_indicators, + "DaitaSmartRouting should be enabled" + ); } // NOTE: If this match statement fails to compile, it means that a new feature indicator has @@ -362,6 +409,7 @@ mod tests { FeatureIndicator::CustomMtu => {} FeatureIndicator::CustomMssFix => {} FeatureIndicator::Daita => {} + FeatureIndicator::DaitaSmartRouting => {} } } } diff --git a/mullvad-types/src/location.rs b/mullvad-types/src/location.rs index 7807b65d6a91..d8a47176c190 100644 --- a/mullvad-types/src/location.rs +++ b/mullvad-types/src/location.rs @@ -19,7 +19,8 @@ pub struct Location { const RAIDUS_OF_EARTH: f64 = 6372.8; impl Location { - pub fn distance_from(&self, other: &Coordinates) -> f64 { + pub fn distance_from(&self, other: impl Into) -> f64 { + let other: Coordinates = other.into(); haversine_dist_deg( self.latitude, self.longitude, @@ -33,7 +34,7 @@ impl Location { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Coordinates { pub latitude: f64, pub longitude: f64, diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index f3e38adc7943..2b3d5099d249 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -210,21 +210,18 @@ impl GeographicLocationConstraint { impl Match for GeographicLocationConstraint { fn matches(&self, relay: &Relay) -> bool { match self { - GeographicLocationConstraint::Country(ref country) => relay - .location - .as_ref() - .map_or(false, |loc| loc.country_code == *country), + GeographicLocationConstraint::Country(ref country) => { + relay.location.country_code == *country + } GeographicLocationConstraint::City(ref country, ref city) => { - relay.location.as_ref().map_or(false, |loc| { - loc.country_code == *country && loc.city_code == *city - }) + let loc = &relay.location; + loc.country_code == *country && loc.city_code == *city } GeographicLocationConstraint::Hostname(ref country, ref city, ref hostname) => { - relay.location.as_ref().map_or(false, |loc| { - loc.country_code == *country - && loc.city_code == *city - && relay.hostname == *hostname - }) + let loc = &relay.location; + loc.country_code == *country + && loc.city_code == *city + && relay.hostname == *hostname } } } diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 1693720d4af1..afe8ba6378d3 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -90,7 +90,7 @@ pub struct Relay { pub provider: String, pub weight: u64, pub endpoint_data: RelayEndpointData, - pub location: Option, + pub location: Location, } impl Relay { @@ -134,7 +134,14 @@ impl PartialEq for Relay { /// # daita: false, /// # shadowsocks_extra_addr_in: vec![], /// # }), - /// # location: None, + /// # location: mullvad_types::location::Location { + /// # country: "Sweden".to_string(), + /// # country_code: "se".to_string(), + /// # city: "Gothenburg".to_string(), + /// # city_code: "got".to_string(), + /// # latitude: 57.71, + /// # longitude: 11.97, + /// # }, /// }; /// /// let mut different_relay = relay.clone(); diff --git a/mullvad-types/src/wireguard.rs b/mullvad-types/src/wireguard.rs index c920e9f0afbe..2850e7f10c38 100644 --- a/mullvad-types/src/wireguard.rs +++ b/mullvad-types/src/wireguard.rs @@ -80,6 +80,9 @@ pub struct QuantumResistantStateParseError; #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] pub struct DaitaSettings { pub enabled: bool, + + #[serde(default)] + pub smart_routing: bool, } /// Contains account specific wireguard data diff --git a/test/test-manager/src/tests/daita.rs b/test/test-manager/src/tests/daita.rs new file mode 100644 index 000000000000..912c811eccc2 --- /dev/null +++ b/test/test-manager/src/tests/daita.rs @@ -0,0 +1,200 @@ +use anyhow::{anyhow, bail, ensure, Context}; +use futures::StreamExt; +use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; +use mullvad_relay_selector::query::builder::RelayQueryBuilder; +use mullvad_types::{ + relay_constraints::GeographicLocationConstraint, relay_list::RelayEndpointData, + states::TunnelState, +}; +use talpid_types::{net::TunnelEndpoint, tunnel::ErrorStateCause}; +use test_macro::test_function; +use test_rpc::ServiceClient; + +use super::{helpers, Error, TestContext}; + +/// Test that daita and daita_smart_routing works by connecting +/// - to a non-DAITA relay with singlehop (should block) +/// - to a DAITA relay with singlehop +/// - to a DAITA relay with auto-multihop using smart_routing +/// - to a DAITA relay with explicit multihop +/// - to a non-DAITA relay with multihop (should block) +/// +/// # Limitations +/// +/// The test does not analyze any traffic, nor verify that DAITA is in use in any way except +/// by looking at [TunnelEndpoint::daita]. +#[test_function] +pub async fn test_daita( + _ctx: TestContext, + _rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> anyhow::Result<()> { + let relay_list = mullvad_client.get_relay_locations().await?; + let wg_relays = relay_list + .relays() + .flat_map(|relay| match &relay.endpoint_data { + RelayEndpointData::Wireguard(wireguard) => Some((relay, wireguard)), + _ => None, + }); + + // Select two relays to use for the test, one with DAITA and one without. + let daita_relay = wg_relays + .clone() + .find(|(_relay, wireguard_data)| wireguard_data.daita) + .map(|(relay, _)| relay) + .context("Failed to find a daita wireguard relay")?; + log::info!("Selected daita relay: {}", daita_relay.hostname); + let daita_relay_location = GeographicLocationConstraint::hostname( + &daita_relay.location.country_code, + &daita_relay.location.city_code, + &daita_relay.hostname, + ); + + let non_daita_relay = wg_relays + .clone() + .find(|(_relay, wireguard_data)| !wireguard_data.daita) + .map(|(relay, _)| relay) + .context("Failed to find a non-daita wireguard relay")?; + let non_daita_relay_location = GeographicLocationConstraint::hostname( + &non_daita_relay.location.country_code, + &non_daita_relay.location.city_code, + &non_daita_relay.hostname, + ); + log::info!("Selected non-daita relay: {}", non_daita_relay.hostname); + + let non_daita_location_query = RelayQueryBuilder::new() + .wireguard() + .location(non_daita_relay_location.clone()) + .build(); + + let daita_location_query = RelayQueryBuilder::new() + .wireguard() + .location(daita_relay_location.clone()) + .build(); + + let daita_to_non_daita_multihop_query = RelayQueryBuilder::new() + .wireguard() + .multihop() + .entry(daita_relay_location.clone()) + .location(non_daita_relay_location.clone()) + .build(); + + let non_daita_multihop_query = RelayQueryBuilder::new() + .wireguard() + .multihop() + .entry(non_daita_relay_location.clone()) + .build(); + + let mut events = mullvad_client + .events_listen() + .await? + .inspect(|event| log::debug!("New daemon event: {event:?}")); + + log::info!("Connecting to non-daita relay with DAITA smart routing"); + { + helpers::set_relay_settings(&mut mullvad_client, non_daita_location_query.clone()).await?; + mullvad_client.set_enable_daita(true).await?; + mullvad_client.connect_tunnel().await?; + let state = wait_for_daemon_reconnect(&mut events) + .await + .context("Failed to connect with smart_routing enabled")?; + + let endpoint: &TunnelEndpoint = state.endpoint().ok_or(anyhow!("No endpoint"))?; + ensure!(endpoint.daita, "DAITA must be used"); + ensure!(endpoint.entry_endpoint.is_some(), "multihop must be used"); + + log::info!("Successfully multihopped with use smart_routing"); + } + + log::info!("Connecting to non-daita relay with DAITA but no smart routing"); + { + mullvad_client.set_daita_smart_routing(false).await?; + + let result = wait_for_daemon_reconnect(&mut events).await; + let Err(Error::UnexpectedErrorState(state)) = result else { + bail!("Connection failed unsuccessfully, reason: {:?}", result); + }; + let ErrorStateCause::TunnelParameterError(_) = state.cause() else { + bail!("Connection failed unsuccessfully, cause: {}", state.cause()); + }; + + log::info!("Failed to connect, this is expected!"); + } + + log::info!("Connecting to daita relay with smart_routing"); + { + helpers::set_relay_settings(&mut mullvad_client, daita_location_query).await?; + + let state = wait_for_daemon_reconnect(&mut events) + .await + .context("Failed to connect to daita location with smart_routing enabled")?; + + let endpoint = state.endpoint().context("No endpoint")?; + ensure!(endpoint.daita, "DAITA must be used"); + ensure!( + endpoint.entry_endpoint.is_none(), + "multihop must not be used" + ); + + log::info!("Successfully singlehopped with smart_routing"); + } + + log::info!("Connecting to daita relay with multihop"); + { + helpers::set_relay_settings(&mut mullvad_client, daita_to_non_daita_multihop_query).await?; + let state = wait_for_daemon_reconnect(&mut events) + .await + .context("Failed to connect via daita location with multihop enabled")?; + + let endpoint = state.endpoint().context("No endpoint")?; + ensure!(endpoint.daita, "DAITA must be used"); + ensure!(endpoint.entry_endpoint.is_some(), "multihop must be used"); + + log::info!("Successfully connected with multihop"); + } + + log::info!("Connecting to non_daita relay with multihop"); + { + helpers::set_relay_settings(&mut mullvad_client, non_daita_multihop_query).await?; + let result = wait_for_daemon_reconnect(&mut events).await; + let Err(Error::UnexpectedErrorState(state)) = result else { + bail!("Connection failed unsuccessfully, reason: {:?}", result); + }; + let ErrorStateCause::TunnelParameterError(_) = state.cause() else { + bail!("Connection failed unsuccessfully, cause: {}", state.cause()); + }; + + log::info!("Failed to connect, this is expected!"); + } + + Ok(()) +} + +async fn wait_for_daemon_reconnect( + mut event_stream: impl futures::Stream> + + Unpin, +) -> Result { + // wait until the daemon informs us that it's trying to connect + helpers::find_daemon_event(&mut event_stream, |event| match event { + DaemonEvent::TunnelState(state) => Some(match state { + TunnelState::Connecting { .. } => Ok(state), + TunnelState::Connected { .. } => return None, + TunnelState::Disconnecting { .. } => return None, + TunnelState::Disconnected { .. } => Err(Error::UnexpectedTunnelState(Box::new(state))), + TunnelState::Error(state) => Err(Error::UnexpectedErrorState(state)), + }), + _ => None, + }) + .await??; + + // then wait until the daemon informs us that it connected (or failed) + helpers::find_daemon_event(&mut event_stream, |event| match event { + DaemonEvent::TunnelState(state) => match state { + TunnelState::Connecting { .. } => None, + TunnelState::Connected { .. } => Some(Ok(state)), + _ => Some(Err(Error::UnexpectedTunnelState(Box::new(state)))), + }, + _ => None, + }) + .await? +} diff --git a/test/test-manager/src/tests/dns.rs b/test/test-manager/src/tests/dns.rs index d7ea3d021d6a..69f98450caf1 100644 --- a/test/test-manager/src/tests/dns.rs +++ b/test/test-manager/src/tests/dns.rs @@ -642,7 +642,7 @@ async fn connect_local_wg_relay(mullvad_client: &mut MullvadProxyClient) -> Resu .set_quantum_resistant_tunnel(QuantumResistantState::Off) .await?; mullvad_client - .set_daita_settings(DaitaSettings { enabled: false }) + .set_daita_settings(DaitaSettings::default()) .await?; let peer_addr: SocketAddr = SocketAddr::new( diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs index 6c51b1f18599..3eb0be0cc238 100644 --- a/test/test-manager/src/tests/helpers.rs +++ b/test/test-manager/src/tests/helpers.rs @@ -17,7 +17,6 @@ use mullvad_relay_selector::{ }; use mullvad_types::{ constraints::Constraint, - location::Location, relay_constraints::{ GeographicLocationConstraint, LocationConstraint, RelayConstraints, RelaySettings, }, @@ -710,7 +709,7 @@ pub async fn constrain_to_relay( .. } | GetRelay::OpenVpn { exit, .. } => { - let location = into_constraint(&exit)?; + let location = into_constraint(&exit); let (mut relay_constraints, ..) = query.into_settings(); relay_constraints.location = location; Ok((exit, relay_constraints)) @@ -736,22 +735,14 @@ pub async fn constrain_to_relay( /// # Panics /// /// The relay must have a location set. -pub fn into_constraint(relay: &Relay) -> anyhow::Result> { - relay - .location - .as_ref() - .map( - |Location { - country_code, - city_code, - .. - }| { - GeographicLocationConstraint::hostname(country_code, city_code, &relay.hostname) - }, - ) - .map(LocationConstraint::Location) - .map(Constraint::Only) - .ok_or(anyhow!("relay is missing location")) +pub fn into_constraint(relay: &Relay) -> Constraint { + let constraint = GeographicLocationConstraint::hostname( + relay.location.country_code.clone(), + relay.location.city_code.clone(), + &relay.hostname, + ); + + Constraint::Only(LocationConstraint::Location(constraint)) } /// Ping monitoring made easy! diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs index 7e4cbc9eb6a5..bc17a7f3f64a 100644 --- a/test/test-manager/src/tests/mod.rs +++ b/test/test-manager/src/tests/mod.rs @@ -2,6 +2,7 @@ mod access_methods; mod account; pub mod config; mod cve_2019_14899; +mod daita; mod dns; mod helpers; mod install; @@ -57,7 +58,10 @@ pub enum Error { #[error("The daemon returned an error: {0}")] Daemon(String), - #[error("The daemon ended up in the error state")] + #[error("The daemon ended up in the the wrong tunnel-state: {0:?}")] + UnexpectedTunnelState(Box), + + #[error("The daemon ended up in the error state: {0:?}")] UnexpectedErrorState(talpid_types::tunnel::ErrorState), #[error("The gRPC client ran into an error: {0}")] diff --git a/test/test-manager/src/tests/relay_ip_overrides.rs b/test/test-manager/src/tests/relay_ip_overrides.rs index 48df8ff0e4b6..3805412ee92a 100644 --- a/test/test-manager/src/tests/relay_ip_overrides.rs +++ b/test/test-manager/src/tests/relay_ip_overrides.rs @@ -298,12 +298,7 @@ async fn pick_a_relay( let relay_ip = relay.ipv4_addr_in; let hostname = relay.hostname.clone(); - let city = relay - .location - .as_ref() - .ok_or(anyhow!("Got Relay with an unknown location"))? - .city_code - .clone(); + let city = relay.location.city_code.clone(); log::info!("selected {hostname} ({relay_ip})"); let location = GeographicLocationConstraint::Hostname(country, city, hostname.clone()).into(); diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index 575339c5a815..6fb57adc1498 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -390,38 +390,6 @@ pub async fn test_wireguard_autoconnect( Ok(()) } -/// Test connecting to a WireGuard relay using DAITA. -/// -/// # Limitations -/// -/// The test does not analyze any traffic, nor verify that DAITA is in use. -#[test_function] -pub async fn test_daita( - _: TestContext, - rpc: ServiceClient, - mut mullvad_client: MullvadProxyClient, -) -> anyhow::Result<()> { - log::info!("Connecting to relay with DAITA"); - - apply_settings_from_relay_query( - &mut mullvad_client, - RelayQueryBuilder::new().wireguard().build(), - ) - .await?; - - mullvad_client - .set_daita_settings(wireguard::DaitaSettings { enabled: true }) - .await - .context("Failed to enable daita")?; - - connect_and_wait(&mut mullvad_client).await?; - - log::info!("Check that the connection works"); - let _ = helpers::geoip_lookup_with_retries(&rpc).await?; - - Ok(()) -} - /// Test whether the daemon automatically connects on reboot when using /// OpenVPN. ///