summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-07-20 13:28:34 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-07-25 11:40:06 +0200
commitc4ae17e338e78bfb0b22b1ba0bc25e6a8f6f28ad (patch)
tree04d68c0dfa03fabeb8f295fc62b6774f573f2d30 /gui/src/renderer
parenta46df6ba2f6f3fedd79dab4980c7fc883f79536d (diff)
downloadmullvadvpn-c4ae17e338e78bfb0b22b1ba0bc25e6a8f6f28ad.tar.xz
mullvadvpn-c4ae17e338e78bfb0b22b1ba0bc25e6a8f6f28ad.zip
Refactore OpenVpn settings view
Diffstat (limited to 'gui/src/renderer')
-rw-r--r--gui/src/renderer/app.tsx8
-rw-r--r--gui/src/renderer/components/AppRouter.tsx4
-rw-r--r--gui/src/renderer/components/OpenVPNSettings.tsx719
-rw-r--r--gui/src/renderer/containers/OpenVPNSettingsPage.tsx136
4 files changed, 422 insertions, 445 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 9e2130ff1f..ec69b5d8a7 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -436,11 +436,11 @@ export default class AppRenderer {
actions.settings.updateEnableIpv6(enableIpv6);
};
- public async setBridgeState(bridgeState: BridgeState) {
+ public setBridgeState = async (bridgeState: BridgeState) => {
const actions = this.reduxActions;
await IpcRendererEventChannel.settings.setBridgeState(bridgeState);
actions.settings.updateBridgeState(bridgeState);
- }
+ };
public setBlockWhenDisconnected = async (blockWhenDisconnected: boolean) => {
const actions = this.reduxActions;
@@ -448,11 +448,11 @@ export default class AppRenderer {
actions.settings.updateBlockWhenDisconnected(blockWhenDisconnected);
};
- public async setOpenVpnMssfix(mssfix?: number) {
+ public setOpenVpnMssfix = async (mssfix?: number) => {
const actions = this.reduxActions;
actions.settings.updateOpenVpnMssfix(mssfix);
await IpcRendererEventChannel.settings.setOpenVpnMssfix(mssfix);
- }
+ };
public setWireguardMtu = async (mtu?: number) => {
const actions = this.reduxActions;
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 4791faecb6..b6f2865299 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -4,7 +4,6 @@ import { Route, Switch } from 'react-router';
import AccountPage from '../containers/AccountPage';
import LoginPage from '../containers/LoginPage';
-import OpenVPNSettingsPage from '../containers/OpenVPNSettingsPage';
import ProblemReportPage from '../containers/ProblemReportPage';
import SelectLanguagePage from '../containers/SelectLanguagePage';
import SelectLocationPage from '../containers/SelectLocationPage';
@@ -23,6 +22,7 @@ import Focus, { IFocusHandle } from './Focus';
import InterfaceSettings from './InterfaceSettings';
import Launch from './Launch';
import MainView from './MainView';
+import OpenVPNSettings from './OpenVPNSettings';
import Settings from './Settings';
import SplitTunnelingSettings from './SplitTunnelingSettings';
import Support from './Support';
@@ -93,7 +93,7 @@ class AppRouter extends React.Component<IHistoryProps & IAppContext, IAppRoutesS
<Route exact path={RoutePath.interfaceSettings} component={InterfaceSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
- <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} />
+ <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.support} component={Support} />
<Route exact path={RoutePath.problemReport} component={ProblemReportPage} />
diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx
index 1b82c9971a..fce949c6a9 100644
--- a/gui/src/renderer/components/OpenVPNSettings.tsx
+++ b/gui/src/renderer/components/OpenVPNSettings.tsx
@@ -1,11 +1,17 @@
-import * as React from 'react';
+import { useCallback, useMemo } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { strings } from '../../config.json';
-import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types';
+import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import log from '../../shared/logging';
+import RelaySettingsBuilder from '../../shared/relay-settings-builder';
+import { useAppContext } from '../context';
+import { useHistory } from '../lib/history';
+import { useBoolean } from '../lib/utilityHooks';
import { formatMarkdown } from '../markdown-formatter';
+import { useSelector } from '../redux/store';
import * as AppButton from './AppButton';
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
import * as Cell from './cell';
@@ -49,226 +55,86 @@ export const StyledSelectorContainer = styled.div({
flex: 0,
});
-interface IProps {
- bridgeModeAvailablity: BridgeModeAvailability;
- 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;
-}
-
-interface IState {
- showBridgeStateConfirmationDialog: boolean;
-}
-
-export default class OpenVpnSettings extends React.Component<IProps, IState> {
- public state = { showBridgeStateConfirmationDialog: false };
+export default function OpenVpnSettings() {
+ const { pop } = useHistory();
- private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> };
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
- constructor(props: IProps) {
- super(props);
+ const protocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
- const automaticPort: ISelectorItem<OptionalPort> = {
- label: messages.gettext('Automatic'),
- value: undefined,
- };
+ return (
+ <BackAction action={pop}>
+ <Layout>
+ <SettingsContainer>
+ <NavigationContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <TitleBarItem>
+ {sprintf(
+ // TRANSLATORS: Title label in navigation bar
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN"
+ messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'),
+ { openvpn: strings.openvpn },
+ )}
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- this.portItems = {
- udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)),
- tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)),
- };
- }
+ <NavigationScrollbars>
+ <SettingsHeader>
+ <HeaderTitle>
+ {sprintf(
+ // TRANSLATORS: %(openvpn)s will be replaced with "OpenVPN"
+ messages.pgettext('openvpn-settings-view', '%(openvpn)s settings'),
+ {
+ openvpn: strings.openvpn,
+ },
+ )}
+ </HeaderTitle>
+ </SettingsHeader>
- public render() {
- return (
- <BackAction action={this.props.onClose}>
- <Layout>
- <SettingsContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {sprintf(
- // TRANSLATORS: Title label in navigation bar
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN"
- messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'),
- { openvpn: strings.openvpn },
- )}
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
-
- <StyledNavigationScrollbars>
- <SettingsHeader>
- <HeaderTitle>
- {sprintf(
- // TRANSLATORS: %(openvpn)s will be replaced with "OpenVPN"
- messages.pgettext('openvpn-settings-view', '%(openvpn)s settings'),
- {
- openvpn: strings.openvpn,
- },
- )}
- </HeaderTitle>
- </SettingsHeader>
+ <Cell.Group>
+ <TransportProtocolSelector />
+ </Cell.Group>
+ {protocol ? (
<Cell.Group>
- <StyledSelectorContainer>
- <AriaInputGroup>
- <Selector
- title={messages.pgettext('openvpn-settings-view', 'Transport protocol')}
- values={this.protocolItems(this.props.bridgeState !== 'on')}
- value={this.props.openvpn.protocol}
- onSelect={this.onSelectOpenvpnProtocol}
- />
- {this.props.bridgeState === 'on' && (
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {formatMarkdown(
- // TRANSLATORS: This is used to instruct users how to make UDP mode
- // TRANSLATORS: available.
- messages.pgettext(
- 'openvpn-settings-view',
- 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.',
- ),
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- )}
- </AriaInputGroup>
- </StyledSelectorContainer>
+ <PortSelector />
</Cell.Group>
+ ) : undefined}
- {this.props.openvpn.protocol ? (
- <Cell.Group>
- <StyledSelectorContainer>
- <AriaInputGroup>
- <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}
- />
- </AriaInputGroup>
- </StyledSelectorContainer>
- </Cell.Group>
- ) : undefined}
+ <Cell.Group>
+ <BridgeModeSelector />
+ </Cell.Group>
- <Cell.Group>
- <AriaInputGroup>
- <StyledSelectorContainer>
- <Selector
- title={
- // TRANSLATORS: The title for the shadowsocks bridge selector section.
- messages.pgettext('openvpn-settings-view', 'Bridge mode')
- }
- values={this.bridgeStateItems(
- this.props.bridgeModeAvailablity === BridgeModeAvailability.available,
- )}
- value={this.props.bridgeState}
- onSelect={this.onSelectBridgeState}
- />
- </StyledSelectorContainer>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>{this.bridgeModeFooterText()}</Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
+ <Cell.Group>
+ <MssFixSetting />
+ </Cell.Group>
+ </NavigationScrollbars>
+ </NavigationContainer>
+ </SettingsContainer>
+ </Layout>
+ </BackAction>
+ );
+}
- <Cell.Group>
- <AriaInputGroup>
- <Cell.Container>
- <AriaLabel>
- <Cell.InputLabel>
- {messages.pgettext('openvpn-settings-view', 'Mssfix')}
- </Cell.InputLabel>
- </AriaLabel>
- <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>
- </Cell.Container>
- <Cell.Footer>
- <AriaDescription>
- <Cell.FooterText>
- {sprintf(
- // TRANSLATORS: The hint displayed below the Mssfix input field.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(openvpn)s - will be replaced with "OpenVPN"
- // TRANSLATORS: %(max)d - the maximum possible mssfix value
- // TRANSLATORS: %(min)d - the minimum possible mssfix value
- messages.pgettext(
- 'openvpn-settings-view',
- 'Set %(openvpn)s MSS value. Valid range: %(min)d - %(max)d.',
- ),
- {
- openvpn: strings.openvpn,
- min: MIN_MSSFIX_VALUE,
- max: MAX_MSSFIX_VALUE,
- },
- )}
- </Cell.FooterText>
- </AriaDescription>
- </Cell.Footer>
- </AriaInputGroup>
- </Cell.Group>
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </SettingsContainer>
+function TransportProtocolSelector() {
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+ const bridgeState = useSelector((state) => state.settings.bridgeState);
- {this.renderBridgeStateConfirmation()}
- </Layout>
- </BackAction>
- );
- }
+ const protocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
- private bridgeStateItems(onAvailable: boolean): Array<ISelectorItem<BridgeState>> {
- return [
- {
- label: messages.gettext('Automatic'),
- value: 'auto',
- },
- {
- label: messages.gettext('On'),
- value: 'on',
- disabled: !onAvailable,
- },
- {
- label: messages.gettext('Off'),
- value: 'off',
- },
- ];
- }
+ const protocolAndPortUpdater = useProtocolAndPortUpdater();
- private protocolItems(udpAvailable: boolean): Array<ISelectorItem<OptionalRelayProtocol>> {
- return [
+ const items: ISelectorItem<OptionalRelayProtocol>[] = useMemo(
+ () => [
{
label: messages.gettext('Automatic'),
value: undefined,
@@ -280,125 +146,372 @@ export default class OpenVpnSettings extends React.Component<IProps, IState> {
{
label: messages.gettext('UDP'),
value: 'udp',
- disabled: !udpAvailable,
+ disabled: bridgeState === 'on',
},
- ];
- }
+ ],
+ [bridgeState],
+ );
- private onSelectOpenvpnProtocol = (protocol?: RelayProtocol) => {
- this.props.setOpenVpnRelayProtocolAndPort(protocol);
- };
+ return (
+ <StyledSelectorContainer>
+ <AriaInputGroup>
+ <Selector
+ title={messages.pgettext('openvpn-settings-view', 'Transport protocol')}
+ values={items}
+ value={protocol}
+ onSelect={protocolAndPortUpdater}
+ />
+ {bridgeState === 'on' && (
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {formatMarkdown(
+ // TRANSLATORS: This is used to instruct users how to make UDP mode
+ // TRANSLATORS: available.
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.',
+ ),
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ )}
+ </AriaInputGroup>
+ </StyledSelectorContainer>
+ );
+}
+
+function useProtocolAndPortUpdater() {
+ const { updateRelaySettings } = useAppContext();
+
+ const updater = useCallback(
+ 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();
- private onSelectOpenVpnPort = (port?: number) => {
- this.props.setOpenVpnRelayProtocolAndPort(this.props.openvpn.protocol, port);
+ try {
+ await updateRelaySettings(relayUpdate);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update relay settings', error.message);
+ }
+ },
+ [updateRelaySettings],
+ );
+
+ return updater;
+}
+
+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;
+ }, [relaySettings]);
+
+ const port = useMemo(() => {
+ const port = 'normal' in relaySettings ? relaySettings.normal.openvpn.port : undefined;
+ return port === 'any' ? undefined : port;
+ }, [relaySettings]);
+
+ const protocolAndPortUpdater = useProtocolAndPortUpdater();
+
+ const onSelect = useCallback(
+ async (port?: number) => {
+ await protocolAndPortUpdater(protocol, port);
+ },
+ [protocolAndPortUpdater, protocol],
+ );
+
+ const automaticPort: ISelectorItem<OptionalPort> = {
+ label: messages.gettext('Automatic'),
+ value: undefined,
};
- private onMssfixSubmit = (value: string) => {
- const parsedValue = value === '' ? undefined : parseInt(value, 10);
- if (OpenVpnSettings.mssfixIsValid(value)) {
- this.props.setOpenVpnMssfix(parsedValue);
- }
+ const portItems = {
+ udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)),
+ tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)),
};
- private static removeNonNumericCharacters(value: string) {
- return value.replace(/[^0-9]/g, '');
+ if (protocol === undefined) {
+ return null;
}
- private static mssfixIsValid(mssfix: string): boolean {
- const parsedMssFix = mssfix ? parseInt(mssfix) : undefined;
- return (
- parsedMssFix === undefined ||
- (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE)
- );
- }
-
- private bridgeModeFooterText() {
- switch (this.props.bridgeModeAvailablity) {
- case BridgeModeAvailability.blockedDueToTunnelProtocol:
- return formatMarkdown(
- sprintf(
- // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
- // TRANSLATORS: available.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(tunnelProtocol)s - the name of the tunnel protocol setting
- // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
- messages.pgettext(
- 'openvpn-settings-view',
- 'To activate Bridge mode, go back and change **%(tunnelProtocol)s** to **%(openvpn)s**.',
- ),
- {
- tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'),
- openvpn: strings.openvpn,
- },
- ),
- );
- case BridgeModeAvailability.blockedDueToTransportProtocol:
- return formatMarkdown(
- sprintf(
- // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
- // TRANSLATORS: available.
+ return (
+ <StyledSelectorContainer>
+ <AriaInputGroup>
+ <Selector
+ title={sprintf(
+ // TRANSLATORS: The title for the port selector section.
// TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(transportProtocol)s - the name of the transport protocol setting
- // TRANSLATORS: %(automat)s - the translation of "Automatic"
- // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
- messages.pgettext(
- 'openvpn-settings-view',
- 'To activate Bridge mode, change **%(transportProtocol)s** to **%(automatic)s** or **%(tcp)s**.',
- ),
+ // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP)
+ messages.pgettext('openvpn-settings-view', '%(portType)s port'),
{
- transportProtocol: messages.pgettext('openvpn-settings-view', 'Transport protocol'),
- automatic: messages.gettext('Automatic'),
- tcp: messages.gettext('TCP'),
+ portType: protocol.toUpperCase(),
},
- ),
- );
- case BridgeModeAvailability.available:
- return sprintf(
- // TRANSLATORS: This is used as a description for the bridge mode
- // TRANSLATORS: setting.
- // TRANSLATORS: Available placeholders:
- // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
- messages.pgettext(
- 'openvpn-settings-view',
- 'Helps circumvent censorship, by routing your traffic through a bridge server before reaching an %(openvpn)s server. Obfuscation is added to make fingerprinting harder.',
- ),
- { openvpn: strings.openvpn },
- );
- }
- }
+ )}
+ values={portItems[protocol]}
+ value={port}
+ onSelect={onSelect}
+ />
+ </AriaInputGroup>
+ </StyledSelectorContainer>
+ );
+}
+
+function BridgeModeSelector() {
+ const { setBridgeState: setBridgeStateImpl } = useAppContext();
+ const relaySettings = useSelector((state) => state.settings.relaySettings);
+
+ const bridgeState = useSelector((state) => state.settings.bridgeState);
+
+ const tunnelProtocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
+
+ const transportProtocol = useMemo(() => {
+ const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined;
+ return protocol === 'any' ? undefined : protocol;
+ }, [relaySettings]);
+
+ const options: ISelectorItem<BridgeState>[] = useMemo(
+ () => [
+ {
+ label: messages.gettext('Automatic'),
+ value: 'auto',
+ },
+ {
+ label: messages.gettext('On'),
+ value: 'on',
+ disabled: tunnelProtocol !== 'openvpn' || transportProtocol === 'udp',
+ },
+ {
+ label: messages.gettext('Off'),
+ value: 'off',
+ },
+ ],
+ [tunnelProtocol, transportProtocol],
+ );
+
+ const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean();
+
+ const setBridgeState = useCallback(
+ async (bridgeState: BridgeState) => {
+ try {
+ await setBridgeStateImpl(bridgeState);
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to update bridge state: ${error.message}`);
+ }
+ },
+ [setBridgeStateImpl],
+ );
+
+ const onSelectBridgeState = useCallback(
+ async (newValue: BridgeState) => {
+ if (newValue === 'on') {
+ showConfirmationDialog();
+ } else {
+ await setBridgeState(newValue);
+ }
+ },
+ [showConfirmationDialog, setBridgeState],
+ );
+
+ const confirmBridgeState = useCallback(async () => {
+ hideConfirmationDialog();
+ await setBridgeState('on');
+ }, [hideConfirmationDialog, setBridgeState]);
- private renderBridgeStateConfirmation = () => {
- return (
+ return (
+ <>
+ <AriaInputGroup>
+ <StyledSelectorContainer>
+ <Selector
+ title={
+ // TRANSLATORS: The title for the shadowsocks bridge selector section.
+ messages.pgettext('openvpn-settings-view', 'Bridge mode')
+ }
+ values={options}
+ value={bridgeState}
+ onSelect={onSelectBridgeState}
+ />
+ </StyledSelectorContainer>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {bridgeModeFooterText(tunnelProtocol, transportProtocol)}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
<ModalAlert
- isOpen={this.state.showBridgeStateConfirmationDialog}
+ isOpen={confirmationDialogVisible}
type={ModalAlertType.info}
message={messages.gettext('This setting increases latency. Use only if needed.')}
buttons={[
- <AppButton.RedButton key="confirm" onClick={this.confirmBridgeState}>
+ <AppButton.RedButton key="confirm" onClick={confirmBridgeState}>
{messages.gettext('Enable anyway')}
</AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={this.hideBridgeStateConfirmationDialog}>
+ <AppButton.BlueButton key="back" onClick={hideConfirmationDialog}>
{messages.gettext('Back')}
</AppButton.BlueButton>,
]}
- close={this.hideBridgeStateConfirmationDialog}></ModalAlert>
+ close={hideConfirmationDialog}
+ />
+ </>
+ );
+}
+
+function bridgeModeFooterText(tunnelProtocol?: TunnelProtocol, transportProtocol?: RelayProtocol) {
+ if (tunnelProtocol !== 'openvpn') {
+ return formatMarkdown(
+ sprintf(
+ // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
+ // TRANSLATORS: available.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(tunnelProtocol)s - the name of the tunnel protocol setting
+ // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'To activate Bridge mode, go back and change **%(tunnelProtocol)s** to **%(openvpn)s**.',
+ ),
+ {
+ tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'),
+ openvpn: strings.openvpn,
+ },
+ ),
);
- };
+ } else if (transportProtocol === 'udp') {
+ return formatMarkdown(
+ sprintf(
+ // TRANSLATORS: This is used to instruct users how to make the bridge mode setting
+ // TRANSLATORS: available.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(transportProtocol)s - the name of the transport protocol setting
+ // TRANSLATORS: %(automat)s - the translation of "Automatic"
+ // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'To activate Bridge mode, change **%(transportProtocol)s** to **%(automatic)s** or **%(tcp)s**.',
+ ),
+ {
+ transportProtocol: messages.pgettext('openvpn-settings-view', 'Transport protocol'),
+ automatic: messages.gettext('Automatic'),
+ tcp: messages.gettext('TCP'),
+ },
+ ),
+ );
+ } else {
+ return sprintf(
+ // TRANSLATORS: This is used as a description for the bridge mode
+ // TRANSLATORS: setting.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'Helps circumvent censorship, by routing your traffic through a bridge server before reaching an %(openvpn)s server. Obfuscation is added to make fingerprinting harder.',
+ ),
+ { openvpn: strings.openvpn },
+ );
+ }
+}
- private onSelectBridgeState = (newValue: BridgeState) => {
- if (newValue === 'on') {
- this.setState({ showBridgeStateConfirmationDialog: true });
- } else {
- this.props.setBridgeState(newValue);
- }
- };
+function removeNonNumericCharacters(value: string) {
+ return value.replace(/[^0-9]/g, '');
+}
- private hideBridgeStateConfirmationDialog = () => {
- this.setState({ showBridgeStateConfirmationDialog: false });
- };
+function mssfixIsValid(mssfix: string): boolean {
+ const parsedMssFix = mssfix ? parseInt(mssfix) : undefined;
+ return (
+ parsedMssFix === undefined ||
+ (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE)
+ );
+}
- private confirmBridgeState = () => {
- this.setState({ showBridgeStateConfirmationDialog: false });
- this.props.setBridgeState('on');
- };
+function MssFixSetting() {
+ const { setOpenVpnMssfix: setOpenVpnMssfixImpl } = useAppContext();
+ const mssfix = useSelector((state) => state.settings.openVpn.mssfix);
+
+ const setOpenVpnMssfix = useCallback(
+ async (mssfix?: number) => {
+ try {
+ await setOpenVpnMssfixImpl(mssfix);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update mssfix value', error.message);
+ }
+ },
+ [setOpenVpnMssfixImpl],
+ );
+
+ const onMssfixSubmit = useCallback(
+ async (value: string) => {
+ const parsedValue = value === '' ? undefined : parseInt(value, 10);
+ if (mssfixIsValid(value)) {
+ await setOpenVpnMssfix(parsedValue);
+ }
+ },
+ [setOpenVpnMssfix],
+ );
+
+ return (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>{messages.pgettext('openvpn-settings-view', 'Mssfix')}</Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.AutoSizingTextInput
+ value={mssfix ? mssfix.toString() : ''}
+ inputMode={'numeric'}
+ maxLength={4}
+ placeholder={messages.gettext('Default')}
+ onSubmitValue={onMssfixSubmit}
+ validateValue={mssfixIsValid}
+ submitOnBlur={true}
+ modifyValue={removeNonNumericCharacters}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {sprintf(
+ // TRANSLATORS: The hint displayed below the Mssfix input field.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(openvpn)s - will be replaced with "OpenVPN"
+ // TRANSLATORS: %(max)d - the maximum possible mssfix value
+ // TRANSLATORS: %(min)d - the minimum possible mssfix value
+ messages.pgettext(
+ 'openvpn-settings-view',
+ 'Set %(openvpn)s MSS value. Valid range: %(min)d - %(max)d.',
+ ),
+ {
+ openvpn: strings.openvpn,
+ min: MIN_MSSFIX_VALUE,
+ max: MAX_MSSFIX_VALUE,
+ },
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ );
}
diff --git a/gui/src/renderer/containers/OpenVPNSettingsPage.tsx b/gui/src/renderer/containers/OpenVPNSettingsPage.tsx
deleted file mode 100644
index ab42f4a137..0000000000
--- a/gui/src/renderer/containers/OpenVPNSettingsPage.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-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, { BridgeModeAvailability } 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);
-
- let bridgeModeAvailablity = BridgeModeAvailability.available;
- if (mapRelaySettingsToProtocol(state.settings.relaySettings) !== 'openvpn') {
- bridgeModeAvailablity = BridgeModeAvailability.blockedDueToTunnelProtocol;
- } else if (protocolAndPort.openvpn.protocol === 'udp') {
- bridgeModeAvailablity = BridgeModeAvailability.blockedDueToTransportProtocol;
- }
-
- return {
- bridgeModeAvailablity,
- mssfix: state.settings.openVpn.mssfix,
- bridgeState: state.settings.bridgeState,
- ...protocolAndPort,
- };
-};
-
-const mapRelaySettingsToProtocol = (relaySettings: RelaySettingsRedux) => {
- if ('normal' in relaySettings) {
- 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 undefined;
- } else {
- throw new Error('Unknown type of relay settings.');
- }
-};
-
-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) {
- const error = e as Error;
- log.error('Failed to update relay settings', error.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) {
- const error = e as Error;
- log.error('Failed to update relay settings', error.message);
- }
- },
-
- setBridgeState: async (bridgeState: BridgeState) => {
- try {
- await props.app.setBridgeState(bridgeState);
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to update bridge state: ${error.message}`);
- }
- },
-
- setOpenVpnMssfix: async (mssfix?: number) => {
- try {
- await props.app.setOpenVpnMssfix(mssfix);
- } catch (e) {
- const error = e as Error;
- log.error('Failed to update mssfix value', error.message);
- }
- },
- };
-};
-
-export default withAppContext(
- withHistory(connect(mapStateToProps, mapDispatchToProps)(OpenVPNSettings)),
-);