import { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { strings } from '../../config.json'; import { IpVersion, liftConstraint, LiftedConstraint, ObfuscationType, wrapConstraint, } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import { removeNonNumericCharacters } from '../../shared/string-helpers'; import { useAppContext } from '../context'; import { useRelaySettingsUpdater } from '../lib/constraint-updater'; import { useHistory } from '../lib/history'; import { useBoolean } from '../lib/utilityHooks'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import Selector, { SelectorItem, SelectorWithCustomItem } from './cell/Selector'; import { InfoIcon } from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; import { NavigationBar, NavigationContainer, NavigationItems, NavigationScrollbars, TitleBarItem, } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; const MIN_WIREGUARD_MTU_VALUE = 1280; const MAX_WIREGUARD_MTU_VALUE = 1420; const WIREUGARD_UDP_PORTS = [51820, 53]; const UDP2TCP_PORTS = [80, 5001]; function mapPortToSelectorItem(value: number): SelectorItem { return { label: value.toString(), value }; } export const StyledContent = styled.div({ display: 'flex', flexDirection: 'column', flex: 1, marginBottom: '2px', }); export const StyledCellIcon = styled(Cell.UntintedIcon)({ marginRight: '8px', }); export const StyledInfoIcon = styled(InfoIcon)({ marginRight: '16px', }); export const StyledSelectorContainer = styled.div({ flex: 0, }); export default function WireguardSettings() { const { pop } = useHistory(); return ( {sprintf( // TRANSLATORS: Title label in navigation bar // TRANSLATORS: Available placeholders: // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" messages.pgettext('wireguard-settings-nav', '%(wireguard)s settings'), { wireguard: strings.wireguard }, )} {sprintf( // TRANSLATORS: Available placeholders: // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" messages.pgettext('wireguard-settings-view', '%(wireguard)s settings'), { wireguard: strings.wireguard }, )} ); } function PortSelector() { const relaySettings = useSelector((state) => state.settings.relaySettings); const relaySettingsUpdater = useRelaySettingsUpdater(); const allowedPortRanges = useSelector((state) => state.settings.wireguardEndpointData.portRanges); const wireguardPortItems = useMemo>>( () => WIREUGARD_UDP_PORTS.map(mapPortToSelectorItem), [], ); const port = useMemo(() => { const port = 'normal' in relaySettings ? relaySettings.normal.wireguard.port : 'any'; return port === 'any' ? null : port; }, [relaySettings]); const setWireguardPort = useCallback( async (port: number | null) => { try { await relaySettingsUpdater((settings) => { settings.wireguardConstraints.port = wrapConstraint(port); return settings; }); } catch (e) { const error = e as Error; log.error('Failed to update relay settings', error.message); } }, [relaySettingsUpdater], ); const parseValue = useCallback((port: string) => parseInt(port), []); const validateValue = useCallback( (value: number) => allowedPortRanges.some(([start, end]) => value >= start && value <= end), [allowedPortRanges], ); const portRangesText = allowedPortRanges .map(([start, end]) => (start === end ? start : `${start}-${end}`)) .join(', '); return ( {messages.pgettext( 'wireguard-settings-view', 'The automatic setting will randomly choose from the valid port ranges shown below.', )} {sprintf( messages.pgettext( 'wireguard-settings-view', 'The custom port can be any value inside the valid ranges: %(portRanges)s.', ), { portRanges: portRangesText }, )} } /> ); } function ObfuscationSettings() { const { setObfuscationSettings } = useAppContext(); const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings); const obfuscationType = obfuscationSettings.selectedObfuscation; const obfuscationTypeItems: SelectorItem[] = useMemo( () => [ { label: messages.pgettext('wireguard-settings-view', 'On (UDP-over-TCP)'), value: ObfuscationType.udp2tcp, }, { label: messages.gettext('Off'), value: ObfuscationType.off, }, ], [], ); const selectObfuscationType = useCallback( async (value: ObfuscationType) => { await setObfuscationSettings({ ...obfuscationSettings, selectedObfuscation: value, }); }, [setObfuscationSettings, obfuscationSettings], ); return ( {messages.pgettext( 'wireguard-settings-view', 'Obfuscation hides the WireGuard traffic inside another protocol. It can be used to help circumvent censorship and other types of filtering, where a plain WireGuard connect would be blocked.', )} } items={obfuscationTypeItems} value={obfuscationType} onSelect={selectObfuscationType} automaticValue={ObfuscationType.auto} /> ); } function Udp2tcpPortSetting() { const { setObfuscationSettings } = useAppContext(); const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings); const port = liftConstraint(obfuscationSettings.udp2tcpSettings.port); const portItems: SelectorItem[] = useMemo( () => UDP2TCP_PORTS.map(mapPortToSelectorItem), [], ); const expandableProps = useMemo(() => ({ expandable: true, id: 'udp2tcp-port' }), []); const selectPort = useCallback( async (port: LiftedConstraint) => { await setObfuscationSettings({ ...obfuscationSettings, udp2tcpSettings: { ...obfuscationSettings.udp2tcpSettings, port: wrapConstraint(port), }, }); }, [setObfuscationSettings, obfuscationSettings], ); return ( {messages.pgettext( 'wireguard-settings-view', 'Which TCP port the UDP-over-TCP obfuscation protocol should connect to on the VPN server.', )} } items={portItems} value={port} onSelect={selectPort} disabled={obfuscationSettings.selectedObfuscation === ObfuscationType.off} expandable={expandableProps} thinTitle automaticValue={'any' as const} /> ); } function MultihopSetting() { const relaySettings = useSelector((state) => state.settings.relaySettings); const relaySettingsUpdater = useRelaySettingsUpdater(); const multihop = 'normal' in relaySettings ? relaySettings.normal.wireguard.useMultihop : false; const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(); const setMultihopImpl = useCallback( async (enabled: boolean) => { try { await relaySettingsUpdater((settings) => { settings.wireguardConstraints.useMultihop = enabled; return settings; }); } catch (e) { const error = e as Error; log.error('Failed to update WireGuard multihop settings', error.message); } }, [relaySettingsUpdater], ); const setMultihop = useCallback( async (newValue: boolean) => { if (newValue) { showConfirmationDialog(); } else { await setMultihopImpl(false); } }, [setMultihopImpl], ); const confirmMultihop = useCallback(async () => { await setMultihopImpl(true); hideConfirmationDialog(); }, [setMultihopImpl]); return ( <> { // TRANSLATORS: The label next to the multihop settings toggle. messages.pgettext('vpn-settings-view', 'Enable multihop') } {sprintf( // TRANSLATORS: Description for multihop settings toggle. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" messages.pgettext( 'vpn-settings-view', 'Increases anonymity by routing your traffic into one %(wireguard)s server and out another, making it harder to trace.', ), { wireguard: strings.wireguard }, )} {messages.gettext('Enable anyway')} , {messages.gettext('Back')} , ]} close={hideConfirmationDialog} /> ); } function IpVersionSetting() { const relaySettingsUpdater = useRelaySettingsUpdater(); const relaySettings = useSelector((state) => state.settings.relaySettings); const ipVersion = useMemo(() => { const ipVersion = 'normal' in relaySettings ? relaySettings.normal.wireguard.ipVersion : 'any'; return ipVersion === 'any' ? null : ipVersion; }, [relaySettings]); const ipVersionItems: SelectorItem[] = useMemo( () => [ { label: messages.gettext('IPv4'), value: 'ipv4', }, { label: messages.gettext('IPv6'), value: 'ipv6', }, ], [], ); const setIpVersion = useCallback( async (ipVersion: IpVersion | null) => { try { await relaySettingsUpdater((settings) => { settings.wireguardConstraints.ipVersion = wrapConstraint(ipVersion); return settings; }); } catch (e) { const error = e as Error; log.error('Failed to update relay settings', error.message); } }, [relaySettingsUpdater], ); return ( {sprintf( // TRANSLATORS: The hint displayed below the WireGuard IP version selector. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" messages.pgettext( 'wireguard-settings-view', 'This allows access to %(wireguard)s for devices that only support IPv6.', ), { wireguard: strings.wireguard }, )} ); } function mtuIsValid(mtu: string): boolean { const parsedMtu = mtu ? parseInt(mtu) : undefined; return ( parsedMtu === undefined || (parsedMtu >= MIN_WIREGUARD_MTU_VALUE && parsedMtu <= MAX_WIREGUARD_MTU_VALUE) ); } function MtuSetting() { const { setWireguardMtu: setWireguardMtuImpl } = useAppContext(); const mtu = useSelector((state) => state.settings.wireguard.mtu); const setMtu = useCallback( async (mtu?: number) => { try { await setWireguardMtuImpl(mtu); } catch (e) { const error = e as Error; log.error('Failed to update mtu value', error.message); } }, [setWireguardMtuImpl], ); const onSubmit = useCallback( async (value: string) => { const parsedValue = value === '' ? undefined : parseInt(value, 10); if (mtuIsValid(value)) { await setMtu(parsedValue); } }, [setMtu], ); return ( {messages.pgettext('wireguard-settings-view', 'MTU')} {sprintf( // TRANSLATORS: The hint displayed below the WireGuard MTU input field. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(wireguard)s - Will be replaced with the string "WireGuard" // 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)s MTU value. Valid range: %(min)d - %(max)d.', ), { wireguard: strings.wireguard, min: MIN_WIREGUARD_MTU_VALUE, max: MAX_WIREGUARD_MTU_VALUE, }, )} ); } function QuantumResistantSetting() { const { setWireguardQuantumResistant } = useAppContext(); const quantumResistant = useSelector((state) => state.settings.wireguard.quantumResistant); const items: SelectorItem[] = useMemo( () => [ { label: messages.gettext('On'), value: true, }, { label: messages.gettext('Off'), value: false, }, ], [], ); const selectQuantumResistant = useCallback( async (quantumResistant: boolean | null) => { await setWireguardQuantumResistant(quantumResistant ?? undefined); }, [setWireguardQuantumResistant], ); return ( {messages.pgettext( 'wireguard-settings-view', 'This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers.', )} {messages.pgettext( 'wireguard-settings-view', 'It does this by performing an extra key exchange using a quantum safe algorithm and mixing the result into WireGuard’s regular encryption. This extra step uses approximately 500 kiB of traffic every time a new tunnel is established.', )} } items={items} value={quantumResistant ?? null} onSelect={selectQuantumResistant} automaticValue={null} /> ); }