summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2024-10-22 14:27:41 +0200
committerOskar <oskar@mullvad.net>2024-10-22 14:27:41 +0200
commit8a9c15f4f098c8197c46223bd9a67c734ab020d3 (patch)
treece80a8e9cec4c509beeb5e84cce36c3c015e57a7
parentd339daefa6784a1e734ab20bb4a7db9fc8482ec5 (diff)
parentc63d501223bac2a430a68d824dd37a3f611f4310 (diff)
downloadmullvadvpn-8a9c15f4f098c8197c46223bd9a67c734ab020d3.tar.xz
mullvadvpn-8a9c15f4f098c8197c46223bd9a67c734ab020d3.zip
Merge branch 'implement-ui-for-smart-routing-override-multihop-des-1302'
-rw-r--r--gui/locales/messages.pot16
-rw-r--r--gui/src/main/default-settings.ts4
-rw-r--r--gui/src/renderer/components/select-location/SelectLocation.tsx175
-rw-r--r--gui/src/renderer/components/select-location/SelectLocationStyles.tsx22
-rw-r--r--gui/test/e2e/mocked/select-location.spec.ts148
5 files changed, 272 insertions, 93 deletions
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index 58f3fcc5e9..3850b7f239 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -1345,6 +1345,10 @@ msgctxt "select-location-view"
msgid "(Added)"
msgstr ""
+msgctxt "select-location-view"
+msgid "%(daita)s overrides %(multihop)s. To use %(multihop)s, please enable “%(directOnly)s” or disable %(daita)s in the %(daita)s settings."
+msgstr ""
+
#. This is used for appending information about a location.
#. E.g. "Gothenburg (Entry)" if Gothenburg has been selected as the entrypoint.
#. Available placeholders:
@@ -1406,6 +1410,10 @@ msgid "Filtered:"
msgstr ""
msgctxt "select-location-view"
+msgid "Go to %(daita)s settings"
+msgstr ""
+
+msgctxt "select-location-view"
msgid "List names must be unique."
msgstr ""
@@ -1429,14 +1437,6 @@ msgctxt "select-location-view"
msgid "The app selects a random bridge server, but servers have a higher probability the closer they are to you."
msgstr ""
-msgctxt "select-location-view"
-msgid "While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server)."
-msgstr ""
-
-msgctxt "select-location-view"
-msgid "While connected, your traffic will be routed through two secure locations, the entry point and the exit point (needs to be two different VPN servers)."
-msgstr ""
-
msgctxt "settings-import"
msgid "Clear all overrides"
msgstr ""
diff --git a/gui/src/main/default-settings.ts b/gui/src/main/default-settings.ts
index e11a7434e1..46355dd439 100644
--- a/gui/src/main/default-settings.ts
+++ b/gui/src/main/default-settings.ts
@@ -53,6 +53,10 @@ export function getDefaultSettings(): ISettings {
wireguard: {
mtu: undefined,
quantumResistant: undefined,
+ daita: {
+ enabled: false,
+ directOnly: false,
+ },
},
dns: {
state: 'default',
diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx
index 01297f5630..438fdc430d 100644
--- a/gui/src/renderer/components/select-location/SelectLocation.tsx
+++ b/gui/src/renderer/components/select-location/SelectLocation.tsx
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { sprintf } from 'sprintf-js';
-import { colors } from '../../../config.json';
+import { colors, strings } from '../../../config.json';
import { Ownership } from '../../../shared/daemon-rpc-types';
import { messages } from '../../../shared/gettext';
import { useRelaySettingsUpdater } from '../../lib/constraint-updater';
@@ -39,14 +39,14 @@ import { useSelectLocationContext } from './SelectLocationContainer';
import {
StyledClearFilterButton,
StyledContent,
+ StyledDaitaSettingsButton,
StyledFilter,
StyledFilterRow,
- StyledHeaderSubTitle,
StyledNavigationBarAttachment,
- StyledNoResult,
- StyledNoResultText,
StyledScopeBar,
StyledSearchBar,
+ StyledSelectionUnavailable,
+ StyledSelectionUnavailableText,
} from './SelectLocationStyles';
import { SpacePreAllocationView } from './SpacePreAllocationView';
import {
@@ -163,81 +163,69 @@ export default function SelectLocation() {
</ScopeBarItem>
<ScopeBarItem>{messages.pgettext('select-location-view', 'Exit')}</ScopeBarItem>
</StyledScopeBar>
-
- {tunnelProtocol === 'openvpn' ? (
- <StyledHeaderSubTitle>
- {messages.pgettext(
- 'select-location-view',
- 'While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server).',
- )}
- </StyledHeaderSubTitle>
- ) : (
- <StyledHeaderSubTitle>
- {messages.pgettext(
- 'select-location-view',
- 'While connected, your traffic will be routed through two secure locations, the entry point and the exit point (needs to be two different VPN servers).',
- )}
- </StyledHeaderSubTitle>
- )}
</>
)}
- {showFilters && (
- <StyledFilterRow>
- {messages.pgettext('select-location-view', 'Filtered:')}
+ {locationType === LocationType.entry && daita && !directOnly ? null : (
+ <>
+ {showFilters && (
+ <StyledFilterRow>
+ {messages.pgettext('select-location-view', 'Filtered:')}
- {showOwnershipFilter && (
- <StyledFilter>
- {ownershipFilterLabel(ownership)}
- <StyledClearFilterButton
- aria-label={messages.gettext('Clear')}
- onClick={onClearOwnership}>
- <ImageView
- height={16}
- width={16}
- source="icon-close"
- tintColor={colors.white60}
- tintHoverColor={colors.white80}
- />
- </StyledClearFilterButton>
- </StyledFilter>
- )}
+ {showOwnershipFilter && (
+ <StyledFilter>
+ {ownershipFilterLabel(ownership)}
+ <StyledClearFilterButton
+ aria-label={messages.gettext('Clear')}
+ onClick={onClearOwnership}>
+ <ImageView
+ height={16}
+ width={16}
+ source="icon-close"
+ tintColor={colors.white60}
+ tintHoverColor={colors.white80}
+ />
+ </StyledClearFilterButton>
+ </StyledFilter>
+ )}
- {showProvidersFilter && (
- <StyledFilter>
- {sprintf(
- messages.pgettext(
- 'select-location-view',
- 'Providers: %(numberOfProviders)d',
- ),
- { numberOfProviders: filteredProviders.length },
+ {showProvidersFilter && (
+ <StyledFilter>
+ {sprintf(
+ messages.pgettext(
+ 'select-location-view',
+ 'Providers: %(numberOfProviders)d',
+ ),
+ { numberOfProviders: filteredProviders.length },
+ )}
+ <StyledClearFilterButton
+ aria-label={messages.gettext('Clear')}
+ onClick={onClearProviders}>
+ <ImageView
+ height={16}
+ width={16}
+ source="icon-close"
+ tintColor={colors.white60}
+ tintHoverColor={colors.white80}
+ />
+ </StyledClearFilterButton>
+ </StyledFilter>
)}
- <StyledClearFilterButton
- aria-label={messages.gettext('Clear')}
- onClick={onClearProviders}>
- <ImageView
- height={16}
- width={16}
- source="icon-close"
- tintColor={colors.white60}
- tintHoverColor={colors.white80}
- />
- </StyledClearFilterButton>
- </StyledFilter>
- )}
- {showDaitaFilter && (
- <StyledFilter>
- {sprintf(
- messages.pgettext('select-location-view', 'Setting: %(settingName)s'),
- { settingName: 'DAITA' },
+ {showDaitaFilter && (
+ <StyledFilter>
+ {sprintf(
+ messages.pgettext('select-location-view', 'Setting: %(settingName)s'),
+ { settingName: 'DAITA' },
+ )}
+ </StyledFilter>
)}
- </StyledFilter>
+ </StyledFilterRow>
)}
- </StyledFilterRow>
- )}
- <StyledSearchBar searchTerm={searchValue} onSearch={updateSearchTerm} />
+ <StyledSearchBar searchTerm={searchValue} onSearch={updateSearchTerm} />
+ </>
+ )}
</StyledNavigationBarAttachment>
<NavigationScrollbars ref={scrollViewRef}>
@@ -273,6 +261,9 @@ function SelectLocationContent() {
const [onSelectEntryRelay, onSelectEntrySpecial] = useOnSelectEntryLocation();
const [onSelectBridgeRelay, onSelectBridgeSpecial] = useOnSelectBridgeLocation();
+ const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false);
+ const directOnly = useSelector((state) => state.settings.wireguard.daita?.directOnly ?? false);
+
const relaySettings = useNormalRelaySettings();
const bridgeSettings = useSelector((state) => state.settings.bridgeSettings);
@@ -311,6 +302,10 @@ function SelectLocationContent() {
</>
);
} else if (relaySettings?.tunnelProtocol !== 'openvpn') {
+ if (daita && !directOnly) {
+ return <DisabledEntrySelection />;
+ }
+
return (
<>
<CustomLists selectedElementRef={selectedLocationRef} onSelect={onSelectEntryRelay} />
@@ -409,15 +404,47 @@ function NoSearchResult(props: NoSearchResultProps) {
}
return (
- <StyledNoResult>
- <StyledNoResultText>
+ <StyledSelectionUnavailable>
+ <StyledSelectionUnavailableText>
{formatHtml(
sprintf(messages.gettext('No result for <b>%(searchTerm)s</b>.'), {
searchTerm,
}),
)}
- </StyledNoResultText>
- <StyledNoResultText>{messages.gettext('Try a different search.')}</StyledNoResultText>
- </StyledNoResult>
+ </StyledSelectionUnavailableText>
+ <StyledSelectionUnavailableText>
+ {messages.gettext('Try a different search.')}
+ </StyledSelectionUnavailableText>
+ </StyledSelectionUnavailable>
+ );
+}
+
+function DisabledEntrySelection() {
+ const { push } = useHistory();
+
+ const multihop = messages.pgettext('settings-view', 'Multihop');
+ const directOnly = messages.gettext('Direct only');
+
+ const navigateToDaitaSettings = useCallback(() => {
+ push(RoutePath.daitaSettings);
+ }, [push]);
+
+ return (
+ <StyledSelectionUnavailable>
+ <StyledSelectionUnavailableText>
+ {sprintf(
+ messages.pgettext(
+ 'select-location-view',
+ '%(daita)s overrides %(multihop)s. To use %(multihop)s, please enable “%(directOnly)s” or disable %(daita)s in the %(daita)s settings.',
+ ),
+ { daita: strings.daita, multihop, directOnly },
+ )}
+ </StyledSelectionUnavailableText>
+ <StyledDaitaSettingsButton onClick={navigateToDaitaSettings}>
+ {sprintf(messages.pgettext('select-location-view', 'Go to %(daita)s settings'), {
+ daita: strings.daita,
+ })}
+ </StyledDaitaSettingsButton>
+ </StyledSelectionUnavailable>
);
}
diff --git a/gui/src/renderer/components/select-location/SelectLocationStyles.tsx b/gui/src/renderer/components/select-location/SelectLocationStyles.tsx
index fd401c8e8c..ff83a6fc8d 100644
--- a/gui/src/renderer/components/select-location/SelectLocationStyles.tsx
+++ b/gui/src/renderer/components/select-location/SelectLocationStyles.tsx
@@ -4,7 +4,7 @@ import { colors } from '../../../config.json';
import * as Cell from '../cell';
import { normalText, tinyText } from '../common-styles';
import SearchBar from '../SearchBar';
-import { HeaderSubTitle } from '../SettingsHeader';
+import { SmallButton } from '../SmallButton';
import { ScopeBar } from './ScopeBar';
export const StyledContent = styled.div({
@@ -15,17 +15,17 @@ export const StyledContent = styled.div({
});
export const StyledScopeBar = styled(ScopeBar)({
- marginBottom: '14px',
+ marginBottom: '16px',
});
export const StyledNavigationBarAttachment = styled.div({
- padding: '0 16px 14px',
+ padding: '0 16px 16px',
});
export const StyledFilterRow = styled.div({
...tinyText,
color: colors.white,
- margin: '0 6px 14px',
+ margin: '0 6px 16px',
});
export const StyledFilter = styled.div({
@@ -48,26 +48,26 @@ export const StyledClearFilterButton = styled.div({
backgroundColor: 'transparent',
});
-export const StyledHeaderSubTitle = styled(HeaderSubTitle)({
- display: 'block',
- margin: '0 6px 14px',
-});
-
export const StyledSearchBar = styled(SearchBar)({
margin: '0 6px',
});
-export const StyledNoResult = styled(Cell.CellFooter)({
+export const StyledSelectionUnavailable = styled(Cell.CellFooter)({
display: 'flex',
flexDirection: 'column',
paddingTop: 0,
marginTop: 0,
});
-export const StyledNoResultText = styled(Cell.CellFooterText)({
+export const StyledSelectionUnavailableText = styled(Cell.CellFooterText)({
textAlign: 'center',
});
export const StyledAllLocationsTitle = styled(Cell.Label)(normalText, {
fontWeight: 'normal',
});
+
+export const StyledDaitaSettingsButton = styled(SmallButton)({
+ marginLeft: 0,
+ marginTop: '24px',
+});
diff --git a/gui/test/e2e/mocked/select-location.spec.ts b/gui/test/e2e/mocked/select-location.spec.ts
new file mode 100644
index 0000000000..342d511b9e
--- /dev/null
+++ b/gui/test/e2e/mocked/select-location.spec.ts
@@ -0,0 +1,148 @@
+import { expect, test } from '@playwright/test';
+import { Page } from 'playwright';
+
+import { colors } from '../../../src/config.json';
+import { getDefaultSettings } from '../../../src/main/default-settings';
+import {
+ IRelayList,
+ IRelayListWithEndpointData,
+ ISettings,
+ IWireguardEndpointData,
+} from '../../../src/shared/daemon-rpc-types';
+import { MockedTestUtils, startMockedApp } from './mocked-utils';
+
+const relayList: IRelayList = {
+ countries: [
+ {
+ name: 'Sweden',
+ code: 'se',
+ cities: [
+ {
+ name: 'Gothenburg',
+ code: 'got',
+ latitude: 58,
+ longitude: 12,
+ relays: [
+ {
+ hostname: 'se-got-wg-101',
+ provider: 'mullvad',
+ ipv4AddrIn: '10.0.0.1',
+ includeInCountry: true,
+ active: true,
+ weight: 0,
+ owned: true,
+ endpointType: 'wireguard',
+ daita: true,
+ },
+ {
+ hostname: 'se-got-wg-102',
+ provider: 'mullvad',
+ ipv4AddrIn: '10.0.0.2',
+ includeInCountry: true,
+ active: true,
+ weight: 0,
+ owned: true,
+ endpointType: 'wireguard',
+ daita: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+const wireguardEndpointData: IWireguardEndpointData = {
+ portRanges: [],
+ udp2tcpPorts: [],
+};
+
+let page: Page;
+let util: MockedTestUtils;
+
+test.beforeAll(async () => {
+ ({ page, util } = await startMockedApp());
+ await setMultihop();
+ await util.waitForNavigation(() => page.getByLabel('Select location').click());
+});
+
+test.afterAll(async () => {
+ await page.close();
+});
+
+async function setMultihop() {
+ const settings = getDefaultSettings();
+ if ('normal' in settings.relaySettings) {
+ settings.relaySettings.normal.wireguardConstraints.useMultihop = true;
+ }
+
+ await util.sendMockIpcResponse<ISettings>({
+ channel: 'settings-',
+ response: settings,
+ });
+
+ await util.sendMockIpcResponse<IRelayListWithEndpointData>({
+ channel: 'relays-',
+ response: { relayList, wireguardEndpointData },
+ });
+}
+
+test('App should show entry selection', async () => {
+ const entryTab = page.getByText('Entry');
+ await entryTab.click();
+ await expect(entryTab).toHaveCSS('background-color', colors.green);
+
+ const sweden = page.getByText('Sweden');
+ await expect(sweden).toBeVisible();
+});
+
+test('App should show exit selection', async () => {
+ const exitTab = page.getByText('Exit');
+ await exitTab.click();
+ await expect(exitTab).toHaveCSS('background-color', colors.green);
+
+ const sweden = page.getByText('Sweden');
+ await expect(sweden).toBeVisible();
+});
+
+test("App shouldn't show entry selection when daita is enabled without direct only", async () => {
+ const settings = getDefaultSettings();
+ if ('normal' in settings.relaySettings && settings.tunnelOptions.wireguard.daita) {
+ settings.relaySettings.normal.wireguardConstraints.useMultihop = true;
+ settings.tunnelOptions.wireguard.daita.enabled = true;
+ settings.tunnelOptions.wireguard.daita.directOnly = false;
+ }
+
+ await util.sendMockIpcResponse<ISettings>({
+ channel: 'settings-',
+ response: settings,
+ });
+
+ const entryTab = page.getByText('Entry');
+ await entryTab.click();
+ await expect(entryTab).toHaveCSS('background-color', colors.green);
+
+ const sweden = page.getByText('Sweden');
+ await expect(sweden).not.toBeVisible();
+});
+
+test('App should show entry selection when daita is enabled with direct only', async () => {
+ const settings = getDefaultSettings();
+ if ('normal' in settings.relaySettings && settings.tunnelOptions.wireguard.daita) {
+ settings.relaySettings.normal.wireguardConstraints.useMultihop = true;
+ settings.tunnelOptions.wireguard.daita.enabled = true;
+ settings.tunnelOptions.wireguard.daita.directOnly = true;
+ }
+
+ await util.sendMockIpcResponse<ISettings>({
+ channel: 'settings-',
+ response: settings,
+ });
+
+ const entryTab = page.getByText('Entry');
+ await entryTab.click();
+ await expect(entryTab).toHaveCSS('background-color', colors.green);
+
+ const sweden = page.getByText('Sweden');
+ await expect(sweden).toBeVisible();
+});