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 { messages } from '../../shared/gettext'; import consumePromise from '../../shared/promise'; import { IpAddress } from '../lib/ip'; import { WgKeyState } from '../redux/settings/reducers'; import { StyledButtonCellGroup, StyledContainer, StyledInputFrame, StyledNavigationScrollbars, StyledNoWireguardKeyError, StyledNoWireguardKeyErrorContainer, StyledSelectorContainer, StyledSelectorForFooter, StyledTunnelProtocolContainer, StyledCustomDnsSwitchContainer, StyledCustomDnsFotter, StyledAddCustomDnsLabel, StyledAddCustomDnsButton, } from './AdvancedSettingsStyles'; import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import CellList, { ICellListItem } from './cell/List'; import { Layout } from './Layout'; import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal'; import { BackBarItem, NavigationBar, NavigationContainer, NavigationItems, TitleBarItem, } from './NavigationBar'; import Selector, { ISelectorItem } from './cell/Selector'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import Accordion from './Accordion'; 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 { 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; onViewWireguardKeys: () => void; onViewLinuxSplitTunneling: () => void; onClose: () => void; } interface IState { showConfirmBlockWhenDisconnectedAlert: boolean; showAddCustomDns: boolean; invalidDnsIp: boolean; publicDnsIpToConfirm?: string; } export default class AdvancedSettings extends React.Component { public state = { showConfirmBlockWhenDisconnectedAlert: false, showAddCustomDns: false, invalidDnsIp: false, publicDnsIpToConfirm: undefined, }; private customDnsSwitchRef = React.createRef(); private customDnsAddButtonRef = React.createRef(); private customDnsInputContainerRef = React.createRef(); 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.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'; 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', 'Always require VPN')} {messages.pgettext( 'advanced-settings-view', 'If you disconnect or quit the app, this setting will block your internet.', )} {!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' ? ( { // 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.', ) } ) : undefined} {messages.pgettext('advanced-settings-view', 'OpenVPN 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 MTU')} {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, }, )} {messages.pgettext('advanced-settings-view', 'WireGuard key')} {window.platform === 'linux' && ( {messages.pgettext('advanced-settings-view', 'Split tunneling')} )} {messages.pgettext('advanced-settings-view', 'Use custom DNS server')} {this.state.showAddCustomDns && (
)} {messages.pgettext('advanced-settings-view', 'Add a server')}
{messages.pgettext( 'advanced-settings-view', 'Enable to add at least one DNS server.', )}
{this.state.showConfirmBlockWhenDisconnectedAlert && this.renderConfirmBlockWhenDisconnectedAlert()} {this.state.publicDnsIpToConfirm && this.renderCustomDnsConfirmationDialog()}
); } private setCustomDnsEnabled = async (enabled: boolean) => { if (this.props.dns.addresses.length > 0) { await this.props.setDnsOptions({ custom: enabled, addresses: this.props.dns.addresses, }); } if (enabled && this.props.dns.addresses.length === 0) { this.showAddCustomDnsRow(); } if (!enabled) { this.setState({ showAddCustomDns: false }); } }; private customDnsItems(): ICellListItem[] { return this.props.dns.addresses.map((address) => ({ label: address, value: address, })); } private showAddCustomDnsRow = () => { this.setState({ showAddCustomDns: true }); }; // The input field should be hidden when it loses focus unless something on the same row or the // add-button is the new focused element. private customDnsInputBlur = (event?: React.FocusEvent) => { const relatedTarget = event?.relatedTarget as Node | undefined; if ( relatedTarget && (this.customDnsSwitchRef.current?.contains(relatedTarget) || this.customDnsAddButtonRef.current?.contains(relatedTarget) || this.customDnsInputContainerRef.current?.contains(relatedTarget)) ) { event?.target.focus(); } else { this.hideAddCustomDnsRow(); } }; private hideAddCustomDnsRow() { if (!this.state.publicDnsIpToConfirm) { this.setState({ showAddCustomDns: false }); } } private addDnsInputChange = (_value: string) => { this.setState({ invalidDnsIp: false }); }; private hideCustomDnsConfirmationDialog = () => { this.setState({ publicDnsIpToConfirm: undefined }); }; private confirmPublicDnsAddress = () => { consumePromise(this.addDnsAddress(this.state.publicDnsIpToConfirm!, true)); this.hideCustomDnsConfirmationDialog(); }; private addDnsAddress = async (address: string, confirmed?: boolean) => { try { const ipAddress = IpAddress.fromString(address); if (ipAddress.isLocal() || confirmed) { await this.props.setDnsOptions({ custom: this.props.dns.custom || this.state.showAddCustomDns, addresses: [...this.props.dns.addresses, address], }); this.hideAddCustomDnsRow(); } else { this.setState({ publicDnsIpToConfirm: address }); } } catch (e) { this.setState({ invalidDnsIp: true }); } }; private removeDnsAddress = (address: string) => { const addresses = this.props.dns.addresses.filter((item) => item !== address); consumePromise( this.props.setDnsOptions({ custom: addresses.length > 0 && this.props.dns.custom, addresses, }), ); }; 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 renderCustomDnsConfirmationDialog = () => { return ( {messages.pgettext('advanced-settings-view', 'Add anyway')} , {messages.gettext('Back')} , ]} close={this.hideCustomDnsConfirmationDialog} message={messages.pgettext( 'advanced-settings-view', 'The DNS server you are trying to add might not work because it is public. Currently we only support local DNS servers.', )}> ); }; private renderConfirmBlockWhenDisconnectedAlert = () => { return ( {messages.pgettext('advanced-settings-view', 'Enable anyway')} , {messages.gettext('Back')} , ]} close={this.hideConfirmBlockWhenDisconnectedAlert}> {messages.pgettext( 'advanced-settings-view', 'Attention: enabling this will always require a Mullvad VPN connection in order to reach the internet.', )} {messages.pgettext( 'advanced-settings-view', 'The app’s built-in kill switch is always on. This setting will additionally block the internet if clicking Disconnect or Quit.', )} ); }; private setBlockWhenDisconnected = (newValue: boolean) => { if (newValue) { this.props.setBlockWhenDisconnected(true); this.setState({ showConfirmBlockWhenDisconnectedAlert: true }); } else { this.props.setBlockWhenDisconnected(false); } }; private hideConfirmBlockWhenDisconnectedAlert = () => { this.props.setBlockWhenDisconnected(false); this.setState({ showConfirmBlockWhenDisconnectedAlert: false }); }; private confirmEnableBlockWhenDisconnected = () => { this.props.setBlockWhenDisconnected(true); this.setState({ showConfirmBlockWhenDisconnectedAlert: false }); }; 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) ); } }