diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-11-03 17:12:25 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-11-16 17:19:49 +0100 |
| commit | d698175c9b521c1af805a22bec08c518efd21759 (patch) | |
| tree | 479dbc696fa7fe223061575ccc445a8f5c8692f5 /gui/src | |
| parent | fbf3cd5485cc7f0669b1bba9480bf4bf88e11208 (diff) | |
| download | mullvadvpn-d698175c9b521c1af805a22bec08c518efd21759.tar.xz mullvadvpn-d698175c9b521c1af805a22bec08c518efd21759.zip | |
Add custom DNS setting
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 160 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettingsStyles.tsx | 28 | ||||
| -rw-r--r-- | gui/src/renderer/containers/AdvancedSettingsPage.tsx | 13 |
3 files changed, 195 insertions, 6 deletions
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index 239b5206cd..abf82b79b3 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -1,10 +1,17 @@ import * as React from 'react'; import { sprintf } from 'sprintf-js'; -import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types'; +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 { WgKeyState } from '../redux/settings/reducers'; import { - StyledBottomCellGroup, + StyledButtonCellGroup, StyledContainer, StyledInputFrame, StyledNavigationScrollbars, @@ -13,10 +20,15 @@ import { StyledSelectorContainer, StyledTunnelProtocolSelector, 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 { @@ -28,6 +40,7 @@ import { } 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; @@ -59,6 +72,7 @@ interface IProps { mssfix?: number; wireguardMtu?: number; bridgeState: BridgeState; + dns: IDnsOptions; setBridgeState: (value: BridgeState) => void; setEnableIpv6: (value: boolean) => void; setBlockWhenDisconnected: (value: boolean) => void; @@ -67,6 +81,7 @@ interface IProps { setWireguardMtu: (value: number | undefined) => void; setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void; setWireguardRelayPort: (port?: number) => void; + setDnsOptions: (dns: IDnsOptions) => Promise<void>; onViewWireguardKeys: () => void; onViewLinuxSplitTunneling: () => void; onClose: () => void; @@ -74,13 +89,21 @@ interface IProps { interface IState { showConfirmBlockWhenDisconnectedAlert: boolean; + showAddCustomDns: boolean; + invalidDnsIp: boolean; } export default class AdvancedSettings extends React.Component<IProps, IState> { public state = { showConfirmBlockWhenDisconnectedAlert: false, + showAddCustomDns: false, + invalidDnsIp: false, }; + private customDnsSwitchRef = React.createRef<HTMLDivElement>(); + private customDnsAddButtonRef = React.createRef<HTMLButtonElement>(); + private customDnsInputContainerRef = React.createRef<HTMLDivElement>(); + private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> }; private protocolItems: Array<ISelectorItem<OptionalRelayProtocol>>; private bridgeStateItems: Array<ISelectorItem<BridgeState>>; @@ -395,7 +418,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { </Cell.Footer> </AriaInputGroup> - <StyledBottomCellGroup> + <StyledButtonCellGroup> <Cell.CellButton onClick={this.props.onViewWireguardKeys}> <Cell.Label> {messages.pgettext('advanced-settings-view', 'WireGuard key')} @@ -411,7 +434,61 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> )} - </StyledBottomCellGroup> + </StyledButtonCellGroup> + + <StyledCustomDnsSwitchContainer> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'Use custom DNS server')} + </Cell.InputLabel> + <Cell.Switch + ref={this.customDnsSwitchRef} + isOn={this.props.dns.custom} + onChange={this.setCustomDnsEnabled} + /> + </StyledCustomDnsSwitchContainer> + <Accordion expanded={this.props.dns.custom}> + <CellList items={this.customDnsItems()} onRemove={this.removeDnsAddress} /> + + {this.state.showAddCustomDns && ( + <div ref={this.customDnsInputContainerRef}> + <Cell.RowInput + onSubmit={this.addDnsAddress} + onChange={this.addDnsInputChange} + invalid={this.state.invalidDnsIp} + paddingLeft={32} + onBlur={this.customDnsInputBlur} + autofocus + /> + </div> + )} + + <StyledAddCustomDnsButton + ref={this.customDnsAddButtonRef} + onClick={this.showAddCustomDnsRow} + disabled={this.state.showAddCustomDns} + tabIndex={-1}> + <StyledAddCustomDnsLabel tabIndex={-1}> + {messages.pgettext('advanced-settings-view', 'Add a server')} + </StyledAddCustomDnsLabel> + <Cell.UntintedIcon + source="icon-add" + width={22} + height={22} + tintColor={colors.white60} + tintHoverColor={colors.white80} + tabIndex={-1} + /> + </StyledAddCustomDnsButton> + </Accordion> + + <StyledCustomDnsFotter> + <Cell.FooterText> + {messages.pgettext( + 'advanced-settings-view', + 'Enable to add at least one DNS server.', + )} + </Cell.FooterText> + </StyledCustomDnsFotter> </StyledNavigationScrollbars> </NavigationContainer> </StyledContainer> @@ -423,6 +500,81 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { ); } + private setCustomDnsEnabled = async (enabled: boolean) => { + 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<string>[] { + 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<HTMLTextAreaElement>) => { + 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(false); + } + }; + + private hideAddCustomDnsRow(justAdded: boolean) { + this.setState({ showAddCustomDns: false }); + if (!justAdded && this.props.dns.addresses.length === 0) { + consumePromise(this.setCustomDnsEnabled(false)); + } + } + + private addDnsInputChange = (_value: string) => { + this.setState({ invalidDnsIp: false }); + }; + + private addDnsAddress = async (address: string) => { + try { + await this.props.setDnsOptions({ + custom: this.props.dns.custom, + addresses: [...this.props.dns.addresses, address], + }); + this.hideAddCustomDnsRow(true); + } 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<ISelectorItem<OptionalTunnelProtocol>> => { diff --git a/gui/src/renderer/components/AdvancedSettingsStyles.tsx b/gui/src/renderer/components/AdvancedSettingsStyles.tsx index 0bccc7c158..d26972ed1c 100644 --- a/gui/src/renderer/components/AdvancedSettingsStyles.tsx +++ b/gui/src/renderer/components/AdvancedSettingsStyles.tsx @@ -29,7 +29,7 @@ export const StyledNavigationScrollbars = styled(NavigationScrollbars)({ flex: 1, }); -export const StyledBottomCellGroup = styled.div({ +export const StyledButtonCellGroup = styled.div({ display: 'flex', flexDirection: 'column', flex: 1, @@ -44,3 +44,29 @@ export const StyledNoWireguardKeyError = styled(Cell.FooterText)({ fontWeight: 800, color: colors.red, }); + +export const StyledCustomDnsSwitchContainer = styled(Cell.Container)({ + marginBottom: '1px', +}); + +export const StyledCustomDnsFotter = styled(Cell.Footer)({ + marginBottom: '2px', +}); + +export const StyledAddCustomDnsButton = styled(Cell.CellButton)({ + backgroundColor: colors.blue40, +}); + +export const StyledAddCustomDnsLabel = styled(Cell.Label)( + {}, + (props: { paddingLeft?: number }) => ({ + fontFamily: 'Open Sans', + fontWeight: 'normal', + fontSize: '16px', + paddingLeft: (props.paddingLeft ?? 32) + 'px', + whiteSpace: 'pre-wrap', + overflowWrap: 'break-word', + width: '171px', + marginRight: '25px', + }), +); diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx index 6dec67cf4d..68d6899acb 100644 --- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx @@ -1,7 +1,12 @@ import log from 'electron-log'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; -import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types'; +import { + BridgeState, + IDnsOptions, + RelayProtocol, + TunnelProtocol, +} from '../../shared/daemon-rpc-types'; import RelaySettingsBuilder from '../../shared/relay-settings-builder'; import AdvancedSettings from '../components/AdvancedSettings'; @@ -19,6 +24,7 @@ const mapStateToProps = (state: IReduxState) => { mssfix: state.settings.openVpn.mssfix, wireguardMtu: state.settings.wireguard.mtu, bridgeState: state.settings.bridgeState, + dns: state.settings.dns, ...protocolAndPort, }; }; @@ -152,6 +158,11 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps log.error('Failed to update mtu value', e.message); } }, + + setDnsOptions: (dns: IDnsOptions) => { + return props.app.setDnsOptions(dns); + }, + onViewWireguardKeys: () => props.history.push('/settings/advanced/wireguard-keys'), onViewLinuxSplitTunneling: () => props.history.push('/settings/advanced/linux-split-tunneling'), }; |
