summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-09-27 11:37:32 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-09-27 11:37:32 +0200
commit8c4d03d4f31069ba9648ffb22c056185d9706167 (patch)
tree5d61a310d8f70cbb4ccb5708395cc0e254ff81a9 /gui/src
parentde9bf32918295037d1616d256afedf52ab7d4b6d (diff)
parentdcdd4b44efb5f77148d262f4c573dd6aca0a5e03 (diff)
downloadmullvadvpn-8c4d03d4f31069ba9648ffb22c056185d9706167.tar.xz
mullvadvpn-8c4d03d4f31069ba9648ffb22c056185d9706167.zip
Merge branch 'add-custom-port-option'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/daemon-rpc.ts29
-rw-r--r--gui/src/main/relay-list.ts36
-rw-r--r--gui/src/renderer/app.tsx3
-rw-r--r--gui/src/renderer/components/Filter.tsx12
-rw-r--r--gui/src/renderer/components/OpenVpnSettings.tsx74
-rw-r--r--gui/src/renderer/components/SelectLanguage.tsx6
-rw-r--r--gui/src/renderer/components/VpnSettings.tsx17
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx120
-rw-r--r--gui/src/renderer/components/cell/Input.tsx183
-rw-r--r--gui/src/renderer/components/cell/Selector.tsx225
-rw-r--r--gui/src/renderer/redux/settings/actions.ts23
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts9
-rw-r--r--gui/src/shared/daemon-rpc-types.ts12
-rw-r--r--gui/src/shared/ipc-schema.ts2
-rw-r--r--gui/src/shared/string-helpers.ts4
15 files changed, 488 insertions, 267 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts
index 950cf1a4a3..a6136bc6f3 100644
--- a/gui/src/main/daemon-rpc.ts
+++ b/gui/src/main/daemon-rpc.ts
@@ -31,14 +31,15 @@ import {
IObfuscationEndpoint,
IOpenVpnConstraints,
IProxyEndpoint,
- IRelayList,
IRelayListCity,
IRelayListCountry,
IRelayListHostname,
+ IRelayListWithEndpointData,
ISettings,
ITunnelOptions,
ITunnelStateRelayInfo,
IWireguardConstraints,
+ IWireguardEndpointData,
LoggedInDeviceState,
LoggedOutDeviceState,
ObfuscationSettings,
@@ -258,7 +259,7 @@ export class DaemonRpc {
}
}
- public async getRelayLocations(): Promise<IRelayList> {
+ public async getRelayLocations(): Promise<IRelayListWithEndpointData> {
if (this.isConnected) {
const response = await this.callEmpty<grpcTypes.RelayList>(this.client.getRelayLocations);
return convertFromRelayList(response);
@@ -757,13 +758,25 @@ function liftConstraint<T>(constraint: Constraint<T> | undefined): T | undefined
return undefined;
}
-function convertFromRelayList(relayList: grpcTypes.RelayList): IRelayList {
+function convertFromRelayList(relayList: grpcTypes.RelayList): IRelayListWithEndpointData {
return {
- countries: relayList
- .getCountriesList()
- .map((country: grpcTypes.RelayListCountry) =>
- convertFromRelayListCountry(country.toObject()),
- ),
+ relayList: {
+ countries: relayList
+ .getCountriesList()
+ .map((country: grpcTypes.RelayListCountry) =>
+ convertFromRelayListCountry(country.toObject()),
+ ),
+ },
+ wireguardEndpointData: convertWireguardEndpointData(relayList.getWireguard()!),
+ };
+}
+
+function convertWireguardEndpointData(
+ data: grpcTypes.WireguardEndpointData,
+): IWireguardEndpointData {
+ return {
+ portRanges: data.getPortRangesList().map((range) => [range.getFirst(), range.getLast()]),
+ udp2tcpPorts: data.getUdp2tcpPortsList(),
};
}
diff --git a/gui/src/main/relay-list.ts b/gui/src/main/relay-list.ts
index b43548e6a1..c7b4a1fff1 100644
--- a/gui/src/main/relay-list.ts
+++ b/gui/src/main/relay-list.ts
@@ -1,12 +1,26 @@
-import { BridgeState, IRelayList, liftConstraint, RelaySettings } from '../shared/daemon-rpc-types';
+import {
+ BridgeState,
+ IRelayList,
+ IRelayListWithEndpointData,
+ liftConstraint,
+ RelaySettings,
+} from '../shared/daemon-rpc-types';
import { IRelayListPair } from '../shared/ipc-schema';
import { IpcMainEventChannel } from './ipc-event-channel';
export default class RelayList {
- private relays: IRelayList = { countries: [] };
+ private relays: IRelayListWithEndpointData = {
+ relayList: {
+ countries: [],
+ },
+ wireguardEndpointData: {
+ portRanges: [],
+ udp2tcpPorts: [],
+ },
+ };
public setRelays(
- newRelayList: IRelayList,
+ newRelayList: IRelayListWithEndpointData,
relaySettings: RelaySettings,
bridgeState: BridgeState,
) {
@@ -25,14 +39,18 @@ export default class RelayList {
}
private processRelays(
- relayList: IRelayList,
+ relayList: IRelayListWithEndpointData,
relaySettings: RelaySettings,
bridgeState: BridgeState,
): IRelayListPair {
- const filteredRelays = this.processRelaysForPresentation(relayList, relaySettings);
- const filteredBridges = this.processBridgesForPresentation(relayList, bridgeState);
+ const filteredRelays = this.processRelaysForPresentation(relayList.relayList, relaySettings);
+ const filteredBridges = this.processBridgesForPresentation(relayList.relayList, bridgeState);
- return { relays: filteredRelays, bridges: filteredBridges };
+ return {
+ relays: filteredRelays,
+ bridges: filteredBridges,
+ wireguardEndpointData: relayList.wireguardEndpointData,
+ };
}
private processRelaysForPresentation(
@@ -75,9 +93,7 @@ export default class RelayList {
}))
.filter((country) => country.cities.length > 0);
- return {
- countries: filteredCountries,
- };
+ return { countries: filteredCountries };
}
private processBridgesForPresentation(
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 1c222c9299..6bc2c61ff3 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -849,6 +849,9 @@ export default class AppRenderer {
this.reduxActions.settings.updateRelayLocations(relays);
this.reduxActions.settings.updateBridgeLocations(bridges);
+ this.reduxActions.settings.updateWireguardEndpointData(
+ this.relayListPair.wireguardEndpointData,
+ );
}
private setCurrentVersion(versionInfo: ICurrentAppVersionInfo) {
diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx
index 79253ed7b0..5c2e7e0f55 100644
--- a/gui/src/renderer/components/Filter.tsx
+++ b/gui/src/renderer/components/Filter.tsx
@@ -191,10 +191,6 @@ function FilterByOwnership(props: IFilterByOwnershipProps) {
() =>
[
{
- label: messages.gettext('Any'),
- value: Ownership.any,
- },
- {
label: messages.pgettext('filter-view', 'Mullvad owned only'),
value: Ownership.mullvadOwned,
},
@@ -220,7 +216,13 @@ function FilterByOwnership(props: IFilterByOwnershipProps) {
</Cell.CellButton>
<Accordion expanded={expanded}>
- <StyledSelector values={values} value={props.ownership} onSelect={props.setOwnership} />
+ <StyledSelector
+ items={values}
+ value={props.ownership}
+ onSelect={props.setOwnership}
+ automaticLabel={messages.gettext('Any')}
+ automaticValue={Ownership.any}
+ />
</Accordion>
</AriaInputGroup>
);
diff --git a/gui/src/renderer/components/OpenVpnSettings.tsx b/gui/src/renderer/components/OpenVpnSettings.tsx
index fce949c6a9..af4404fd1a 100644
--- a/gui/src/renderer/components/OpenVpnSettings.tsx
+++ b/gui/src/renderer/components/OpenVpnSettings.tsx
@@ -7,6 +7,7 @@ import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-
import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
import RelaySettingsBuilder from '../../shared/relay-settings-builder';
+import { removeNonNumericCharacters } from '../../shared/string-helpers';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { useBoolean } from '../lib/utilityHooks';
@@ -15,7 +16,7 @@ import { useSelector } from '../redux/store';
import * as AppButton from './AppButton';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
-import Selector, { ISelectorItem } from './cell/Selector';
+import Selector, { SelectorItem } from './cell/Selector';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
@@ -33,17 +34,13 @@ const MAX_MSSFIX_VALUE = 1450;
const UDP_PORTS = [1194, 1195, 1196, 1197, 1300, 1301, 1302];
const TCP_PORTS = [80, 443];
-type OptionalPort = number | undefined;
-
-type OptionalRelayProtocol = RelayProtocol | undefined;
-
export enum BridgeModeAvailability {
available,
blockedDueToTunnelProtocol,
blockedDueToTransportProtocol,
}
-function mapPortToSelectorItem(value: number): ISelectorItem<number> {
+function mapPortToSelectorItem(value: number): SelectorItem<number> {
return { label: value.toString(), value };
}
@@ -127,19 +124,15 @@ function TransportProtocolSelector() {
const bridgeState = useSelector((state) => state.settings.bridgeState);
const protocol = useMemo(() => {
- const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
- return protocol === 'any' ? undefined : protocol;
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : 'any';
+ return protocol === 'any' ? null : protocol;
}, [relaySettings]);
const protocolAndPortUpdater = useProtocolAndPortUpdater();
- const items: ISelectorItem<OptionalRelayProtocol>[] = useMemo(
+ const items: SelectorItem<RelayProtocol>[] = useMemo(
() => [
{
- label: messages.gettext('Automatic'),
- value: undefined,
- },
- {
label: messages.gettext('TCP'),
value: 'tcp',
},
@@ -157,9 +150,10 @@ function TransportProtocolSelector() {
<AriaInputGroup>
<Selector
title={messages.pgettext('openvpn-settings-view', 'Transport protocol')}
- values={items}
+ items={items}
value={protocol}
onSelect={protocolAndPortUpdater}
+ automaticValue={null}
/>
{bridgeState === 'on' && (
<Cell.Footer>
@@ -186,7 +180,7 @@ function useProtocolAndPortUpdater() {
const { updateRelaySettings } = useAppContext();
const updater = useCallback(
- async (protocol?: RelayProtocol, port?: number) => {
+ async (protocol: RelayProtocol | null, port?: number | null) => {
const relayUpdate = RelaySettingsBuilder.normal()
.tunnel.openvpn((openvpn) => {
if (protocol) {
@@ -220,35 +214,30 @@ function PortSelector() {
const relaySettings = useSelector((state) => state.settings.relaySettings);
const protocol = useMemo(() => {
- const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
- return protocol === 'any' ? undefined : protocol;
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : 'any';
+ return protocol === 'any' ? null : protocol;
}, [relaySettings]);
const port = useMemo(() => {
- const port = 'normal' in relaySettings ? relaySettings.normal.openvpn.port : undefined;
- return port === 'any' ? undefined : port;
+ const port = 'normal' in relaySettings ? relaySettings.normal.openvpn.port : 'any';
+ return port === 'any' ? null : port;
}, [relaySettings]);
const protocolAndPortUpdater = useProtocolAndPortUpdater();
const onSelect = useCallback(
- async (port?: number) => {
+ async (port: number | null) => {
await protocolAndPortUpdater(protocol, port);
},
[protocolAndPortUpdater, protocol],
);
- const automaticPort: ISelectorItem<OptionalPort> = {
- label: messages.gettext('Automatic'),
- value: undefined,
- };
-
const portItems = {
- udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)),
- tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)),
+ udp: UDP_PORTS.map(mapPortToSelectorItem),
+ tcp: TCP_PORTS.map(mapPortToSelectorItem),
};
- if (protocol === undefined) {
+ if (protocol === null) {
return null;
}
@@ -265,9 +254,10 @@ function PortSelector() {
portType: protocol.toUpperCase(),
},
)}
- values={portItems[protocol]}
+ items={portItems[protocol]}
value={port}
onSelect={onSelect}
+ automaticValue={null}
/>
</AriaInputGroup>
</StyledSelectorContainer>
@@ -281,22 +271,18 @@ function BridgeModeSelector() {
const bridgeState = useSelector((state) => state.settings.bridgeState);
const tunnelProtocol = useMemo(() => {
- const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : undefined;
- return protocol === 'any' ? undefined : protocol;
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : 'any';
+ return protocol === 'any' ? null : protocol;
}, [relaySettings]);
const transportProtocol = useMemo(() => {
- const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
- return protocol === 'any' ? undefined : protocol;
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : 'any';
+ return protocol === 'any' ? null : protocol;
}, [relaySettings]);
- const options: ISelectorItem<BridgeState>[] = useMemo(
+ const options: SelectorItem<BridgeState>[] = useMemo(
() => [
{
- label: messages.gettext('Automatic'),
- value: 'auto',
- },
- {
label: messages.gettext('On'),
value: 'on',
disabled: tunnelProtocol !== 'openvpn' || transportProtocol === 'udp',
@@ -348,9 +334,10 @@ function BridgeModeSelector() {
// TRANSLATORS: The title for the shadowsocks bridge selector section.
messages.pgettext('openvpn-settings-view', 'Bridge mode')
}
- values={options}
+ items={options}
value={bridgeState}
onSelect={onSelectBridgeState}
+ automaticValue={'auto' as const}
/>
</StyledSelectorContainer>
<Cell.Footer>
@@ -379,7 +366,10 @@ function BridgeModeSelector() {
);
}
-function bridgeModeFooterText(tunnelProtocol?: TunnelProtocol, transportProtocol?: RelayProtocol) {
+function bridgeModeFooterText(
+ tunnelProtocol: TunnelProtocol | null,
+ transportProtocol: RelayProtocol | null,
+) {
if (tunnelProtocol !== 'openvpn') {
return formatMarkdown(
sprintf(
@@ -433,10 +423,6 @@ function bridgeModeFooterText(tunnelProtocol?: TunnelProtocol, transportProtocol
}
}
-function removeNonNumericCharacters(value: string) {
- return value.replace(/[^0-9]/g, '');
-}
-
function mssfixIsValid(mssfix: string): boolean {
const parsedMssFix = mssfix ? parseInt(mssfix) : undefined;
return (
diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx
index 419557f700..2b11f0a924 100644
--- a/gui/src/renderer/components/SelectLanguage.tsx
+++ b/gui/src/renderer/components/SelectLanguage.tsx
@@ -3,7 +3,7 @@ import styled from 'styled-components';
import { messages } from '../../shared/gettext';
import { AriaInputGroup } from './AriaGroup';
-import Selector, { ISelectorItem } from './cell/Selector';
+import Selector, { SelectorItem } from './cell/Selector';
import { CustomScrollbarsRef } from './CustomScrollbars';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
@@ -24,7 +24,7 @@ interface IProps {
}
interface IState {
- source: Array<ISelectorItem<string>>;
+ source: Array<SelectorItem<string>>;
}
const StyledNavigationScrollbars = styled(NavigationScrollbars)({
@@ -79,7 +79,7 @@ export default class SelectLanguage extends React.Component<IProps, IState> {
<AriaInputGroup>
<StyledSelector
title=""
- values={this.state.source}
+ items={this.state.source}
value={this.props.preferredLocale}
onSelect={this.props.setPreferredLocale}
selectedCellRef={this.selectedCellRef}
diff --git a/gui/src/renderer/components/VpnSettings.tsx b/gui/src/renderer/components/VpnSettings.tsx
index 47b48e4fcd..76d8a782b6 100644
--- a/gui/src/renderer/components/VpnSettings.tsx
+++ b/gui/src/renderer/components/VpnSettings.tsx
@@ -17,7 +17,7 @@ import { useSelector } from '../redux/store';
import * as AppButton from './AppButton';
import { AriaDescription, AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
-import Selector, { ISelectorItem } from './cell/Selector';
+import Selector, { SelectorItem } from './cell/Selector';
import CustomDnsSettings from './CustomDnsSettings';
import InfoButton, { InfoIcon } from './InfoButton';
import { BackAction } from './KeyboardNavigation';
@@ -612,10 +612,10 @@ function TunnelProtocolSetting() {
);
const { updateRelaySettings } = useAppContext();
- const setTunnelProtocol = useCallback(async (tunnelProtocol: TunnelProtocol | undefined) => {
+ const setTunnelProtocol = useCallback(async (tunnelProtocol: TunnelProtocol | null) => {
const relayUpdate = RelaySettingsBuilder.normal()
.tunnel.tunnelProtocol((config) => {
- if (tunnelProtocol) {
+ if (tunnelProtocol !== null) {
config.tunnelProtocol.exact(tunnelProtocol);
} else {
config.tunnelProtocol.any();
@@ -630,13 +630,9 @@ function TunnelProtocolSetting() {
}
}, []);
- const tunnelProtocolItems: Array<ISelectorItem<TunnelProtocol | undefined>> = useMemo(
+ const tunnelProtocolItems: Array<SelectorItem<TunnelProtocol>> = useMemo(
() => [
{
- label: messages.gettext('Automatic'),
- value: undefined,
- },
- {
label: strings.wireguard,
value: 'wireguard',
},
@@ -653,9 +649,10 @@ function TunnelProtocolSetting() {
<StyledSelectorContainer>
<Selector
title={messages.pgettext('vpn-settings-view', 'Tunnel protocol')}
- values={tunnelProtocolItems}
- value={tunnelProtocol}
+ items={tunnelProtocolItems}
+ value={tunnelProtocol ?? null}
onSelect={setTunnelProtocol}
+ automaticValue={null}
/>
</StyledSelectorContainer>
</AriaInputGroup>
diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx
index 222e845001..9a422cc18e 100644
--- a/gui/src/renderer/components/WireguardSettings.tsx
+++ b/gui/src/renderer/components/WireguardSettings.tsx
@@ -11,6 +11,7 @@ import {
} from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import log from '../../shared/logging';
+import { removeNonNumericCharacters } from '../../shared/string-helpers';
import { useAppContext } from '../context';
import { createWireguardRelayUpdater } from '../lib/constraint-updater';
import { useHistory } from '../lib/history';
@@ -19,7 +20,7 @@ import { useSelector } from '../redux/store';
import * as AppButton from './AppButton';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
-import Selector, { ISelectorItem } from './cell/Selector';
+import Selector, { SelectorItem, SelectorWithCustomItem } from './cell/Selector';
import { InfoIcon } from './InfoButton';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
@@ -38,10 +39,7 @@ const MAX_WIREGUARD_MTU_VALUE = 1420;
const WIREUGARD_UDP_PORTS = [51820, 53];
const UDP2TCP_PORTS = [80, 443, 5001];
-type OptionalPort = number | undefined;
-type OptionalIpVersion = IpVersion | undefined;
-
-function mapPortToSelectorItem(value: number): ISelectorItem<number> {
+function mapPortToSelectorItem(value: number): SelectorItem<number> {
return { label: value.toString(), value };
}
@@ -131,26 +129,23 @@ export default function WireguardSettings() {
function PortSelector() {
const relaySettings = useSelector((state) => state.settings.relaySettings);
const { updateRelaySettings } = useAppContext();
+ const allowedPortRanges = useSelector((state) => state.settings.wireguardEndpointData.portRanges);
- const wireguardPortItems = useMemo(() => {
- const automaticPort: ISelectorItem<OptionalPort> = {
- label: messages.gettext('Automatic'),
- value: undefined,
- };
-
- return [automaticPort].concat(WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem));
- }, []);
+ const wireguardPortItems = useMemo<Array<SelectorItem<number>>>(
+ () => WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem),
+ [],
+ );
const port = useMemo(() => {
- const port = 'normal' in relaySettings ? relaySettings.normal.wireguard.port : undefined;
- return port === 'any' ? undefined : port;
+ const port = 'normal' in relaySettings ? relaySettings.normal.wireguard.port : 'any';
+ return port === 'any' ? null : port;
}, [relaySettings]);
const setWireguardPort = useCallback(
- async (port?: number) => {
+ async (port: number | null) => {
const relayUpdate = createWireguardRelayUpdater(relaySettings)
.tunnel.wireguard((wireguard) => {
- if (port) {
+ if (port !== null) {
wireguard.port.exact(port);
} else {
wireguard.port.any();
@@ -167,15 +162,56 @@ function PortSelector() {
[relaySettings],
);
+ const setCustomPort = useCallback(
+ async (port: string) => {
+ await setWireguardPort(parseInt(port));
+ },
+ [setWireguardPort],
+ );
+
+ const validateValue = useCallback(
+ (value) => allowedPortRanges.some(([start, end]) => value >= start && value <= end),
+ [allowedPortRanges],
+ );
+
+ const portRangesText = allowedPortRanges
+ .map(([start, end]) => (start === end ? start : `${start}-${end}`))
+ .join(', ');
+
return (
<AriaInputGroup>
<StyledSelectorContainer>
- <Selector
+ <SelectorWithCustomItem
// TRANSLATORS: The title for the WireGuard port selector.
title={messages.pgettext('wireguard-settings-view', 'Port')}
- values={wireguardPortItems}
+ items={wireguardPortItems}
value={port}
onSelect={setWireguardPort}
+ onSelectCustom={setCustomPort}
+ inputPlaceholder={messages.pgettext('wireguard-settings-view', 'Port')}
+ automaticValue={null}
+ modifyValue={removeNonNumericCharacters}
+ validateValue={validateValue}
+ maxLength={5}
+ details={
+ <>
+ <ModalMessage>
+ {messages.pgettext(
+ 'wireguard-settings-view',
+ 'The automatic setting will randomly choose from the valid port ranges shown below.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {sprintf(
+ messages.pgettext(
+ 'wireguard-settings-view',
+ 'The custom port can be any value inside the valid ranges: %(portRanges)s.',
+ ),
+ { portRanges: portRangesText },
+ )}
+ </ModalMessage>
+ </>
+ }
/>
</StyledSelectorContainer>
<Cell.Footer>
@@ -200,13 +236,9 @@ function ObfuscationSettings() {
const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings);
const obfuscationType = obfuscationSettings.selectedObfuscation;
- const obfuscationTypeItems: ISelectorItem<ObfuscationType>[] = useMemo(
+ const obfuscationTypeItems: SelectorItem<ObfuscationType>[] = useMemo(
() => [
{
- label: messages.gettext('Automatic'),
- value: ObfuscationType.auto,
- },
- {
label: messages.pgettext('wireguard-settings-view', 'On (UDP-over-TCP)'),
value: ObfuscationType.udp2tcp,
},
@@ -242,9 +274,10 @@ function ObfuscationSettings() {
)}
</ModalMessage>
}
- values={obfuscationTypeItems}
+ items={obfuscationTypeItems}
value={obfuscationType}
onSelect={selectObfuscationType}
+ automaticValue={ObfuscationType.auto}
/>
</StyledSelectorContainer>
</AriaInputGroup>
@@ -256,14 +289,10 @@ function Udp2tcpPortSetting() {
const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings);
const port = liftConstraint(obfuscationSettings.udp2tcpSettings.port);
- const portItems: ISelectorItem<LiftedConstraint<number>>[] = useMemo(() => {
- const automaticItem: ISelectorItem<LiftedConstraint<number>> = {
- label: messages.gettext('Automatic'),
- value: 'any',
- };
-
- return [automaticItem].concat(UDP2TCP_PORTS.map(mapPortToSelectorItem));
- }, []);
+ const portItems: SelectorItem<number>[] = useMemo(
+ () => UDP2TCP_PORTS.map(mapPortToSelectorItem),
+ [],
+ );
const selectPort = useCallback(
async (port: LiftedConstraint<number>) => {
@@ -292,12 +321,13 @@ function Udp2tcpPortSetting() {
)}
</ModalMessage>
}
- values={portItems}
+ items={portItems}
value={port}
onSelect={selectPort}
disabled={obfuscationSettings.selectedObfuscation === ObfuscationType.off}
expandable
thinTitle
+ automaticValue={'any' as const}
/>
</StyledSelectorContainer>
</AriaInputGroup>
@@ -401,18 +431,13 @@ function IpVersionSetting() {
const { updateRelaySettings } = useAppContext();
const relaySettings = useSelector((state) => state.settings.relaySettings);
const ipVersion = useMemo(() => {
- const ipVersion =
- 'normal' in relaySettings ? relaySettings.normal.wireguard.ipVersion : undefined;
- return ipVersion === 'any' ? undefined : ipVersion;
+ const ipVersion = 'normal' in relaySettings ? relaySettings.normal.wireguard.ipVersion : 'any';
+ return ipVersion === 'any' ? null : ipVersion;
}, [relaySettings]);
- const ipVersionItems: ISelectorItem<OptionalIpVersion>[] = useMemo(
+ const ipVersionItems: SelectorItem<IpVersion>[] = useMemo(
() => [
{
- label: messages.gettext('Automatic'),
- value: undefined,
- },
- {
label: messages.gettext('IPv4'),
value: 'ipv4',
},
@@ -425,10 +450,10 @@ function IpVersionSetting() {
);
const setIpVersion = useCallback(
- async (ipVersion?: IpVersion) => {
+ async (ipVersion: IpVersion | null) => {
const relayUpdate = createWireguardRelayUpdater(relaySettings)
.tunnel.wireguard((wireguard) => {
- if (ipVersion) {
+ if (ipVersion !== null) {
wireguard.ipVersion.exact(ipVersion);
} else {
wireguard.ipVersion.any();
@@ -451,9 +476,10 @@ function IpVersionSetting() {
<Selector
// TRANSLATORS: The title for the WireGuard IP version selector.
title={messages.pgettext('wireguard-settings-view', 'IP version')}
- values={ipVersionItems}
+ items={ipVersionItems}
value={ipVersion}
onSelect={setIpVersion}
+ automaticValue={null}
/>
</StyledSelectorContainer>
<Cell.Footer>
@@ -476,10 +502,6 @@ function IpVersionSetting() {
);
}
-function removeNonNumericCharacters(value: string) {
- return value.replace(/[^0-9]/g, '');
-}
-
function mtuIsValid(mtu: string): boolean {
const parsedMtu = mtu ? parseInt(mtu) : undefined;
return (
diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx
index 81ba648338..e68fac424c 100644
--- a/gui/src/renderer/components/cell/Input.tsx
+++ b/gui/src/renderer/components/cell/Input.tsx
@@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'rea
import styled from 'styled-components';
import { colors } from '../../../config.json';
-import { useBoolean } from '../../lib/utilityHooks';
+import { useBoolean, useCombinedRefs } from '../../lib/utilityHooks';
import { normalText } from '../common-styles';
import ImageView from '../ImageView';
import { BackAction } from '../KeyboardNavigation';
@@ -45,112 +45,96 @@ interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
onChangeValue?: (value: string) => void;
}
-interface IInputState {
- value?: string;
- focused: boolean;
-}
-
-export class Input extends React.Component<IInputProps, IInputState> {
- public state = {
- value: this.props.value ?? '',
- focused: false,
- };
-
- public inputRef = React.createRef<HTMLInputElement>();
-
- public componentDidUpdate(prevProps: IInputProps, _prevState: IInputState) {
- if (
- !this.state.focused &&
- prevProps.value !== this.props.value &&
- this.props.value !== this.state.value
- ) {
- this.setState(
- (_state, props) => ({
- value: props.value,
- }),
- () => {
- this.props.onChangeValue?.(this.state.value);
- },
- );
- }
- }
+function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputElement>) {
+ const {
+ validateValue,
+ modifyValue,
+ submitOnBlur,
+ onSubmitValue,
+ onChangeValue,
+ ...otherProps
+ } = props;
- public render() {
- const {
- type: _type,
- onChange: _onChange,
- onFocus: _onFocus,
- onBlur: _onBlur,
- onKeyPress: _onKeyPress,
- value: _value,
- modifyValue: _modifyValue,
- submitOnBlur: _submitOnBlur,
- onChangeValue: _onChangeValue,
- onSubmitValue: _onSubmitValue,
- validateValue,
- ...otherProps
- } = this.props;
+ const [value, setValue] = useState(props.value ?? '');
+ const [isFocused, setFocused, setBlurred] = useBoolean(false);
- const valid = validateValue?.(this.state.value);
+ const inputRef = useRef() as React.RefObject<HTMLInputElement>;
+ const combinedRef = useCombinedRefs(inputRef, forwardedRef);
- return (
- <CellDisabledContext.Consumer>
- {(disabled) => (
- <StyledInput
- ref={this.inputRef}
- type="text"
- valid={valid}
- focused={this.state.focused}
- aria-invalid={!valid}
- onChange={this.onChange}
- onFocus={this.onFocus}
- onBlur={this.onBlur}
- onKeyPress={this.onKeyPress}
- value={this.state.value}
- disabled={disabled}
- {...otherProps}
- />
- )}
- </CellDisabledContext.Consumer>
- );
- }
+ const onFocus = useCallback(
+ (event: React.FocusEvent<HTMLInputElement>) => {
+ setFocused();
+ props.onFocus?.(event);
+ },
+ [props.onFocus],
+ );
- public blur = () => {
- this.inputRef.current?.blur();
- };
+ const onBlur = useCallback(
+ (event: React.FocusEvent<HTMLInputElement>) => {
+ setBlurred();
+ props.onBlur?.(event);
- private onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- const value = this.props.modifyValue?.(event.target.value) ?? event.target.value;
- this.setState({ value });
- this.props.onChange?.(event);
- this.props.onChangeValue?.(value);
- };
+ if (validateValue?.(value) !== false && submitOnBlur) {
+ onSubmitValue?.(value);
+ }
+ },
+ [value, props.onBlur, validateValue, onSubmitValue, submitOnBlur],
+ );
- private onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
- this.setState({ focused: true });
- this.props.onFocus?.(event);
- };
+ const onChange = useCallback(
+ (event: React.ChangeEvent<HTMLInputElement>) => {
+ const value = modifyValue?.(event.target.value) ?? event.target.value;
+ setValue(value);
+ props.onChange?.(event);
+ onChangeValue?.(value);
+ },
+ [value, modifyValue, props.onSubmit, onSubmitValue],
+ );
- private onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
- this.setState({ focused: false });
+ const onKeyPress = useCallback(
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
+ if (event.key === 'Enter') {
+ onSubmitValue?.(value);
+ inputRef.current?.blur();
+ }
+ props.onKeyPress?.(event);
+ },
+ [value, onSubmitValue, inputRef, props.onKeyPress],
+ );
- this.props.onBlur?.(event);
- if (this.props.validateValue?.(this.state.value) !== false && this.props.submitOnBlur) {
- this.props.onSubmitValue?.(this.state.value);
- } else {
- this.setState({ value: this.props.value });
+ useEffect(() => {
+ if (!isFocused && props.value !== undefined && value !== props.value) {
+ setValue(props.value);
+ onChangeValue?.(props.value);
}
- };
+ }, [props.value]);
- private onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
- if (event.key === 'Enter') {
- this.props.onSubmitValue?.(this.state.value);
- this.inputRef.current?.blur();
- }
- this.props.onKeyPress?.(event);
- };
+ const valid = validateValue?.(value);
+
+ return (
+ <CellDisabledContext.Consumer>
+ {(disabled) => (
+ <StyledInput
+ {...otherProps}
+ ref={combinedRef}
+ type="text"
+ valid={valid}
+ focused={isFocused}
+ aria-invalid={!valid}
+ onChange={onChange}
+ onFocus={onFocus}
+ onBlur={onBlur}
+ onKeyPress={onKeyPress}
+ value={value}
+ disabled={disabled}
+ />
+ )}
+ </CellDisabledContext.Consumer>
+ );
}
+export const Input = React.memo(React.forwardRef(InputWithRef));
+
const InputFrame = styled.div((props: { focused: boolean }) => ({
display: 'flex',
flexGrow: 0,
@@ -177,12 +161,13 @@ const StyledAutoSizingTextInputWrapper = styled.div({
height: '100%',
});
-export function AutoSizingTextInput(props: IInputProps) {
+function AutoSizingTextInputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputElement>) {
const { onChangeValue, onFocus, onBlur, ...otherProps } = props;
const [value, setValue] = useState(otherProps.value ?? '');
const [focused, setFocused, setBlurred] = useBoolean(false);
- const inputRef = useRef() as React.RefObject<Input>;
+ const inputRef = useRef() as React.RefObject<HTMLInputElement>;
+ const combinedRef = useCombinedRefs(inputRef, forwardedRef);
const onChangeValueWrapper = useCallback(
(value: string) => {
@@ -216,7 +201,7 @@ export function AutoSizingTextInput(props: IInputProps) {
<StyledAutoSizingTextInputContainer>
<StyledAutoSizingTextInputWrapper>
<Input
- ref={inputRef}
+ ref={combinedRef}
onChangeValue={onChangeValueWrapper}
onBlur={onBlurWrapper}
onFocus={onFocusWrapper}
@@ -232,6 +217,8 @@ export function AutoSizingTextInput(props: IInputProps) {
);
}
+export const AutoSizingTextInput = React.memo(React.forwardRef(AutoSizingTextInputWithRef));
+
const StyledCellInputRowContainer = styled(Container)({
backgroundColor: 'white',
marginBottom: '1px',
diff --git a/gui/src/renderer/components/cell/Selector.tsx b/gui/src/renderer/components/cell/Selector.tsx
index e815519552..d898e3f51d 100644
--- a/gui/src/renderer/components/cell/Selector.tsx
+++ b/gui/src/renderer/components/cell/Selector.tsx
@@ -1,7 +1,8 @@
-import * as React from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { colors } from '../../../config.json';
+import { messages } from '../../../shared/gettext';
import { useBoolean } from '../../lib/utilityHooks';
import Accordion from '../Accordion';
import { AriaDetails, AriaInput, AriaLabel } from '../AriaGroup';
@@ -24,44 +25,69 @@ const StyledChevronButton = styled(ChevronButton)({
marginRight: '16px',
});
-export interface ISelectorItem<T> {
+export interface SelectorItem<T> {
label: string;
value: T;
disabled?: boolean;
}
-interface ISelectorProps<T> {
+// T represents the available values and U represent the value of "Automatic"/"Any" if there is one.
+interface CommonSelectorProps<T, U> {
title?: string;
- values: Array<ISelectorItem<T>>;
- value: T;
- onSelect: (value: T) => void;
- selectedCellRef?: React.Ref<HTMLButtonElement>;
+ items: Array<SelectorItem<T>>;
+ value: T | U;
+ selectedCellRef?: React.Ref<HTMLElement>;
className?: string;
details?: React.ReactElement;
expandable?: boolean;
disabled?: boolean;
thinTitle?: boolean;
+ automaticLabel?: string;
+ automaticValue?: U;
+ children?: React.ReactNode | Array<React.ReactNode>;
+}
+
+interface SelectorProps<T, U> extends CommonSelectorProps<T, U> {
+ onSelect: (value: T | U) => void;
}
-export default function Selector<T>(props: ISelectorProps<T>) {
+export default function Selector<T, U>(props: SelectorProps<T, U>) {
const [expanded, , , toggleExpanded] = useBoolean(!props.expandable);
- const items = props.values.map((item, i) => {
- const selected = item.value === props.value;
+ const items = props.items.map((item) => {
+ const selected = props.value === item.value;
+ const ref = selected ? (props.selectedCellRef as React.Ref<HTMLButtonElement>) : undefined;
return (
<SelectorCell
- key={i}
+ key={`value-${item.value}`}
value={item.value}
- selected={selected}
+ isSelected={selected}
disabled={props.disabled || item.disabled}
- forwardedRef={selected ? props.selectedCellRef : undefined}
+ forwardedRef={ref}
onSelect={props.onSelect}>
{item.label}
</SelectorCell>
);
});
+ if (props.automaticValue !== undefined) {
+ const selected = props.value === props.automaticValue;
+ const ref = selected ? (props.selectedCellRef as React.Ref<HTMLButtonElement>) : undefined;
+
+ items.unshift(
+ <SelectorCell
+ key={'automatic'}
+ value={props.automaticValue}
+ isSelected={selected}
+ disabled={props.disabled}
+ forwardedRef={ref}
+ onSelect={props.onSelect}>
+ {props.automaticLabel ?? messages.gettext('Automatic')}
+ </SelectorCell>,
+ );
+ }
+
const title = props.title && (
<StyledTitle>
<AriaLabel>
@@ -78,11 +104,19 @@ export default function Selector<T>(props: ISelectorProps<T>) {
</StyledTitle>
);
+ // Add potential additional items to the list. Used for custom entry.
+ const children = (
+ <>
+ {items}
+ {props.children}
+ </>
+ );
+
return (
<AriaInput>
<Cell.Section role="listbox" className={props.className}>
{title}
- {props.expandable ? <Accordion expanded={expanded}>{items}</Accordion> : items}
+ {props.expandable ? <Accordion expanded={expanded}>{children}</Accordion> : children}
</Cell.Section>
</AriaInput>
);
@@ -97,40 +131,155 @@ const StyledLabel = styled(Cell.Label)(normalText, {
fontWeight: 400,
});
-interface ISelectorCellProps<T> {
+interface SelectorCellProps<T> {
value: T;
- selected: boolean;
+ isSelected: boolean;
disabled?: boolean;
onSelect: (value: T) => void;
- children?: React.ReactText;
+ children: React.ReactNode | Array<React.ReactNode>;
forwardedRef?: React.Ref<HTMLButtonElement>;
}
-class SelectorCell<T> extends React.Component<ISelectorCellProps<T>> {
- public render() {
- return (
- <Cell.CellButton
- ref={this.props.forwardedRef}
- onClick={this.onClick}
- selected={this.props.selected}
- disabled={this.props.disabled}
+function SelectorCell<T>(props: SelectorCellProps<T>) {
+ const handleClick = useCallback(() => {
+ if (!props.isSelected) {
+ props.onSelect(props.value);
+ }
+ }, [props.isSelected, props.onSelect, props.value]);
+
+ return (
+ <Cell.CellButton
+ ref={props.forwardedRef}
+ onClick={handleClick}
+ selected={props.isSelected}
+ disabled={props.disabled}
+ role="option"
+ aria-selected={props.isSelected}
+ aria-disabled={props.disabled}>
+ <StyledCellIcon
+ visible={props.isSelected}
+ source="icon-tick"
+ width={18}
+ tintColor={colors.white}
+ />
+ <StyledLabel>{props.children}</StyledLabel>
+ </Cell.CellButton>
+ );
+}
+
+interface StyledCustomContainerProps {
+ selected: boolean;
+}
+
+const StyledCustomContainer = styled(Cell.Container)((props: StyledCustomContainerProps) => ({
+ minHeight: '44px',
+ backgroundColor: props.selected ? colors.green : colors.blue40,
+ ':hover': {
+ backgroundColor: props.selected ? colors.green : colors.blue,
+ },
+}));
+
+// Adding undefined as possible value of the selector to be able to select nothing.
+interface SelectorWithCustomItemProps<T, U> extends CommonSelectorProps<T | undefined, U> {
+ inputPlaceholder: string;
+ onSelect: (value: T | U) => void;
+ onSelectCustom: (value: string) => void;
+ maxLength?: number;
+ selectedCellRef?: React.Ref<HTMLDivElement>;
+ validateValue?: (value: string) => boolean;
+ modifyValue?: (value: string) => string;
+}
+
+export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<T, U>) {
+ const {
+ value,
+ inputPlaceholder,
+ onSelect,
+ onSelectCustom,
+ maxLength,
+ selectedCellRef,
+ validateValue,
+ modifyValue,
+ ...otherProps
+ } = props;
+
+ // The component needs to keep track of when the custom item should look selected before it has a
+ // value.
+ const [customIsSelectedWithoutValue, setCustomIsSelectedWithoutValue] = useState(false);
+ const inputRef = useRef() as React.RefObject<HTMLInputElement>;
+
+ const itemIsSelected =
+ props.items.some((item) => item.value === value) || props.automaticValue === value;
+ const customIsSelected = !itemIsSelected || customIsSelectedWithoutValue;
+
+ const handleClick = useCallback(() => {
+ if (!customIsSelected) {
+ setCustomIsSelectedWithoutValue(true);
+ inputRef.current?.focus();
+ }
+ }, [customIsSelected, inputRef.current]);
+
+ // Wrap onSelect to be able to catch when a new value is selected during the
+ // customIsSelectedWithoutValue phase.
+ const handleSelectValue = useCallback(
+ (newValue: T | U | undefined) => {
+ if (customIsSelectedWithoutValue && newValue === value) {
+ setCustomIsSelectedWithoutValue(false);
+ } else if (newValue !== undefined) {
+ onSelect(newValue);
+ }
+ },
+ [customIsSelected, value, onSelect],
+ );
+
+ const handleSubmit = useCallback((value: string) => {
+ if (validateValue?.(value) !== false) {
+ onSelectCustom(value);
+ }
+ }, []);
+
+ // If props.value changes while customIsSelectedWithoutValue then we want to switch to that value
+ // instead.
+ useEffect(() => {
+ if (customIsSelected) {
+ setCustomIsSelectedWithoutValue(false);
+ }
+ }, [value]);
+
+ return (
+ <Selector<T | undefined, U>
+ {...otherProps}
+ onSelect={handleSelectValue}
+ value={customIsSelected ? undefined : value}>
+ <StyledCustomContainer
+ ref={customIsSelected ? props.selectedCellRef : undefined}
+ onClick={handleClick}
+ selected={customIsSelected}
+ disabled={props.disabled}
role="option"
- aria-selected={this.props.selected}
- aria-disabled={this.props.disabled}>
+ aria-selected={customIsSelected}
+ aria-disabled={props.disabled}>
<StyledCellIcon
- visible={this.props.selected}
+ visible={customIsSelected}
source="icon-tick"
width={18}
tintColor={colors.white}
/>
- <StyledLabel>{this.props.children}</StyledLabel>
- </Cell.CellButton>
- );
- }
-
- private onClick = () => {
- if (!this.props.selected) {
- this.props.onSelect(this.props.value);
- }
- };
+ <StyledLabel>{messages.gettext('Custom')}</StyledLabel>
+ <AriaInput>
+ <Cell.AutoSizingTextInput
+ ref={inputRef}
+ value={itemIsSelected || customIsSelectedWithoutValue ? '' : `${props.value}`}
+ placeholder={inputPlaceholder}
+ inputMode={'numeric'}
+ maxLength={maxLength ?? 4}
+ onSubmitValue={handleSubmit}
+ submitOnBlur={true}
+ validateValue={validateValue}
+ modifyValue={modifyValue}
+ />
+ </AriaInput>
+ </StyledCustomContainer>
+ </Selector>
+ );
}
diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts
index 8126f30205..c6e5aed985 100644
--- a/gui/src/renderer/redux/settings/actions.ts
+++ b/gui/src/renderer/redux/settings/actions.ts
@@ -1,5 +1,10 @@
import { IWindowsApplication } from '../../../shared/application-types';
-import { BridgeState, IDnsOptions, ObfuscationSettings } from '../../../shared/daemon-rpc-types';
+import {
+ BridgeState,
+ IDnsOptions,
+ IWireguardEndpointData,
+ ObfuscationSettings,
+} from '../../../shared/daemon-rpc-types';
import { IGuiSettingsState } from '../../../shared/gui-settings-state';
import { BridgeSettingsRedux, IRelayLocationRedux, RelaySettingsRedux } from './reducers';
@@ -23,6 +28,11 @@ export interface IUpdateBridgeLocationsAction {
bridgeLocations: IRelayLocationRedux[];
}
+export interface IUpdateWireguardEndpointData {
+ type: 'UPDATE_WIREGUARD_ENDPOINT_DATA';
+ wireguardEndpointData: IWireguardEndpointData;
+}
+
export interface IUpdateAllowLanAction {
type: 'UPDATE_ALLOW_LAN';
allowLan: boolean;
@@ -93,6 +103,7 @@ export type SettingsAction =
| IUpdateRelayAction
| IUpdateRelayLocationsAction
| IUpdateBridgeLocationsAction
+ | IUpdateWireguardEndpointData
| IUpdateAllowLanAction
| IUpdateEnableIpv6Action
| IUpdateBlockWhenDisconnectedAction
@@ -137,6 +148,15 @@ function updateBridgeLocations(
};
}
+function updateWireguardEndpointData(
+ wireguardEndpointData: IWireguardEndpointData,
+): IUpdateWireguardEndpointData {
+ return {
+ type: 'UPDATE_WIREGUARD_ENDPOINT_DATA',
+ wireguardEndpointData,
+ };
+}
+
function updateAllowLan(allowLan: boolean): IUpdateAllowLanAction {
return {
type: 'UPDATE_ALLOW_LAN',
@@ -239,6 +259,7 @@ export default {
updateRelay,
updateRelayLocations,
updateBridgeLocations,
+ updateWireguardEndpointData,
updateAllowLan,
updateEnableIpv6,
updateBlockWhenDisconnected,
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index ee9d81e37f..e4437563ec 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -3,6 +3,7 @@ import {
BridgeState,
IDnsOptions,
IpVersion,
+ IWireguardEndpointData,
LiftedConstraint,
ObfuscationSettings,
ObfuscationType,
@@ -82,6 +83,7 @@ export interface ISettingsReduxState {
relaySettings: RelaySettingsRedux;
relayLocations: IRelayLocationRedux[];
bridgeLocations: IRelayLocationRedux[];
+ wireguardEndpointData: IWireguardEndpointData;
allowLan: boolean;
enableIpv6: boolean;
bridgeSettings: BridgeSettingsRedux;
@@ -127,6 +129,7 @@ const initialState: ISettingsReduxState = {
},
relayLocations: [],
bridgeLocations: [],
+ wireguardEndpointData: { portRanges: [], udp2tcpPorts: [] },
allowLan: false,
enableIpv6: true,
bridgeSettings: {
@@ -191,6 +194,12 @@ export default function (
bridgeLocations: action.bridgeLocations,
};
+ case 'UPDATE_WIREGUARD_ENDPOINT_DATA':
+ return {
+ ...state,
+ wireguardEndpointData: action.wireguardEndpointData,
+ };
+
case 'UPDATE_ALLOW_LAN':
return {
...state,
diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts
index 5893e4def6..6b75ee7d3c 100644
--- a/gui/src/shared/daemon-rpc-types.ts
+++ b/gui/src/shared/daemon-rpc-types.ts
@@ -119,7 +119,7 @@ export interface IProxyEndpoint {
export type DaemonEvent =
| { tunnelState: TunnelState }
| { settings: ISettings }
- | { relayList: IRelayList }
+ | { relayList: IRelayListWithEndpointData }
| { appVersionInfo: IAppVersionInfo }
| { device: DeviceEvent }
| { deviceRemoval: Array<IDevice> };
@@ -224,10 +224,20 @@ export type RelaySettingsUpdate =
customTunnelEndpoint: IRelaySettingsCustom;
};
+export interface IRelayListWithEndpointData {
+ relayList: IRelayList;
+ wireguardEndpointData: IWireguardEndpointData;
+}
+
export interface IRelayList {
countries: IRelayListCountry[];
}
+export interface IWireguardEndpointData {
+ portRanges: [number, number][];
+ udp2tcpPorts: number[];
+}
+
export interface IRelayListCountry {
name: string;
code: string;
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index 33ac82180e..85c7f2cabc 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -15,6 +15,7 @@ import {
ILocation,
IRelayList,
ISettings,
+ IWireguardEndpointData,
ObfuscationSettings,
RelaySettingsUpdate,
TunnelState,
@@ -45,6 +46,7 @@ export interface ITranslations {
export interface IRelayListPair {
relays: IRelayList;
bridges: IRelayList;
+ wireguardEndpointData: IWireguardEndpointData;
}
export type LaunchApplicationResult = { success: true } | { error: string };
diff --git a/gui/src/shared/string-helpers.ts b/gui/src/shared/string-helpers.ts
index c69ebddfcf..983a8e8796 100644
--- a/gui/src/shared/string-helpers.ts
+++ b/gui/src/shared/string-helpers.ts
@@ -5,3 +5,7 @@ export function capitalize(inputString: string): string {
export function capitalizeEveryWord(inputString: string): string {
return inputString.split(' ').map(capitalize).join(' ');
}
+
+export function removeNonNumericCharacters(value: string) {
+ return value.replace(/[^0-9]/g, '');
+}