summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-12-06 10:42:20 +0100
committerOskar Nyberg <oskar@mullvad.net>2022-01-03 13:48:03 +0100
commita75abb22ed42c8c811aa4967ba82be5077f52ea2 (patch)
treeeab547f9b27566fa34fab981de6e0eb398c8fb46
parent9b6f208a2135344cb7b6e28ac28b76d31b54130c (diff)
downloadmullvadvpn-a75abb22ed42c8c811aa4967ba82be5077f52ea2.tar.xz
mullvadvpn-a75abb22ed42c8c811aa4967ba82be5077f52ea2.zip
Add entry location selection in SelectLocation
-rw-r--r--gui/src/renderer/components/SelectLocation.tsx188
-rw-r--r--gui/src/renderer/containers/SelectLocationPage.tsx56
-rw-r--r--gui/src/renderer/redux/userinterface/actions.ts15
-rw-r--r--gui/src/renderer/redux/userinterface/reducers.ts10
4 files changed, 168 insertions, 101 deletions
diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx
index 528e8129ef..902ca27c80 100644
--- a/gui/src/renderer/components/SelectLocation.tsx
+++ b/gui/src/renderer/components/SelectLocation.tsx
@@ -1,13 +1,12 @@
import React from 'react';
import { sprintf } from 'sprintf-js';
import { colors } from '../../config.json';
-import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types';
+import { LiftedConstraint, RelayLocation, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { IRelayLocationRedux } from '../redux/settings/reducers';
-import { LocationScope } from '../redux/userinterface/reducers';
import BridgeLocations, { SpecialBridgeLocationType } from './BridgeLocations';
import { CustomScrollbarsRef } from './CustomScrollbars';
-import ExitLocations from './ExitLocations';
+import { EntryLocations, ExitLocations } from './Locations';
import ImageView from './ImageView';
import { Layout } from './Layout';
import LocationList, { LocationSelection, LocationSelectionType } from './LocationList';
@@ -37,25 +36,32 @@ import {
import { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
interface IProps {
- locationScope: LocationScope;
selectedExitLocation?: RelayLocation;
+ selectedEntryLocation?: RelayLocation;
selectedBridgeLocation?: LiftedConstraint<RelayLocation>;
relayLocations: IRelayLocationRedux[];
bridgeLocations: IRelayLocationRedux[];
- allowBridgeSelection: boolean;
+ allowEntrySelection: boolean;
+ tunnelProtocol: LiftedConstraint<TunnelProtocol>;
providers: string[];
onClose: () => void;
onViewFilterByProvider: () => void;
- onChangeLocationScope: (location: LocationScope) => void;
onSelectExitLocation: (location: RelayLocation) => void;
+ onSelectEntryLocation: (location: RelayLocation) => void;
onSelectBridgeLocation: (location: RelayLocation) => void;
onSelectClosestToExit: () => void;
onClearProviders: () => void;
}
+enum LocationScope {
+ entry = 0,
+ exit,
+}
+
interface IState {
showFilterMenu: boolean;
headingHeight: number;
+ locationScope: LocationScope;
}
interface ISelectLocationSnapshot {
@@ -64,17 +70,19 @@ interface ISelectLocationSnapshot {
}
export default class SelectLocation extends React.Component<IProps, IState> {
- public state = { showFilterMenu: false, headingHeight: 0 };
+ public state = { showFilterMenu: false, headingHeight: 0, locationScope: LocationScope.exit };
private scrollView = React.createRef<CustomScrollbarsRef>();
private spacePreAllocationViewRef = React.createRef<SpacePreAllocationView>();
private selectedExitLocationRef = React.createRef<React.ReactInstance>();
+ private selectedEntryLocationRef = React.createRef<React.ReactInstance>();
private selectedBridgeLocationRef = React.createRef<React.ReactInstance>();
private exitLocationList = React.createRef<LocationList<never>>();
+ private entryLocationList = React.createRef<LocationList<never>>();
private bridgeLocationList = React.createRef<LocationList<SpecialBridgeLocationType>>();
- private snapshotByScope: { [index: number]: ISelectLocationSnapshot } = {};
+ private snapshotByScope: Partial<Record<LocationScope, ISelectLocationSnapshot>> = {};
private filterButtonRef = React.createRef<HTMLDivElement>();
private headerRef = React.createRef<HTMLHeadingElement>();
@@ -87,25 +95,25 @@ export default class SelectLocation extends React.Component<IProps, IState> {
}
public componentDidUpdate(
- prevProps: IProps,
- _prevState: unknown,
+ _prevProps: IProps,
+ prevState: IState,
snapshot?: ISelectLocationSnapshot,
) {
- if (this.props.locationScope !== prevProps.locationScope) {
- this.restoreScrollPosition(this.props.locationScope);
+ if (this.state.locationScope !== prevState.locationScope) {
+ this.restoreScrollPosition(this.state.locationScope);
if (snapshot) {
- this.snapshotByScope[prevProps.locationScope] = snapshot;
+ this.snapshotByScope[prevState.locationScope] = snapshot;
}
}
}
- public getSnapshotBeforeUpdate(prevProps: IProps): ISelectLocationSnapshot | undefined {
+ public getSnapshotBeforeUpdate(
+ prevProps: IProps,
+ prevState: IState,
+ ): ISelectLocationSnapshot | undefined {
const scrollView = this.scrollView.current;
- const locationList =
- prevProps.locationScope === LocationScope.relay
- ? this.exitLocationList.current
- : this.bridgeLocationList.current;
+ const locationList = this.getLocationListRef(prevProps, prevState);
if (scrollView && locationList) {
return {
@@ -164,13 +172,7 @@ export default class SelectLocation extends React.Component<IProps, IState> {
messages.pgettext('select-location-view', 'Select location')
}
</HeaderTitle>
- <HeaderSubTitle>
- {this.props.allowBridgeSelection &&
- 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).',
- )}
- </HeaderSubTitle>
+ {this.renderHeaderSubtitle()}
</StyledSettingsHeader>
{this.props.providers.length > 0 && (
@@ -200,10 +202,10 @@ export default class SelectLocation extends React.Component<IProps, IState> {
</StyledProvidersCount>
</StyledProviderCountRow>
)}
- {this.props.allowBridgeSelection && (
+ {this.props.allowEntrySelection && (
<StyledScopeBar
- defaultSelectedIndex={this.props.locationScope}
- onChange={this.props.onChangeLocationScope}>
+ defaultSelectedIndex={this.state.locationScope}
+ onChange={this.onChangeLocationScope}>
<ScopeBarItem>
{messages.pgettext('select-location-nav', 'Entry')}
</ScopeBarItem>
@@ -214,31 +216,7 @@ export default class SelectLocation extends React.Component<IProps, IState> {
)}
</StyledNavigationBarAttachment>
- <StyledContent>
- {this.props.locationScope === LocationScope.relay ? (
- <ExitLocations
- ref={this.exitLocationList}
- source={this.props.relayLocations}
- defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()}
- selectedValue={this.props.selectedExitLocation}
- selectedElementRef={this.selectedExitLocationRef}
- onSelect={this.onSelectExitLocation}
- onWillExpand={this.onWillExpand}
- onTransitionEnd={this.resetHeight}
- />
- ) : (
- <BridgeLocations
- ref={this.bridgeLocationList}
- source={this.props.bridgeLocations}
- defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()}
- selectedValue={this.props.selectedBridgeLocation}
- selectedElementRef={this.selectedBridgeLocationRef}
- onSelect={this.onSelectBridgeLocation}
- onWillExpand={this.onWillExpand}
- onTransitionEnd={this.resetHeight}
- />
- )}
- </StyledContent>
+ <StyledContent>{this.renderLocationList()}</StyledContent>
</SpacePreAllocationView>
</NavigationScrollbars>
</NavigationContainer>
@@ -257,12 +235,101 @@ export default class SelectLocation extends React.Component<IProps, IState> {
}
}
+ private getLocationListRef(prevProps: IProps, prevState: IState) {
+ if (prevState.locationScope === LocationScope.exit) {
+ return this.exitLocationList.current;
+ } else if (prevProps.tunnelProtocol === 'wireguard') {
+ return this.entryLocationList.current;
+ } else {
+ return this.bridgeLocationList.current;
+ }
+ }
+
+ private getSelectedLocationRef() {
+ if (this.state.locationScope === LocationScope.exit) {
+ return this.selectedExitLocationRef.current;
+ } else if (this.props.tunnelProtocol === 'wireguard') {
+ return this.selectedEntryLocationRef.current;
+ } else {
+ return this.selectedBridgeLocationRef.current;
+ }
+ }
+
+ private renderHeaderSubtitle() {
+ if (this.props.allowEntrySelection) {
+ if (this.props.tunnelProtocol === 'openvpn') {
+ return (
+ <HeaderSubTitle>
+ {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).',
+ )}
+ </HeaderSubTitle>
+ );
+ } else {
+ return (
+ <HeaderSubTitle>
+ {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).',
+ )}
+ </HeaderSubTitle>
+ );
+ }
+ } else {
+ return null;
+ }
+ }
+
+ private renderLocationList() {
+ if (this.state.locationScope === LocationScope.exit) {
+ return (
+ <ExitLocations
+ ref={this.exitLocationList}
+ source={this.props.relayLocations}
+ defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()}
+ selectedValue={this.props.selectedExitLocation}
+ selectedElementRef={this.selectedExitLocationRef}
+ onSelect={this.onSelectExitLocation}
+ onWillExpand={this.onWillExpand}
+ onTransitionEnd={this.resetHeight}
+ />
+ );
+ } else if (this.props.tunnelProtocol === 'any' || this.props.tunnelProtocol === 'wireguard') {
+ return (
+ <EntryLocations
+ ref={this.entryLocationList}
+ source={this.props.relayLocations}
+ defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()}
+ selectedValue={this.props.selectedEntryLocation}
+ selectedElementRef={this.selectedEntryLocationRef}
+ onSelect={this.onSelectEntryLocation}
+ onWillExpand={this.onWillExpand}
+ onTransitionEnd={this.resetHeight}
+ />
+ );
+ } else {
+ return (
+ <BridgeLocations
+ ref={this.bridgeLocationList}
+ source={this.props.bridgeLocations}
+ defaultExpandedLocations={this.getExpandedLocationsFromSnapshot()}
+ selectedValue={this.props.selectedBridgeLocation}
+ selectedElementRef={this.selectedBridgeLocationRef}
+ onSelect={this.onSelectBridgeLocation}
+ onWillExpand={this.onWillExpand}
+ onTransitionEnd={this.resetHeight}
+ />
+ );
+ }
+ }
+
private resetHeight = () => {
this.spacePreAllocationViewRef.current?.reset();
};
private getExpandedLocationsFromSnapshot(): RelayLocation[] | undefined {
- const snapshot = this.snapshotByScope[this.props.locationScope];
+ const snapshot = this.snapshotByScope[this.state.locationScope];
if (snapshot) {
return snapshot.expandedLocations;
} else {
@@ -278,10 +345,7 @@ export default class SelectLocation extends React.Component<IProps, IState> {
}
private scrollToSelectedCell() {
- const ref =
- this.props.locationScope === LocationScope.relay
- ? this.selectedExitLocationRef.current
- : this.selectedBridgeLocationRef.current;
+ const ref = this.getSelectedLocationRef();
const scrollView = this.scrollView.current;
if (scrollView) {
@@ -295,12 +359,20 @@ export default class SelectLocation extends React.Component<IProps, IState> {
}
}
+ private onChangeLocationScope = (locationScope: LocationScope) => {
+ this.setState({ locationScope });
+ };
+
private onSelectExitLocation = (location: LocationSelection<never>) => {
if (location.type === LocationSelectionType.relay) {
this.props.onSelectExitLocation(location.value);
}
};
+ private onSelectEntryLocation = (location: LocationSelection<never>) => {
+ this.props.onSelectEntryLocation(location.value);
+ };
+
private onSelectBridgeLocation = (location: LocationSelection<SpecialBridgeLocationType>) => {
if (location.type === LocationSelectionType.relay) {
this.props.onSelectBridgeLocation(location.value);
diff --git a/gui/src/renderer/containers/SelectLocationPage.tsx b/gui/src/renderer/containers/SelectLocationPage.tsx
index cd455a01f6..a4f78f87a5 100644
--- a/gui/src/renderer/containers/SelectLocationPage.tsx
+++ b/gui/src/renderer/containers/SelectLocationPage.tsx
@@ -1,21 +1,21 @@
import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
import BridgeSettingsBuilder from '../../shared/bridge-settings-builder';
import { LiftedConstraint, RelayLocation } from '../../shared/daemon-rpc-types';
import log from '../../shared/logging';
import RelaySettingsBuilder from '../../shared/relay-settings-builder';
import SelectLocation from '../components/SelectLocation';
import withAppContext, { IAppContext } from '../context';
+import { createWireguardRelayUpdater } from '../lib/constraint-updater';
import { IHistoryProps, withHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { IRelayLocationRedux } from '../redux/settings/reducers';
import { IReduxState, ReduxDispatch } from '../redux/store';
-import userInterfaceActions from '../redux/userinterface/actions';
-import { LocationScope } from '../redux/userinterface/reducers';
-const mapStateToProps = (state: IReduxState) => {
+const mapStateToProps = (state: IReduxState, props: IHistoryProps & IAppContext) => {
let selectedExitLocation: RelayLocation | undefined;
+ let selectedEntryLocation: RelayLocation | undefined;
let selectedBridgeLocation: LiftedConstraint<RelayLocation> | undefined;
+ let multihopEnabled = false;
if ('normal' in state.settings.relaySettings) {
const exitLocation = state.settings.relaySettings.normal.location;
@@ -24,37 +24,57 @@ const mapStateToProps = (state: IReduxState) => {
}
}
- if ('normal' in state.settings.bridgeSettings) {
+ const relaySettings = state.settings.relaySettings;
+ const tunnelProtocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : 'any';
+
+ if (tunnelProtocol === 'openvpn' && 'normal' in state.settings.bridgeSettings) {
selectedBridgeLocation = state.settings.bridgeSettings.normal.location;
+ } else if ('normal' in relaySettings) {
+ const entryLocation = relaySettings.normal.wireguard.entryLocation;
+ if (entryLocation !== 'any') {
+ selectedEntryLocation = entryLocation;
+ }
+
+ multihopEnabled = relaySettings.normal.wireguard.useMultihop;
}
- const allowBridgeSelection = state.settings.bridgeState === 'on';
- const locationScope = allowBridgeSelection
- ? state.userInterface.locationScope
- : LocationScope.relay;
+ const allowEntrySelection =
+ (tunnelProtocol === 'openvpn' && state.settings.bridgeState === 'on') ||
+ ((tunnelProtocol === 'any' || tunnelProtocol === 'wireguard') && multihopEnabled);
- const relaySettings = state.settings.relaySettings;
const providers = 'normal' in relaySettings ? relaySettings.normal.providers : [];
return {
selectedExitLocation,
+ selectedEntryLocation,
selectedBridgeLocation,
relayLocations: filterLocationsByProvider(state.settings.relayLocations, providers),
bridgeLocations: filterLocationsByProvider(state.settings.bridgeLocations, providers),
- locationScope,
- allowBridgeSelection,
+ allowEntrySelection,
+ tunnelProtocol,
providers,
+
+ onSelectEntryLocation: async (entryLocation: RelayLocation) => {
+ // dismiss the view first
+ props.history.dismiss();
+
+ const relayUpdate = createWireguardRelayUpdater(state.settings.relaySettings)
+ .tunnel.wireguard((wireguard) => wireguard.entryLocation.exact(entryLocation))
+ .build();
+
+ try {
+ await props.app.updateRelaySettings(relayUpdate);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to select the entry location', error.message);
+ }
+ },
};
};
-const mapDispatchToProps = (dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
- const userInterface = bindActionCreators(userInterfaceActions, dispatch);
-
+const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
return {
onClose: () => props.history.dismiss(),
onViewFilterByProvider: () => props.history.push(RoutePath.filterByProvider),
- onChangeLocationScope: (scope: LocationScope) => {
- userInterface.setLocationScope(scope);
- },
onSelectExitLocation: async (relayLocation: RelayLocation) => {
// dismiss the view first
props.history.dismiss();
diff --git a/gui/src/renderer/redux/userinterface/actions.ts b/gui/src/renderer/redux/userinterface/actions.ts
index 88ffbf216f..e6010d42f2 100644
--- a/gui/src/renderer/redux/userinterface/actions.ts
+++ b/gui/src/renderer/redux/userinterface/actions.ts
@@ -1,5 +1,4 @@
import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema';
-import { LocationScope } from './reducers';
export interface IUpdateLocaleAction {
type: 'UPDATE_LOCALE';
@@ -15,11 +14,6 @@ export interface IUpdateConnectionInfoOpenAction {
type: 'TOGGLE_CONNECTION_PANEL';
}
-export interface ISetLocationScopeAction {
- type: 'SET_LOCATION_SCOPE';
- scope: LocationScope;
-}
-
export interface ISetWindowFocusedAction {
type: 'SET_WINDOW_FOCUSED';
focused: boolean;
@@ -50,7 +44,6 @@ export type UserInterfaceAction =
| IUpdateLocaleAction
| IUpdateWindowArrowPositionAction
| IUpdateConnectionInfoOpenAction
- | ISetLocationScopeAction
| ISetWindowFocusedAction
| IAddScrollPosition
| IRemoveScrollPosition
@@ -77,13 +70,6 @@ function toggleConnectionPanel(): IUpdateConnectionInfoOpenAction {
};
}
-function setLocationScope(scope: LocationScope): ISetLocationScopeAction {
- return {
- type: 'SET_LOCATION_SCOPE',
- scope,
- };
-}
-
function setWindowFocused(focused: boolean): ISetWindowFocusedAction {
return {
type: 'SET_WINDOW_FOCUSED',
@@ -126,7 +112,6 @@ export default {
updateLocale,
updateWindowArrowPosition,
toggleConnectionPanel,
- setLocationScope,
setWindowFocused,
addScrollPosition,
removeScrollPosition,
diff --git a/gui/src/renderer/redux/userinterface/reducers.ts b/gui/src/renderer/redux/userinterface/reducers.ts
index 5adac07937..9247dc8a5f 100644
--- a/gui/src/renderer/redux/userinterface/reducers.ts
+++ b/gui/src/renderer/redux/userinterface/reducers.ts
@@ -1,16 +1,10 @@
import { MacOsScrollbarVisibility } from '../../../shared/ipc-schema';
import { ReduxAction } from '../store';
-export enum LocationScope {
- bridge = 0,
- relay,
-}
-
export interface IUserInterfaceReduxState {
locale: string;
arrowPosition?: number;
connectionPanelVisible: boolean;
- locationScope: LocationScope;
windowFocused: boolean;
scrollPosition: Record<string, [number, number]>;
macOsScrollbarVisibility?: MacOsScrollbarVisibility;
@@ -20,7 +14,6 @@ export interface IUserInterfaceReduxState {
const initialState: IUserInterfaceReduxState = {
locale: 'en',
connectionPanelVisible: false,
- locationScope: LocationScope.relay,
windowFocused: false,
scrollPosition: {},
macOsScrollbarVisibility: undefined,
@@ -41,9 +34,6 @@ export default function (
case 'TOGGLE_CONNECTION_PANEL':
return { ...state, connectionPanelVisible: !state.connectionPanelVisible };
- case 'SET_LOCATION_SCOPE':
- return { ...state, locationScope: action.scope };
-
case 'SET_WINDOW_FOCUSED':
return { ...state, windowFocused: action.focused };