diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-08-18 11:00:22 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-08-19 11:10:07 +0200 |
| commit | 7f1a1331f87a5cd392cfce1193c229cb05f7945a (patch) | |
| tree | f0e6f44cdc343020efd543231794606fd2c89298 | |
| parent | 10f11292e778c3700ac301b08a02ad9e29957166 (diff) | |
| download | mullvadvpn-7f1a1331f87a5cd392cfce1193c229cb05f7945a.tar.xz mullvadvpn-7f1a1331f87a5cd392cfce1193c229cb05f7945a.zip | |
Move parts of advanced settings into openvpn and wireguard settings
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 357 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppRouter.tsx | 8 | ||||
| -rw-r--r-- | gui/src/renderer/components/OpenVPNSettings.tsx | 251 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeys.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardSettings.tsx | 195 | ||||
| -rw-r--r-- | gui/src/renderer/containers/AdvancedSettingsPage.tsx | 103 | ||||
| -rw-r--r-- | gui/src/renderer/containers/OpenVPNSettingsPage.tsx | 111 | ||||
| -rw-r--r-- | gui/src/renderer/containers/WireguardSettingsPage.tsx | 73 | ||||
| -rw-r--r-- | gui/src/renderer/lib/routes.ts | 4 | ||||
| -rw-r--r-- | gui/src/shared/localization-contexts.ts | 5 |
10 files changed, 689 insertions, 420 deletions
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index 338a4d5b81..df2c824358 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -1,23 +1,16 @@ import * as React from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; -import { - BridgeState, - IDnsOptions, - RelayProtocol, - TunnelProtocol, -} from '../../shared/daemon-rpc-types'; +import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { IpAddress } from '../lib/ip'; import { WgKeyState } from '../redux/settings/reducers'; import { StyledButtonCellGroup, StyledContainer, - StyledInputFrame, StyledNavigationScrollbars, StyledNoWireguardKeyError, StyledNoWireguardKeyErrorContainer, - StyledSelectorContainer, StyledSelectorForFooter, StyledTunnelProtocolContainer, StyledCustomDnsSwitchContainer, @@ -39,52 +32,25 @@ import { NavigationItems, TitleBarItem, } from './NavigationBar'; -import Selector, { ISelectorItem } from './cell/Selector'; +import { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import Accordion from './Accordion'; import { formatMarkdown } from '../markdown-formatter'; -const MIN_MSSFIX_VALUE = 1000; -const MAX_MSSFIX_VALUE = 1450; -const MIN_WIREGUARD_MTU_VALUE = 1280; -const MAX_WIREGUARD_MTU_VALUE = 1420; -const UDP_PORTS = [1194, 1195, 1196, 1197, 1300, 1301, 1302]; -const TCP_PORTS = [80, 443]; -const WIREUGARD_UDP_PORTS = [51820, 53]; - -type OptionalPort = number | undefined; - -type OptionalRelayProtocol = RelayProtocol | undefined; type OptionalTunnelProtocol = TunnelProtocol | undefined; -function mapPortToSelectorItem(value: number): ISelectorItem<number> { - return { label: value.toString(), value }; -} - interface IProps { enableIpv6: boolean; blockWhenDisconnected: boolean; tunnelProtocol?: TunnelProtocol; - openvpn: { - protocol?: RelayProtocol; - port?: number; - }; wireguardKeyState: WgKeyState; - wireguard: { port?: number }; - mssfix?: number; - wireguardMtu?: number; - bridgeState: BridgeState; dns: IDnsOptions; - setBridgeState: (value: BridgeState) => void; setEnableIpv6: (value: boolean) => void; setBlockWhenDisconnected: (value: boolean) => void; setTunnelProtocol: (value: OptionalTunnelProtocol) => void; - setOpenVpnMssfix: (value: number | undefined) => void; - setWireguardMtu: (value: number | undefined) => void; - setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void; - setWireguardRelayPort: (port?: number) => void; setDnsOptions: (dns: IDnsOptions) => Promise<void>; - onViewWireguardKeys: () => void; + onViewWireguardSettings: () => void; + onViewOpenVpnSettings: () => void; onViewSplitTunneling: () => void; onClose: () => void; } @@ -108,59 +74,6 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { private customDnsAddButtonRef = React.createRef<HTMLButtonElement>(); private customDnsInputContainerRef = React.createRef<HTMLDivElement>(); - private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> }; - private protocolItems: Array<ISelectorItem<OptionalRelayProtocol>>; - private bridgeStateItems: Array<ISelectorItem<BridgeState>>; - private wireguardPortItems: Array<ISelectorItem<OptionalPort>>; - - constructor(props: IProps) { - super(props); - - const automaticPort: ISelectorItem<OptionalPort> = { - label: messages.pgettext('advanced-settings-view', 'Automatic'), - value: undefined, - }; - - this.portItems = { - udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)), - tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)), - }; - - this.wireguardPortItems = [automaticPort].concat( - WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem), - ); - - this.protocolItems = [ - { - label: messages.pgettext('advanced-settings-view', 'Automatic'), - value: undefined, - }, - { - label: messages.pgettext('advanced-settings-view', 'TCP'), - value: 'tcp', - }, - { - label: messages.pgettext('advanced-settings-view', 'UDP'), - value: 'udp', - }, - ]; - - this.bridgeStateItems = [ - { - label: messages.pgettext('advanced-settings-view', 'Automatic'), - value: 'auto', - }, - { - label: messages.pgettext('advanced-settings-view', 'On'), - value: 'on', - }, - { - label: messages.pgettext('advanced-settings-view', 'Off'), - value: 'off', - }, - ]; - } - public render() { const hasWireguardKey = this.props.wireguardKeyState.type === 'key-set'; @@ -245,6 +158,18 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { </Cell.Footer> </AriaInputGroup> + {(window.env.platform === 'linux' || window.env.platform === 'win32') && ( + <StyledButtonCellGroup> + <Cell.CellButton onClick={this.props.onViewSplitTunneling}> + <Cell.Label> + {window.env.platform === 'win32' && <StyledBetaLabel />} + {messages.pgettext('advanced-settings-view', 'Split tunneling')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </StyledButtonCellGroup> + )} + <AriaInputGroup> <StyledTunnelProtocolContainer> <StyledSelectorForFooter @@ -268,189 +193,25 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { </StyledTunnelProtocolContainer> </AriaInputGroup> - {this.props.tunnelProtocol !== 'wireguard' ? ( - <AriaInputGroup> - <StyledSelectorContainer> - <Selector - title={messages.pgettext( - 'advanced-settings-view', - 'OpenVPN transport protocol', - )} - values={this.protocolItems} - value={this.props.openvpn.protocol} - onSelect={this.onSelectOpenvpnProtocol} - /> - - {this.props.openvpn.protocol ? ( - <Selector - title={sprintf( - // TRANSLATORS: The title for the port selector section. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) - messages.pgettext( - 'advanced-settings-view', - 'OpenVPN %(portType)s port', - ), - { - portType: this.props.openvpn.protocol.toUpperCase(), - }, - )} - values={this.portItems[this.props.openvpn.protocol]} - value={this.props.openvpn.port} - onSelect={this.onSelectOpenVpnPort} - /> - ) : undefined} - </StyledSelectorContainer> - </AriaInputGroup> - ) : undefined} - - {this.props.tunnelProtocol === 'wireguard' ? ( - <AriaInputGroup> - <StyledSelectorContainer> - <StyledSelectorForFooter - // TRANSLATORS: The title for the shadowsocks bridge selector section. - title={messages.pgettext('advanced-settings-view', 'WireGuard port')} - values={this.wireguardPortItems} - value={this.props.wireguard.port} - onSelect={this.onSelectWireguardPort} - /> - </StyledSelectorContainer> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - { - // TRANSLATORS: The hint displayed below the WireGuard port selector. - messages.pgettext( - 'advanced-settings-view', - 'The automatic setting will randomly choose from a wide range of ports.', - ) - } - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - ) : undefined} - - <AriaInputGroup> - <Selector - title={ - // TRANSLATORS: The title for the shadowsocks bridge selector section. - messages.pgettext('advanced-settings-view', 'Bridge mode') - } - values={this.bridgeStateItems} - value={this.props.bridgeState} - onSelect={this.onSelectBridgeState} - /> - </AriaInputGroup> - - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('advanced-settings-view', 'OpenVPN Mssfix')} - </Cell.InputLabel> - </AriaLabel> - <StyledInputFrame> - <AriaInput> - <Cell.AutoSizingTextInput - value={this.props.mssfix ? this.props.mssfix.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.pgettext('advanced-settings-view', 'Default')} - onSubmitValue={this.onMssfixSubmit} - validateValue={AdvancedSettings.mssfixIsValid} - submitOnBlur={true} - modifyValue={AdvancedSettings.removeNonNumericCharacters} - /> - </AriaInput> - </StyledInputFrame> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the Mssfix input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(max)d - the maximum possible mssfix value - // TRANSLATORS: %(min)d - the minimum possible mssfix value - messages.pgettext( - 'advanced-settings-view', - 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.', - ), - { - min: MIN_MSSFIX_VALUE, - max: MAX_MSSFIX_VALUE, - }, - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('advanced-settings-view', 'WireGuard MTU')} - </Cell.InputLabel> - </AriaLabel> - <StyledInputFrame> - <AriaInput> - <Cell.AutoSizingTextInput - value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.pgettext('advanced-settings-view', 'Default')} - onSubmitValue={this.onWireguardMtuSubmit} - validateValue={AdvancedSettings.wireguarMtuIsValid} - submitOnBlur={true} - modifyValue={AdvancedSettings.removeNonNumericCharacters} - /> - </AriaInput> - </StyledInputFrame> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the WireGuard MTU input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value - // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value - messages.pgettext( - 'advanced-settings-view', - 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.', - ), - { - min: MIN_WIREGUARD_MTU_VALUE, - max: MAX_WIREGUARD_MTU_VALUE, - }, - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - <StyledButtonCellGroup> - <Cell.CellButton onClick={this.props.onViewWireguardKeys}> + <Cell.CellButton + onClick={this.props.onViewWireguardSettings} + disabled={this.props.tunnelProtocol === 'openvpn'}> <Cell.Label> - {messages.pgettext('advanced-settings-view', 'WireGuard key')} + {messages.pgettext('advanced-settings-view', 'WireGuard settings')} </Cell.Label> <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> - </StyledButtonCellGroup> - {(window.env.platform === 'linux' || window.env.platform === 'win32') && ( - <StyledButtonCellGroup> - <Cell.CellButton onClick={this.props.onViewSplitTunneling}> - <Cell.Label> - {window.env.platform === 'win32' && <StyledBetaLabel />} - {messages.pgettext('advanced-settings-view', 'Split tunneling')} - </Cell.Label> - <Cell.Icon height={12} width={7} source="icon-chevron" /> - </Cell.CellButton> - </StyledButtonCellGroup> - )} + <Cell.CellButton + onClick={this.props.onViewOpenVpnSettings} + disabled={this.props.tunnelProtocol === 'wireguard'}> + <Cell.Label> + {messages.pgettext('advanced-settings-view', 'OpenVPN settings')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </StyledButtonCellGroup> <StyledCustomDnsSwitchContainer disabled={!this.customDnsAvailable()}> <AriaInputGroup> @@ -644,19 +405,19 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { value: undefined, }, { - label: messages.pgettext('advanced-settings-view', 'OpenVPN'), - value: 'openvpn', - }, - { label: hasWireguardKey ? messages.pgettext('advanced-settings-view', 'WireGuard') : sprintf('%(label)s (%(error)s)', { label: messages.pgettext('advanced-settings-view', 'WireGuard'), - error: messages.pgettext('advanced-settings-view-wireguard', 'missing key'), + error: messages.pgettext('advanced-settings-view', 'missing key'), }), value: 'wireguard', disabled: !hasWireguardKey, }, + { + label: messages.pgettext('advanced-settings-view', 'OpenVPN'), + value: 'openvpn', + }, ]; }; @@ -731,56 +492,6 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { private onSelectTunnelProtocol = (protocol?: TunnelProtocol) => { this.props.setTunnelProtocol(protocol); }; - - private onSelectOpenvpnProtocol = (protocol?: RelayProtocol) => { - this.props.setOpenVpnRelayProtocolAndPort(protocol); - }; - - private onSelectOpenVpnPort = (port?: number) => { - this.props.setOpenVpnRelayProtocolAndPort(this.props.openvpn.protocol, port); - }; - - private onSelectWireguardPort = (port?: number) => { - this.props.setWireguardRelayPort(port); - }; - - private onSelectBridgeState = (bridgeState: BridgeState) => { - this.props.setBridgeState(bridgeState); - }; - - private onMssfixSubmit = (value: string) => { - const parsedValue = value === '' ? undefined : parseInt(value, 10); - if (AdvancedSettings.mssfixIsValid(value)) { - this.props.setOpenVpnMssfix(parsedValue); - } - }; - - private static removeNonNumericCharacters(value: string) { - return value.replace(/[^0-9]/g, ''); - } - - private static mssfixIsValid(mssfix: string): boolean { - const parsedMssFix = mssfix ? parseInt(mssfix) : undefined; - return ( - parsedMssFix === undefined || - (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE) - ); - } - - private onWireguardMtuSubmit = (value: string) => { - const parsedValue = value === '' ? undefined : parseInt(value, 10); - if (AdvancedSettings.wireguarMtuIsValid(value)) { - this.props.setWireguardMtu(parsedValue); - } - }; - - private static wireguarMtuIsValid(mtu: string): boolean { - const parsedMtu = mtu ? parseInt(mtu) : undefined; - return ( - parsedMtu === undefined || - (parsedMtu >= MIN_WIREGUARD_MTU_VALUE && parsedMtu <= MAX_WIREGUARD_MTU_VALUE) - ); - } } function CustomDnsDisabledMessage() { diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index 9f049fc791..9921109d23 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -10,6 +10,7 @@ import TransitionContainer, { TransitionView } from './TransitionContainer'; import AccountPage from '../containers/AccountPage'; import AdvancedSettingsPage from '../containers/AdvancedSettingsPage'; import LoginPage from '../containers/LoginPage'; +import OpenVPNSettingsPage from '../containers/OpenVPNSettingsPage'; import PlatformWindowContainer from '../containers/PlatformWindowContainer'; import PreferencesPage from '../containers/PreferencesPage'; import SelectLanguagePage from '../containers/SelectLanguagePage'; @@ -17,6 +18,7 @@ import SelectLocationPage from '../containers/SelectLocationPage'; import SettingsPage from '../containers/SettingsPage'; import SupportPage from '../containers/SupportPage'; import WireguardKeysPage from '../containers/WireguardKeysPage'; +import WireguardSettingsPage from '../containers/WireguardSettingsPage'; import { IHistoryProps, ITransitionSpecification, transitions, withHistory } from '../lib/history'; import { SetupFinished, @@ -91,7 +93,13 @@ class AppRouter extends React.Component<IHistoryProps, IAppRoutesState> { <Route exact path={RoutePath.accountSettings} component={AccountPage} /> <Route exact path={RoutePath.preferences} component={PreferencesPage} /> <Route exact path={RoutePath.advancedSettings} component={AdvancedSettingsPage} /> + <Route + exact + path={RoutePath.wireguardSettings} + component={WireguardSettingsPage} + /> <Route exact path={RoutePath.wireguardKeys} component={WireguardKeysPage} /> + <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} /> <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} /> <Route exact path={RoutePath.support} component={SupportPage} /> <Route exact path={RoutePath.selectLocation} component={SelectLocationPage} /> diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx new file mode 100644 index 0000000000..d3ce2ff507 --- /dev/null +++ b/gui/src/renderer/components/OpenVPNSettings.tsx @@ -0,0 +1,251 @@ +import * as React from 'react'; +import { sprintf } from 'sprintf-js'; +import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types'; +import { messages } from '../../shared/gettext'; +import { + StyledContainer, + StyledInputFrame, + StyledNavigationScrollbars, + StyledSelectorContainer, +} from './AdvancedSettingsStyles'; +import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; +import * as Cell from './cell'; +import { Layout } from './Layout'; +import { ModalContainer } from './Modal'; +import { + BackBarItem, + NavigationBar, + NavigationContainer, + NavigationItems, + TitleBarItem, +} from './NavigationBar'; +import Selector, { ISelectorItem } from './cell/Selector'; +import SettingsHeader, { HeaderTitle } from './SettingsHeader'; + +const MIN_MSSFIX_VALUE = 1000; +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; + +function mapPortToSelectorItem(value: number): ISelectorItem<number> { + return { label: value.toString(), value }; +} + +interface IProps { + openvpn: { + protocol?: RelayProtocol; + port?: number; + }; + mssfix?: number; + bridgeState: BridgeState; + setOpenVpnMssfix: (value: number | undefined) => void; + setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void; + setBridgeState: (value: BridgeState) => void; + onClose: () => void; +} + +export default class OpenVpnSettings extends React.Component<IProps> { + private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> }; + private protocolItems: Array<ISelectorItem<OptionalRelayProtocol>>; + private bridgeStateItems: Array<ISelectorItem<BridgeState>>; + + constructor(props: IProps) { + super(props); + + const automaticPort: ISelectorItem<OptionalPort> = { + label: messages.gettext('Automatic'), + value: undefined, + }; + + this.portItems = { + udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)), + tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)), + }; + + this.protocolItems = [ + { + label: messages.gettext('Automatic'), + value: undefined, + }, + { + label: messages.gettext('TCP'), + value: 'tcp', + }, + { + label: messages.gettext('UDP'), + value: 'udp', + }, + ]; + + this.bridgeStateItems = [ + { + label: messages.gettext('Automatic'), + value: 'auto', + }, + { + label: messages.gettext('On'), + value: 'on', + }, + { + label: messages.gettext('Off'), + value: 'off', + }, + ]; + } + + public render() { + return ( + <ModalContainer> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <BackBarItem action={this.props.onClose}> + { + // TRANSLATORS: Back button in navigation bar + messages.pgettext('navigation-bar', 'Advanced') + } + </BackBarItem> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('openvpn-settings-nav', 'OpenVPN settings') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + + <StyledNavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('openvpn-settings-view', 'OpenVPN settings')} + </HeaderTitle> + </SettingsHeader> + + <AriaInputGroup> + <StyledSelectorContainer> + <Selector + title={messages.pgettext('openvpn-settings-view', 'Transport protocol')} + values={this.protocolItems} + value={this.props.openvpn.protocol} + onSelect={this.onSelectOpenvpnProtocol} + /> + + {this.props.openvpn.protocol ? ( + <Selector + title={sprintf( + // TRANSLATORS: The title for the port selector section. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) + messages.pgettext('openvpn-settings-view', '%(portType)s port'), + { + portType: this.props.openvpn.protocol.toUpperCase(), + }, + )} + values={this.portItems[this.props.openvpn.protocol]} + value={this.props.openvpn.port} + onSelect={this.onSelectOpenVpnPort} + /> + ) : undefined} + </StyledSelectorContainer> + </AriaInputGroup> + + <AriaInputGroup> + <Selector + title={ + // TRANSLATORS: The title for the shadowsocks bridge selector section. + messages.pgettext('openvpn-settings-view', 'Bridge mode') + } + values={this.bridgeStateItems} + value={this.props.bridgeState} + onSelect={this.onSelectBridgeState} + /> + </AriaInputGroup> + + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('openvpn-settings-view', 'Mssfix')} + </Cell.InputLabel> + </AriaLabel> + <StyledInputFrame> + <AriaInput> + <Cell.AutoSizingTextInput + value={this.props.mssfix ? this.props.mssfix.toString() : ''} + inputMode={'numeric'} + maxLength={4} + placeholder={messages.gettext('Default')} + onSubmitValue={this.onMssfixSubmit} + validateValue={OpenVpnSettings.mssfixIsValid} + submitOnBlur={true} + modifyValue={OpenVpnSettings.removeNonNumericCharacters} + /> + </AriaInput> + </StyledInputFrame> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the Mssfix input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(max)d - the maximum possible mssfix value + // TRANSLATORS: %(min)d - the minimum possible mssfix value + messages.pgettext( + 'openvpn-settings-view', + 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.', + ), + { + min: MIN_MSSFIX_VALUE, + max: MAX_MSSFIX_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </ModalContainer> + ); + } + + private onSelectOpenvpnProtocol = (protocol?: RelayProtocol) => { + this.props.setOpenVpnRelayProtocolAndPort(protocol); + }; + + private onSelectOpenVpnPort = (port?: number) => { + this.props.setOpenVpnRelayProtocolAndPort(this.props.openvpn.protocol, port); + }; + + private onSelectBridgeState = (bridgeState: BridgeState) => { + this.props.setBridgeState(bridgeState); + }; + + private onMssfixSubmit = (value: string) => { + const parsedValue = value === '' ? undefined : parseInt(value, 10); + if (OpenVpnSettings.mssfixIsValid(value)) { + this.props.setOpenVpnMssfix(parsedValue); + } + }; + + private static removeNonNumericCharacters(value: string) { + return value.replace(/[^0-9]/g, ''); + } + + private static mssfixIsValid(mssfix: string): boolean { + const parsedMssFix = mssfix ? parseInt(mssfix) : undefined; + return ( + parsedMssFix === undefined || + (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE) + ); + } +} diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx index f6e143e6a3..e76820b006 100644 --- a/gui/src/renderer/components/WireguardKeys.tsx +++ b/gui/src/renderer/components/WireguardKeys.tsx @@ -103,7 +103,7 @@ export default class WireguardKeys extends React.Component<IProps, IState> { <BackBarItem action={this.props.onClose}> { // TRANSLATORS: Back button in navigation bar - messages.pgettext('wireguard-keys-nav', 'Advanced') + messages.pgettext('wireguard-keys-nav', 'WireGuard settings') } </BackBarItem> <TitleBarItem> diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx new file mode 100644 index 0000000000..91e55b2c49 --- /dev/null +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -0,0 +1,195 @@ +import * as React from 'react'; +import { sprintf } from 'sprintf-js'; +import { messages } from '../../shared/gettext'; +import { + StyledButtonCellGroup, + StyledContainer, + StyledInputFrame, + StyledNavigationScrollbars, + StyledSelectorContainer, + StyledSelectorForFooter, +} from './AdvancedSettingsStyles'; +import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; +import * as Cell from './cell'; +import { Layout } from './Layout'; +import { + BackBarItem, + NavigationBar, + NavigationContainer, + NavigationItems, + TitleBarItem, +} from './NavigationBar'; +import { ISelectorItem } from './cell/Selector'; +import SettingsHeader, { HeaderTitle } from './SettingsHeader'; + +const MIN_WIREGUARD_MTU_VALUE = 1280; +const MAX_WIREGUARD_MTU_VALUE = 1420; +const WIREUGARD_UDP_PORTS = [51820, 53]; + +type OptionalPort = number | undefined; + +function mapPortToSelectorItem(value: number): ISelectorItem<number> { + return { label: value.toString(), value }; +} + +interface IProps { + wireguard: { port?: number }; + wireguardMtu?: number; + setWireguardMtu: (value: number | undefined) => void; + setWireguardRelayPort: (port?: number) => void; + onViewWireguardKeys: () => void; + onClose: () => void; +} + +export default class WireguardSettings extends React.Component<IProps> { + private wireguardPortItems: Array<ISelectorItem<OptionalPort>>; + + constructor(props: IProps) { + super(props); + + const automaticPort: ISelectorItem<OptionalPort> = { + label: messages.gettext('Automatic'), + value: undefined, + }; + + this.wireguardPortItems = [automaticPort].concat( + WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem), + ); + } + + public render() { + return ( + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <BackBarItem action={this.props.onClose}> + { + // TRANSLATORS: Back button in navigation bar + messages.pgettext('navigation-bar', 'Advanced') + } + </BackBarItem> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('wireguard-settings-nav', 'WireGuard settings') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + + <StyledNavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('wireguard-settings-view', 'WireGuard settings')} + </HeaderTitle> + </SettingsHeader> + + <AriaInputGroup> + <StyledSelectorContainer> + <StyledSelectorForFooter + // TRANSLATORS: The title for the shadowsocks bridge selector section. + title={messages.pgettext('wireguard-settings-view', 'Port')} + values={this.wireguardPortItems} + value={this.props.wireguard.port} + onSelect={this.onSelectWireguardPort} + /> + </StyledSelectorContainer> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + { + // TRANSLATORS: The hint displayed below the WireGuard port selector. + messages.pgettext( + 'wireguard-settings-view', + 'The automatic setting will randomly choose from a wide range of ports.', + ) + } + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + + <StyledButtonCellGroup> + <Cell.CellButton onClick={this.props.onViewWireguardKeys}> + <Cell.Label> + {messages.pgettext('wireguard-settings-view', 'WireGuard key')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </StyledButtonCellGroup> + + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('wireguard-settings-view', 'MTU')} + </Cell.InputLabel> + </AriaLabel> + <StyledInputFrame> + <AriaInput> + <Cell.AutoSizingTextInput + value={this.props.wireguardMtu ? this.props.wireguardMtu.toString() : ''} + inputMode={'numeric'} + maxLength={4} + placeholder={messages.gettext('Default')} + onSubmitValue={this.onWireguardMtuSubmit} + validateValue={WireguardSettings.wireguarMtuIsValid} + submitOnBlur={true} + modifyValue={WireguardSettings.removeNonNumericCharacters} + /> + </AriaInput> + </StyledInputFrame> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the WireGuard MTU input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(max)d - the maximum possible wireguard mtu value + // TRANSLATORS: %(min)d - the minimum possible wireguard mtu value + messages.pgettext( + 'wireguard-settings-view', + 'Set WireGuard MTU value. Valid range: %(min)d - %(max)d.', + ), + { + min: MIN_WIREGUARD_MTU_VALUE, + max: MAX_WIREGUARD_MTU_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + ); + } + + private onSelectWireguardPort = (port?: number) => { + this.props.setWireguardRelayPort(port); + }; + + private static removeNonNumericCharacters(value: string) { + return value.replace(/[^0-9]/g, ''); + } + + private onWireguardMtuSubmit = (value: string) => { + const parsedValue = value === '' ? undefined : parseInt(value, 10); + if (WireguardSettings.wireguarMtuIsValid(value)) { + this.props.setWireguardMtu(parsedValue); + } + }; + + private static wireguarMtuIsValid(mtu: string): boolean { + const parsedMtu = mtu ? parseInt(mtu) : undefined; + return ( + parsedMtu === undefined || + (parsedMtu >= MIN_WIREGUARD_MTU_VALUE && parsedMtu <= MAX_WIREGUARD_MTU_VALUE) + ); + } +} diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx index d16f193ce0..afd6134042 100644 --- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx @@ -1,10 +1,5 @@ import { connect } from 'react-redux'; -import { - BridgeState, - IDnsOptions, - RelayProtocol, - TunnelProtocol, -} from '../../shared/daemon-rpc-types'; +import { IDnsOptions, TunnelProtocol } from '../../shared/daemon-rpc-types'; import log from '../../shared/logging'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import AdvancedSettings from '../components/AdvancedSettings'; @@ -16,42 +11,25 @@ import { RelaySettingsRedux } from '../redux/settings/reducers'; import { IReduxState, ReduxDispatch } from '../redux/store'; const mapStateToProps = (state: IReduxState) => { - const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings); + const tunnelProtocol = mapRelaySettingsToProtocol(state.settings.relaySettings); return { enableIpv6: state.settings.enableIpv6, blockWhenDisconnected: state.settings.blockWhenDisconnected, wireguardKeyState: state.settings.wireguardKeyState, - mssfix: state.settings.openVpn.mssfix, - wireguardMtu: state.settings.wireguard.mtu, - bridgeState: state.settings.bridgeState, dns: state.settings.dns, - ...protocolAndPort, + tunnelProtocol, }; }; -const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => { +const mapRelaySettingsToProtocol = (relaySettings: RelaySettingsRedux) => { if ('normal' in relaySettings) { - const { tunnelProtocol, openvpn, wireguard } = relaySettings.normal; - return { - openvpn: { - protocol: openvpn.protocol === 'any' ? undefined : openvpn.protocol, - port: openvpn.port === 'any' ? undefined : openvpn.port, - }, - wireguard: { port: wireguard.port === 'any' ? undefined : wireguard.port }, - tunnelProtocol: tunnelProtocol === 'any' ? undefined : tunnelProtocol, - }; + const { tunnelProtocol } = relaySettings.normal; + return tunnelProtocol === 'any' ? undefined : tunnelProtocol; // since the GUI doesn't display custom settings, just display the default ones. // If the user sets any settings, then those will be applied. } else if ('customTunnelEndpoint' in relaySettings) { - return { - openvpn: { - protocol: undefined, - port: undefined, - }, - wireguard: { port: undefined }, - tunnelProtocol: undefined, - }; + return undefined; } else { throw new Error('Unknown type of relay settings.'); } @@ -62,46 +40,6 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp onClose: () => { props.history.pop(); }, - setOpenVpnRelayProtocolAndPort: async (protocol?: RelayProtocol, port?: number) => { - const relayUpdate = RelaySettingsBuilder.normal() - .tunnel.openvpn((openvpn) => { - if (protocol) { - openvpn.protocol.exact(protocol); - } else { - openvpn.protocol.any(); - } - - if (port) { - openvpn.port.exact(port); - } else { - openvpn.port.any(); - } - }) - .build(); - - try { - await props.app.updateRelaySettings(relayUpdate); - } catch (e) { - log.error('Failed to update relay settings', e.message); - } - }, - - setWireguardRelayPort: async (port?: number) => { - const relayUpdate = RelaySettingsBuilder.normal() - .tunnel.wireguard((wireguard) => { - if (port) { - wireguard.port.exact(port); - } else { - wireguard.port.any(); - } - }) - .build(); - try { - await props.app.updateRelaySettings(relayUpdate); - } catch (e) { - log.error('Failed to update relay settings', e.message); - } - }, setTunnelProtocol: async (tunnelProtocol: TunnelProtocol | undefined) => { const relayUpdate = RelaySettingsBuilder.normal() @@ -136,35 +74,12 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp } }, - setBridgeState: async (bridgeState: BridgeState) => { - try { - await props.app.setBridgeState(bridgeState); - } catch (e) { - log.error(`Failed to update bridge state: ${e.message}`); - } - }, - - setOpenVpnMssfix: async (mssfix?: number) => { - try { - await props.app.setOpenVpnMssfix(mssfix); - } catch (e) { - log.error('Failed to update mssfix value', e.message); - } - }, - - setWireguardMtu: async (mtu?: number) => { - try { - await props.app.setWireguardMtu(mtu); - } catch (e) { - log.error('Failed to update mtu value', e.message); - } - }, - setDnsOptions: (dns: IDnsOptions) => { return props.app.setDnsOptions(dns); }, - onViewWireguardKeys: () => props.history.push(RoutePath.wireguardKeys), + onViewWireguardSettings: () => props.history.push(RoutePath.wireguardSettings), + onViewOpenVpnSettings: () => props.history.push(RoutePath.openVpnSettings), onViewSplitTunneling: () => props.history.push(RoutePath.splitTunneling), }; }; diff --git a/gui/src/renderer/containers/OpenVPNSettingsPage.tsx b/gui/src/renderer/containers/OpenVPNSettingsPage.tsx new file mode 100644 index 0000000000..378dc9bb82 --- /dev/null +++ b/gui/src/renderer/containers/OpenVPNSettingsPage.tsx @@ -0,0 +1,111 @@ +import { connect } from 'react-redux'; +import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types'; +import log from '../../shared/logging'; +import RelaySettingsBuilder from '../../shared/relay-settings-builder'; +import OpenVPNSettings from '../components/OpenVPNSettings'; + +import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; +import { RelaySettingsRedux } from '../redux/settings/reducers'; +import { IReduxState, ReduxDispatch } from '../redux/store'; + +const mapStateToProps = (state: IReduxState) => { + const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings); + + return { + mssfix: state.settings.openVpn.mssfix, + bridgeState: state.settings.bridgeState, + ...protocolAndPort, + }; +}; + +const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => { + if ('normal' in relaySettings) { + const { openvpn } = relaySettings.normal; + return { + openvpn: { + protocol: openvpn.protocol === 'any' ? undefined : openvpn.protocol, + port: openvpn.port === 'any' ? undefined : openvpn.port, + }, + }; + // since the GUI doesn't display custom settings, just display the default ones. + // If the user sets any settings, then those will be applied. + } else if ('customTunnelEndpoint' in relaySettings) { + return { + openvpn: { + protocol: undefined, + port: undefined, + }, + }; + } else { + throw new Error('Unknown type of relay settings.'); + } +}; + +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { + return { + onClose: () => { + props.history.pop(); + }, + setOpenVpnRelayProtocolAndPort: async (protocol?: RelayProtocol, port?: number) => { + const relayUpdate = RelaySettingsBuilder.normal() + .tunnel.openvpn((openvpn) => { + if (protocol) { + openvpn.protocol.exact(protocol); + } else { + openvpn.protocol.any(); + } + + if (port) { + openvpn.port.exact(port); + } else { + openvpn.port.any(); + } + }) + .build(); + + try { + await props.app.updateRelaySettings(relayUpdate); + } catch (e) { + log.error('Failed to update relay settings', e.message); + } + }, + + setWireguardRelayPort: async (port?: number) => { + const relayUpdate = RelaySettingsBuilder.normal() + .tunnel.wireguard((wireguard) => { + if (port) { + wireguard.port.exact(port); + } else { + wireguard.port.any(); + } + }) + .build(); + try { + await props.app.updateRelaySettings(relayUpdate); + } catch (e) { + log.error('Failed to update relay settings', e.message); + } + }, + + setBridgeState: async (bridgeState: BridgeState) => { + try { + await props.app.setBridgeState(bridgeState); + } catch (e) { + log.error(`Failed to update bridge state: ${e.message}`); + } + }, + + setOpenVpnMssfix: async (mssfix?: number) => { + try { + await props.app.setOpenVpnMssfix(mssfix); + } catch (e) { + log.error('Failed to update mssfix value', e.message); + } + }, + }; +}; + +export default withAppContext( + withHistory(connect(mapStateToProps, mapDispatchToProps)(OpenVPNSettings)), +); diff --git a/gui/src/renderer/containers/WireguardSettingsPage.tsx b/gui/src/renderer/containers/WireguardSettingsPage.tsx new file mode 100644 index 0000000000..e89bcff62a --- /dev/null +++ b/gui/src/renderer/containers/WireguardSettingsPage.tsx @@ -0,0 +1,73 @@ +import { connect } from 'react-redux'; +import log from '../../shared/logging'; +import RelaySettingsBuilder from '../../shared/relay-settings-builder'; +import WireguardSettings from '../components/WireguardSettings'; + +import withAppContext, { IAppContext } from '../context'; +import { IHistoryProps, withHistory } from '../lib/history'; +import { RoutePath } from '../lib/routes'; +import { RelaySettingsRedux } from '../redux/settings/reducers'; +import { IReduxState, ReduxDispatch } from '../redux/store'; + +const mapStateToProps = (state: IReduxState) => { + const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings); + + return { + wireguardMtu: state.settings.wireguard.mtu, + ...protocolAndPort, + }; +}; + +const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => { + if ('normal' in relaySettings) { + const port = relaySettings.normal.wireguard.port; + return { wireguard: { port: port === 'any' ? undefined : port } }; + // since the GUI doesn't display custom settings, just display the default ones. + // If the user sets any settings, then those will be applied. + } else if ('customTunnelEndpoint' in relaySettings) { + return { + wireguard: { port: undefined }, + }; + } else { + throw new Error('Unknown type of relay settings.'); + } +}; + +const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { + return { + onClose: () => { + props.history.pop(); + }, + + setWireguardRelayPort: async (port?: number) => { + const relayUpdate = RelaySettingsBuilder.normal() + .tunnel.wireguard((wireguard) => { + if (port) { + wireguard.port.exact(port); + } else { + wireguard.port.any(); + } + }) + .build(); + try { + await props.app.updateRelaySettings(relayUpdate); + } catch (e) { + log.error('Failed to update relay settings', e.message); + } + }, + + setWireguardMtu: async (mtu?: number) => { + try { + await props.app.setWireguardMtu(mtu); + } catch (e) { + log.error('Failed to update mtu value', e.message); + } + }, + + onViewWireguardKeys: () => props.history.push(RoutePath.wireguardKeys), + }; +}; + +export default withAppContext( + withHistory(connect(mapStateToProps, mapDispatchToProps)(WireguardSettings)), +); diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts index 8d8c2ce27b..dee83c2a42 100644 --- a/gui/src/renderer/lib/routes.ts +++ b/gui/src/renderer/lib/routes.ts @@ -11,7 +11,9 @@ export enum RoutePath { accountSettings = '/settings/account', preferences = '/settings/preferences', advancedSettings = '/settings/advanced', - wireguardKeys = '/settings/advanced/wireguard-keys', + wireguardSettings = '/settings/advanced/wireguard', + wireguardKeys = '/settings/advanced/wireguard/keys', + openVpnSettings = '/settings/advanced/openvpn', splitTunneling = '/settings/advanced/split-tunneling', support = '/settings/support', selectLocation = '/select-location', diff --git a/gui/src/shared/localization-contexts.ts b/gui/src/shared/localization-contexts.ts index 7e80e68a5b..b39dcc0681 100644 --- a/gui/src/shared/localization-contexts.ts +++ b/gui/src/shared/localization-contexts.ts @@ -25,7 +25,10 @@ export type LocalizationContexts = | 'preferences-nav' | 'advanced-settings-view' | 'advanced-settings-nav' - | 'advanced-settings-view-wireguard' + | 'wireguard-settings-view' + | 'wireguard-settings-nav' + | 'openvpn-settings-view' + | 'openvpn-settings-nav' | 'wireguard-key-view' | 'wireguard-keys-nav' | 'split-tunneling-view' |
