import { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { colors, strings } from '../../config.json'; import { IDnsOptions, TunnelProtocol, wrapConstraint } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import { useAppContext } from '../context'; import { useRelaySettingsUpdater } from '../lib/constraint-updater'; import { useHistory } from '../lib/history'; import { formatHtml } from '../lib/html-formatter'; import { RoutePath } from '../lib/routes'; import { useBoolean } from '../lib/utilityHooks'; import { RelaySettingsRedux } from '../redux/settings/reducers'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { AriaDescription, AriaDetails, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import Selector, { SelectorItem } from './cell/Selector'; import CustomDnsSettings from './CustomDnsSettings'; import InfoButton, { 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 StyledContent = styled.div({ display: 'flex', flexDirection: 'column', flex: 1, marginBottom: '2px', }); const StyledInfoIcon = styled(InfoIcon)({ marginRight: '16px', }); const StyledSelectorContainer = styled.div({ flex: 0, }); const StyledTitleLabel = styled(Cell.SectionTitle)({ flex: 1, }); const StyledSectionItem = styled(Cell.Container)({ backgroundColor: colors.blue40, }); const LanIpRanges = styled.ul({ listStyle: 'disc outside', marginLeft: '20px', }); export default function VpnSettings() { const { pop } = useHistory(); return ( { // TRANSLATORS: Title label in navigation bar messages.pgettext('vpn-settings-view', 'VPN settings') } {messages.pgettext('vpn-settings-view', 'VPN settings')} ); } function AutoStart() { const autoStart = useSelector((state) => state.settings.autoStart); const { setAutoStart: setAutoStartImpl } = useAppContext(); const setAutoStart = useCallback( async (autoStart: boolean) => { try { await setAutoStartImpl(autoStart); } catch (e) { const error = e as Error; log.error(`Cannot set auto-start: ${error.message}`); } }, [setAutoStartImpl], ); return ( {messages.pgettext('vpn-settings-view', 'Launch app on start-up')} ); } function AutoConnect() { const autoConnect = useSelector((state) => state.settings.guiSettings.autoConnect); const { setAutoConnect } = useAppContext(); return ( {messages.pgettext('vpn-settings-view', 'Auto-connect')} {messages.pgettext( 'vpn-settings-view', 'Automatically connect to a server when the app launches.', )} ); } function AllowLan() { const allowLan = useSelector((state) => state.settings.allowLan); const { setAllowLan } = useAppContext(); return ( {messages.pgettext('vpn-settings-view', 'Local network sharing')} {messages.pgettext( 'vpn-settings-view', 'This feature allows access to other devices on the local network, such as for sharing, printing, streaming, etc.', )} {messages.pgettext( 'vpn-settings-view', 'It does this by allowing network communication outside the tunnel to local multicast and broadcast ranges as well as to and from these private IP ranges:', )}
  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • 169.254.0.0/16
  • 0xfe80::/10
  • 0xfc00::/7
  • ); } function useDns(setting: keyof IDnsOptions['defaultOptions']) { const dns = useSelector((state) => state.settings.dns); const { setDnsOptions } = useAppContext(); const updateBlockSetting = useCallback( (enabled: boolean) => setDnsOptions({ ...dns, defaultOptions: { ...dns.defaultOptions, [setting]: enabled, }, }), [dns, setDnsOptions], ); return [dns, updateBlockSetting] as const; } function DnsBlockers() { const dns = useSelector((state) => state.settings.dns); const title = ( <> {messages.pgettext('vpn-settings-view', 'DNS content blockers')} {messages.pgettext( 'vpn-settings-view', 'When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more.', )} {messages.pgettext( 'vpn-settings-view', 'This might cause issues on certain websites, services, and programs.', )} ); return ( ); } function BlockAds() { const [dns, setBlockAds] = useDns('blockAds'); return ( { // TRANSLATORS: Label for settings that enables ad blocking. messages.pgettext('vpn-settings-view', 'Ads') } ); } function BlockTrackers() { const [dns, setBlockTrackers] = useDns('blockTrackers'); return ( { // TRANSLATORS: Label for settings that enables tracker blocking. messages.pgettext('vpn-settings-view', 'Trackers') } ); } function BlockMalware() { const [dns, setBlockMalware] = useDns('blockMalware'); return ( { // TRANSLATORS: Label for settings that enables malware blocking. messages.pgettext('vpn-settings-view', 'Malware') } {messages.pgettext( 'vpn-settings-view', 'Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.', )} ); } function BlockGambling() { const [dns, setBlockGambling] = useDns('blockGambling'); return ( { // TRANSLATORS: Label for settings that enables block of gamling related websites. messages.pgettext('vpn-settings-view', 'Gambling') } ); } function BlockAdultContent() { const [dns, setBlockAdultContent] = useDns('blockAdultContent'); return ( { // TRANSLATORS: Label for settings that enables block of adult content. messages.pgettext('vpn-settings-view', 'Adult content') } ); } function BlockSocialMedia() { const [dns, setBlockSocialMedia] = useDns('blockSocialMedia'); return ( { // TRANSLATORS: Label for settings that enables block of social media. messages.pgettext('vpn-settings-view', 'Social media') } {dns.state === 'custom' && } ); } function CustomDnsEnabledFooter() { const customDnsFeatureName = messages.pgettext('vpn-settings-view', 'Use custom DNS server'); // TRANSLATORS: This is displayed when the custom DNS setting is turned on which makes the block // TRANSLATORS: ads/trackers settings disabled. The text enclosed in "" will appear bold. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(customDnsFeatureName)s - The name displayed next to the custom DNS toggle. const blockingDisabledText = messages.pgettext( 'vpn-settings-view', 'Disable %(customDnsFeatureName)s below to activate these settings.', ); return ( {formatHtml(sprintf(blockingDisabledText, { customDnsFeatureName }))} ); } function EnableIpv6() { const enableIpv6 = useSelector((state) => state.settings.enableIpv6); const { setEnableIpv6: setEnableIpv6Impl } = useAppContext(); const setEnableIpv6 = useCallback( async (enableIpv6: boolean) => { try { await setEnableIpv6Impl(enableIpv6); } catch (e) { const error = e as Error; log.error('Failed to update enable IPv6', error.message); } }, [setEnableIpv6Impl], ); return ( {messages.pgettext('vpn-settings-view', 'Enable IPv6')} {messages.pgettext( 'vpn-settings-view', 'When this feature is enabled, IPv6 can be used alongside IPv4 in the VPN tunnel to communicate with internet services.', )} {messages.pgettext( 'vpn-settings-view', 'IPv4 is always enabled and the majority of websites and applications use this protocol. We do not recommend enabling IPv6 unless you know you need it.', )} ); } function KillSwitchInfo() { const [killSwitchInfoVisible, showKillSwitchInfo, hideKillSwitchInfo] = useBoolean(false); return ( <> {messages.pgettext('vpn-settings-view', 'Kill switch')} {messages.gettext('Got it!')} , ]} close={hideKillSwitchInfo}> {messages.pgettext( 'vpn-settings-view', 'This built-in feature prevents your traffic from leaking outside of the VPN tunnel if your network suddenly stops working or if the tunnel fails, it does this by blocking your traffic until your connection is reestablished.', )} {messages.pgettext( 'vpn-settings-view', 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents. With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.', )} ); } function LockdownMode() { const blockWhenDisconnected = useSelector((state) => state.settings.blockWhenDisconnected); const { setBlockWhenDisconnected: setBlockWhenDisconnectedImpl } = useAppContext(); const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean( false, ); const setBlockWhenDisconnected = useCallback( async (blockWhenDisconnected: boolean) => { try { await setBlockWhenDisconnectedImpl(blockWhenDisconnected); } catch (e) { const error = e as Error; log.error('Failed to update block when disconnected', error.message); } }, [setBlockWhenDisconnectedImpl], ); const setLockDownMode = useCallback( async (newValue: boolean) => { if (newValue) { showConfirmationDialog(); } else { await setBlockWhenDisconnected(false); } }, [setBlockWhenDisconnected, showConfirmationDialog], ); const confirmLockdownMode = useCallback(async () => { hideConfirmationDialog(); await setBlockWhenDisconnected(true); }, [hideConfirmationDialog, setBlockWhenDisconnected]); return ( <> {messages.pgettext('vpn-settings-view', 'Lockdown mode')} {messages.pgettext( 'vpn-settings-view', 'The difference between the Kill Switch and Lockdown Mode is that the Kill Switch will prevent any leaks from happening during automatic tunnel reconnects, software crashes and similar accidents.', )} {messages.pgettext( 'vpn-settings-view', 'With Lockdown Mode enabled, you must be connected to a Mullvad VPN server to be able to reach the internet. Manually disconnecting or quitting the app will block your connection.', )} {messages.gettext('Enable anyway')} , {messages.gettext('Back')} , ]} close={hideConfirmationDialog}> {messages.pgettext( 'vpn-settings-view', 'Attention: enabling this will always require a Mullvad VPN connection in order to reach the internet.', )} {messages.pgettext( 'vpn-settings-view', 'The app’s built-in kill switch is always on. This setting will additionally block the internet if clicking Disconnect or Quit.', )} ); } function TunnelProtocolSetting() { const tunnelProtocol = useSelector((state) => mapRelaySettingsToProtocol(state.settings.relaySettings), ); const relaySettingsUpdater = useRelaySettingsUpdater(); const setTunnelProtocol = useCallback( async (tunnelProtocol: TunnelProtocol | null) => { try { await relaySettingsUpdater((settings) => ({ ...settings, tunnelProtocol: wrapConstraint(tunnelProtocol), })); } catch (e) { const error = e as Error; log.error('Failed to update tunnel protocol constraints', error.message); } }, [relaySettingsUpdater], ); const tunnelProtocolItems: Array> = useMemo( () => [ { label: strings.wireguard, value: 'wireguard', }, { label: strings.openvpn, value: 'openvpn', }, ], [], ); return ( ); } function 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.'); } } function WireguardSettingsButton() { const history = useHistory(); const tunnelProtocol = useSelector((state) => mapRelaySettingsToProtocol(state.settings.relaySettings), ); const navigate = useCallback(() => history.push(RoutePath.wireguardSettings), [history]); return ( {sprintf( // TRANSLATORS: %(wireguard)s will be replaced with the string "WireGuard" messages.pgettext('vpn-settings-view', '%(wireguard)s settings'), { wireguard: strings.wireguard }, )} ); } function OpenVpnSettingsButton() { const history = useHistory(); const tunnelProtocol = useSelector((state) => mapRelaySettingsToProtocol(state.settings.relaySettings), ); const navigate = useCallback(() => history.push(RoutePath.openVpnSettings), [history]); return ( {sprintf( // TRANSLATORS: %(openvpn)s will be replaced with the string "OpenVPN" messages.pgettext('vpn-settings-view', '%(openvpn)s settings'), { openvpn: strings.openvpn }, )} ); }