import * as React from 'react'; import { Component, Text, View } from 'reactxp'; import { sprintf } from 'sprintf-js'; import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { WgKeyState } from '../redux/settings/reducers'; import styles from './AdvancedSettingsStyles'; import * as Cell from './Cell'; import { Container, Layout } from './Layout'; import { BackBarItem, NavigationBar, NavigationContainer, NavigationItems, NavigationScrollbars, TitleBarItem, } from './NavigationBar'; import Selector, { ISelectorItem } from './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]; const WIREUGARD_UDP_PORTS = [53]; type OptionalPort = number | undefined; type OptionalRelayProtocol = RelayProtocol | undefined; type OptionalTunnelProtocol = TunnelProtocol | undefined; function mapPortToSelectorItem(value: number): ISelectorItem { 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; bridgeState: BridgeState; setBridgeState: (value: BridgeState) => void; setEnableIpv6: (value: boolean) => void; setBlockWhenDisconnected: (value: boolean) => void; setTunnelProtocol: (value: OptionalTunnelProtocol) => void; setOpenVpnMssfix: (value: number | undefined) => void; setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void; setWireguardRelayPort: (port?: number) => void; onViewWireguardKeys: () => void; onClose: () => void; } interface IState { persistedMssfix?: number; editedMssfix?: number; focusOnMssfix: boolean; } export default class AdvancedSettings extends Component { private portItems: { [key in RelayProtocol]: Array> }; private protocolItems: Array>; private bridgeStateItems: Array>; private wireguardPortItems: Array>; constructor(props: IProps) { super(props); const automaticPort: ISelectorItem = { 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.wireguardPortItems = [automaticPort].concat( WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem), ); 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', }, ]; this.state = { persistedMssfix: props.mssfix, editedMssfix: props.mssfix, focusOnMssfix: false, }; } public componentDidUpdate(_prevProps: IProps, _prevState: IState) { if (this.props.mssfix !== this.state.persistedMssfix) { this.setState((state, props) => ({ ...state, persistedMssfix: props.mssfix, editedMssfix: state.focusOnMssfix ? state.editedMssfix : props.mssfix, })); } } public render() { const mssfixStyle = this.mssfixIsValid() ? styles.advanced_settings__mssfix_valid_value : styles.advanced_settings__mssfix_invalid_value; const mssfixValue = this.state.editedMssfix; const hasWireguardKey = this.props.wireguardKeyState.type === 'key-set'; return ( {// TRANSLATORS: Back button in navigation bar messages.pgettext('navigation-bar', 'Settings')} {// TRANSLATORS: Title label in navigation bar messages.pgettext('advanced-settings-nav', 'Advanced')} {messages.pgettext('advanced-settings-view', 'Advanced')} {messages.pgettext('advanced-settings-view', 'Enable IPv6')} {messages.pgettext( 'advanced-settings-view', 'Enable IPv6 communication through the tunnel.', )} {messages.pgettext('advanced-settings-view', 'Block when disconnected')} {messages.pgettext( 'advanced-settings-view', "Unless connected to Mullvad, this setting will completely block your internet, even when you've disconnected or quit the app.", )} {this.props.blockWhenDisconnected && ( {messages.pgettext( 'advanced-settings-view', "Warning: Your internet won't work without a VPN connection, even when you've quit the app.", )} )} {!hasWireguardKey && ( {messages.pgettext( 'advanced-settings-view', 'To enable WireGuard, generate a key under the "WireGuard key" setting below.', )} )} {this.props.tunnelProtocol !== 'wireguard' ? ( {this.props.openvpn.protocol ? ( ) : ( undefined )} ) : ( undefined )} {this.props.tunnelProtocol === 'wireguard' ? ( ) : ( undefined )} {messages.pgettext('advanced-settings-view', 'Mssfix')} {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, }, )} {messages.pgettext('advanced-settings-view', 'WireGuard key')} ); } private tunnelProtocolItems = ( hasWireguardKey: boolean, ): Array> => { return [ { label: messages.pgettext('advanced-settings-view', 'Automatic'), 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'), }), value: 'wireguard', disabled: !hasWireguardKey, }, ]; }; 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 onMssfixChange = (mssfixString: string) => { const mssfix = mssfixString.replace(/[^0-9]/g, ''); if (mssfix === '') { this.setState({ editedMssfix: undefined }); } else { this.setState({ editedMssfix: parseInt(mssfix, 10) }); } }; private onMssfixFocus = () => { this.setState({ focusOnMssfix: true }); }; private onMssfixBlur = () => { this.setState({ focusOnMssfix: false }); if (this.mssfixIsValid()) { this.props.setOpenVpnMssfix(this.state.editedMssfix); this.setState((state, _props) => ({ persistedMssfix: state.editedMssfix })); } }; private mssfixIsValid(): boolean { const mssfix = this.state.editedMssfix; return mssfix === undefined || (mssfix >= MIN_MSSFIX_VALUE && mssfix <= MAX_MSSFIX_VALUE); } }