diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-11-17 11:09:35 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-11-17 11:09:35 +0100 |
| commit | 6d95d36bb37e73e65f7b4950993dc3e42d447a5c (patch) | |
| tree | ee2d4b9f0de9cd08615c396f7157baa603e9194e /gui/src | |
| parent | 765f777dd4399b334fe6641e5d427d379826501a (diff) | |
| parent | 6332e991b2bfaf334df03d93d5bd20df06ae699f (diff) | |
| download | mullvadvpn-6d95d36bb37e73e65f7b4950993dc3e42d447a5c.tar.xz mullvadvpn-6d95d36bb37e73e65f7b4950993dc3e42d447a5c.zip | |
Merge branch 'custom-dns-ui'
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 13 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 8 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 209 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettingsStyles.tsx | 28 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/Input.tsx | 143 | ||||
| -rw-r--r-- | gui/src/renderer/components/cell/List.tsx | 123 | ||||
| -rw-r--r-- | gui/src/renderer/containers/AdvancedSettingsPage.tsx | 13 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/actions.ts | 23 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 14 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 6 | ||||
| -rw-r--r-- | gui/src/shared/ipc-event-channel.ts | 6 |
12 files changed, 582 insertions, 10 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 130651c200..c7f4ce8608 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -48,6 +48,7 @@ import { ProxySettings, VoucherResponse, TunnelProtocol, + IDnsOptions, } from '../shared/daemon-rpc-types'; import * as managementInterface from './management_interface/management_interface_grpc_pb'; @@ -452,6 +453,14 @@ export class DaemonRpc { }; } + public async setDnsOptions(dns: IDnsOptions): Promise<void> { + const dnsOptions = new grpcTypes.DnsOptions(); + dnsOptions.setCustom(dns.custom); + dnsOptions.setAddressesList(dns.addresses); + + await this.call<grpcTypes.DnsOptions, Empty>(this.client.setDnsOptions, dnsOptions); + } + public async verifyWireguardKey(): Promise<boolean> { const response = await this.callEmpty<BoolValue>(this.client.verifyWireguardKey); return response.getValue(); @@ -1029,6 +1038,10 @@ function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObjec generic: { enableIpv6: tunnelOptions.generic!.enableIpv6, }, + dns: { + custom: tunnelOptions.dnsOptions?.custom ?? false, + addresses: tunnelOptions.dnsOptions?.addressesList ?? [], + }, }; } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 514fa4b4e8..d780e8e128 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -15,6 +15,7 @@ import { DaemonEvent, IAccountData, IAppVersionInfo, + IDnsOptions, ILocation, IRelayList, ISettings, @@ -137,6 +138,10 @@ class ApplicationMain { wireguard: { mtu: undefined, }, + dns: { + custom: false, + addresses: [], + }, }, }; private guiSettings = new GuiSettings(); @@ -984,6 +989,9 @@ class ApplicationMain { IpcMainEventChannel.settings.handleUpdateBridgeSettings((bridgeSettings: BridgeSettings) => { return this.daemonRpc.setBridgeSettings(bridgeSettings); }); + IpcMainEventChannel.settings.handleDnsOptions((dns: IDnsOptions) => { + return this.daemonRpc.setDnsOptions(dns); + }); IpcMainEventChannel.autoStart.handleSet((autoStart: boolean) => { return this.setAutoStart(autoStart); }); diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index d7d5b8bf29..17e117f739 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -32,6 +32,7 @@ import { BridgeState, IAccountData, IAppVersionInfo, + IDnsOptions, ILocation, IRelayList, ISettings, @@ -303,6 +304,10 @@ export default class AppRenderer { return IpcRendererEventChannel.settings.updateBridgeSettings(bridgeSettings); } + public setDnsOptions(dns: IDnsOptions) { + return IpcRendererEventChannel.settings.setDnsOptions(dns); + } + public removeAccountFromHistory(accountToken: AccountToken): Promise<void> { return IpcRendererEventChannel.accountHistory.removeItem(accountToken); } @@ -611,6 +616,7 @@ export default class AppRenderer { reduxSettings.updateOpenVpnMssfix(newSettings.tunnelOptions.openvpn.mssfix); reduxSettings.updateWireguardMtu(newSettings.tunnelOptions.wireguard.mtu); reduxSettings.updateBridgeState(newSettings.bridgeState); + reduxSettings.updateDnsOptions(newSettings.tunnelOptions.dns); this.setRelaySettings(newSettings.relaySettings); this.setBridgeSettings(newSettings.bridgeSettings); diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index 239b5206cd..6633e35746 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -1,10 +1,18 @@ +import ip from 'ip'; 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 +21,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 +41,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 +73,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 +82,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 +90,23 @@ interface IProps { interface IState { showConfirmBlockWhenDisconnectedAlert: boolean; + showAddCustomDns: boolean; + invalidDnsIp: boolean; + publicDnsIpToConfirm?: string; } export default class AdvancedSettings extends React.Component<IProps, IState> { public state = { showConfirmBlockWhenDisconnectedAlert: false, + showAddCustomDns: false, + invalidDnsIp: false, + publicDnsIpToConfirm: undefined, }; + 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 +421,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 +437,67 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> )} - </StyledBottomCellGroup> + </StyledButtonCellGroup> + + <StyledCustomDnsSwitchContainer> + <AriaInputGroup> + <AriaLabel> + <Cell.InputLabel> + {messages.pgettext('advanced-settings-view', 'Use custom DNS server')} + </Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.Switch + ref={this.customDnsSwitchRef} + isOn={this.props.dns.custom} + onChange={this.setCustomDnsEnabled} + /> + </AriaInput> + </AriaInputGroup> + </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.Icon + 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> @@ -419,10 +505,105 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { {this.state.showConfirmBlockWhenDisconnectedAlert && this.renderConfirmBlockWhenDisconnectedAlert()} + {this.state.publicDnsIpToConfirm && this.renderCustomDnsConfirmationDialog()} </ModalContainer> ); } + 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) { + if (!this.state.publicDnsIpToConfirm) { + 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 hideCustomDnsConfirmationDialog = () => { + this.setState({ publicDnsIpToConfirm: undefined }); + }; + + private confirmPublicDnsAddress = () => { + consumePromise(this.addDnsAddress(this.state.publicDnsIpToConfirm!, true)); + this.hideCustomDnsConfirmationDialog(); + }; + + private addDnsAddress = async (address: string, confirmed?: boolean) => { + if (ip.isV4Format(address) || ip.isV6Format(address)) { + if (ip.isPublic(address) && !confirmed) { + this.setState({ publicDnsIpToConfirm: address }); + } else { + 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 }); + } + } + } else { + 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>> => { @@ -448,6 +629,26 @@ export default class AdvancedSettings extends React.Component<IProps, IState> { ]; }; + private renderCustomDnsConfirmationDialog = () => { + return ( + <ModalAlert + type={ModalAlertType.info} + buttons={[ + <AppButton.RedButton key="confirm" onClick={this.confirmPublicDnsAddress}> + {messages.pgettext('advanced-settings-view', 'Add anyway')} + </AppButton.RedButton>, + <AppButton.BlueButton key="back" onClick={this.hideCustomDnsConfirmationDialog}> + {messages.gettext('Back')} + </AppButton.BlueButton>, + ]} + 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.', + )}></ModalAlert> + ); + }; + private renderConfirmBlockWhenDisconnectedAlert = () => { return ( <ModalAlert 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/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx index bfa47b6687..0ab5b4c493 100644 --- a/gui/src/renderer/components/cell/Input.tsx +++ b/gui/src/renderer/components/cell/Input.tsx @@ -1,9 +1,10 @@ -import React, { useCallback, useContext, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { colors } from '../../../config.json'; import { mediumText } from '../common-styles'; -import { CellDisabledContext } from './Container'; +import { CellDisabledContext, Container } from './Container'; import StandaloneSwitch from '../Switch'; +import ImageView from '../ImageView'; export const Switch = React.forwardRef(function SwitchT( props: StandaloneSwitch['props'], @@ -183,3 +184,141 @@ export function AutoSizingTextInput({ onChangeValue, ...otherProps }: IInputProp </StyledAutoSizingTextInputContainer> ); } + +const StyledCellInputRowContainer = styled(Container)({ + backgroundColor: 'white', + marginBottom: '1px', +}); + +const StyledSubmitButton = styled.button({ + border: 'none', + backgroundColor: 'transparent', + padding: '14px 0', +}); + +const StyledInputWrapper = styled.div({}, (props: { marginLeft: number }) => ({ + position: 'relative', + flex: 1, + width: '171px', + marginLeft: props.marginLeft + 'px', + marginRight: '25px', + lineHeight: '24px', + minHeight: '24px', + fontFamily: 'Open Sans', + fontWeight: 'normal', + fontSize: '16px', + padding: '14px 0', + maxWidth: '100%', +})); + +const StyledTextArea = styled.textarea({}, (props: { invalid?: boolean }) => ({ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: 'transparent', + border: 'none', + flex: 1, + lineHeight: '24px', + fontFamily: 'Open Sans', + fontWeight: 'normal', + fontSize: '16px', + resize: 'none', + padding: '14px 0', + color: props.invalid ? colors.red : 'auto', +})); + +const StyledInputFiller = styled.div({ + whiteSpace: 'pre-wrap', + overflowWrap: 'break-word', + minHeight: '24px', + color: 'transparent', +}); + +interface IRowInputProps { + onChange?: (value: string) => void; + onSubmit: (value: string) => void; + onFocus?: (event: React.FocusEvent<HTMLTextAreaElement>) => void; + onBlur?: (event?: React.FocusEvent<HTMLTextAreaElement>) => void; + paddingLeft?: number; + invalid?: boolean; + autofocus?: boolean; +} + +export function RowInput(props: IRowInputProps) { + const [value, setValue] = useState(''); + const textAreaRef = useRef() as React.RefObject<HTMLTextAreaElement>; + + const submit = useCallback(() => props.onSubmit(value), [props.onSubmit, value]); + const onChange = useCallback( + (event: React.ChangeEvent<HTMLTextAreaElement>) => { + const value = event.target.value; + setValue(value); + props.onChange?.(value); + }, + [props.onChange], + ); + const onKeyDown = useCallback( + (event: React.KeyboardEvent<HTMLTextAreaElement>) => { + if (event.key === 'Enter') { + event.preventDefault(); + submit(); + } + }, + [submit], + ); + + const globalKeyListener = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.stopPropagation(); + props.onBlur?.(); + } + }, + [props.onBlur], + ); + + useEffect(() => { + if (props.autofocus) { + textAreaRef.current?.focus(); + } + }, []); + + useEffect(() => { + if (props.invalid) { + textAreaRef.current?.focus(); + } + }, [props.invalid]); + + useEffect(() => { + document.addEventListener('keydown', globalKeyListener, true); + return () => document.removeEventListener('keydown', globalKeyListener, true); + }, []); + + return ( + <StyledCellInputRowContainer> + <StyledInputWrapper marginLeft={props.paddingLeft ?? 0}> + <StyledInputFiller>{value}</StyledInputFiller> + <StyledTextArea + ref={textAreaRef} + onChange={onChange} + onKeyDown={onKeyDown} + rows={1} + value={value} + invalid={props.invalid} + onFocus={props.onFocus} + onBlur={props.onBlur} + /> + </StyledInputWrapper> + <StyledSubmitButton onClick={submit}> + <ImageView + source="icon-tick" + height={22} + tintColor={colors.green} + tintHoverColor={colors.green90} + /> + </StyledSubmitButton> + </StyledCellInputRowContainer> + ); +} diff --git a/gui/src/renderer/components/cell/List.tsx b/gui/src/renderer/components/cell/List.tsx new file mode 100644 index 0000000000..cd99a052cb --- /dev/null +++ b/gui/src/renderer/components/cell/List.tsx @@ -0,0 +1,123 @@ +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import { colors } from '../../../config.json'; +import { messages } from '../../../shared/gettext'; +import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from '../AriaGroup'; +import ImageView from '../ImageView'; +import * as Cell from '.'; + +export interface ICellListItem<T> { + label: string; + value: T; +} + +interface ICellListProps<T> { + title?: string; + items: Array<ICellListItem<T>>; + onSelect?: (value: T) => void; + onRemove?: (value: T) => void; + className?: string; + paddingLeft?: number; +} + +export default function CellList<T>(props: ICellListProps<T>) { + const paddingLeft = props.paddingLeft ?? 32; + + return ( + <Cell.Section role="listbox" className={props.className}> + {props.title && <Cell.SectionTitle as="label">{props.title}</Cell.SectionTitle>} + {props.items.map((item, i) => { + return ( + <CellListItem + key={`${i}-${item.value}`} + value={item.value} + onSelect={props.onSelect} + onRemove={props.onRemove} + paddingLeft={paddingLeft}> + {item.label} + </CellListItem> + ); + })} + </Cell.Section> + ); +} + +const StyledContainer = styled(Cell.Container)({ + display: 'flex', + marginBottom: '1px', + backgroundColor: colors.blue40, +}); + +const StyledButton = styled.button({ + display: 'flex', + alignItems: 'center', + flex: 1, + border: 'none', + background: 'transparent', + padding: 0, + margin: 0, +}); + +const StyledLabel = styled(Cell.Label)({}, (props: { paddingLeft: number }) => ({ + fontFamily: 'Open Sans', + fontWeight: 'normal', + fontSize: '16px', + paddingLeft: props.paddingLeft + 'px', + whiteSpace: 'pre-wrap', + overflowWrap: 'break-word', + width: '171px', + marginRight: '25px', +})); + +const StyledRemoveButton = styled.button({ + background: 'transparent', + border: 'none', + padding: 0, +}); + +const StyledRemoveIcon = styled(ImageView)({ + [StyledRemoveButton + ':hover &']: { + backgroundColor: colors.white80, + }, +}); + +interface ICellListItemProps<T> { + value: T; + onSelect?: (application: T) => void; + onRemove?: (application: T) => void; + paddingLeft: number; + children: string; +} + +function CellListItem<T>(props: ICellListItemProps<T>) { + const onSelect = useCallback(() => props.onSelect?.(props.value), [props.onSelect, props.value]); + const onRemove = useCallback(() => props.onRemove?.(props.value), [props.onRemove, props.value]); + + return ( + <AriaDescriptionGroup> + <StyledContainer> + <StyledButton + onClick={props.onSelect ? onSelect : undefined} + as={props.onSelect ? 'button' : 'span'}> + <AriaDescription> + <StyledLabel paddingLeft={props.paddingLeft}>{props.children}</StyledLabel> + </AriaDescription> + </StyledButton> + {props.onRemove && ( + <AriaDescribed> + <StyledRemoveButton + onClick={onRemove} + aria-label={messages.pgettext('accessibility', 'Remove item')}> + <StyledRemoveIcon + source="icon-close" + width={22} + height={22} + tintColor={colors.white60} + /> + </StyledRemoveButton> + </AriaDescribed> + )} + </StyledContainer> + </AriaDescriptionGroup> + ); +} 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'), }; diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index d1f564bccd..6428badde8 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -1,4 +1,9 @@ -import { BridgeState, IWireguardPublicKey, KeygenEvent } from '../../../shared/daemon-rpc-types'; +import { + BridgeState, + IDnsOptions, + IWireguardPublicKey, + KeygenEvent, +} from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; import { BridgeSettingsRedux, IRelayLocationRedux, IWgKey, RelaySettingsRedux } from './reducers'; @@ -97,6 +102,11 @@ export interface IWireguardKeyVerifiedAction { verified?: boolean; } +export interface IUpdateDnsOptionsAction { + type: 'UPDATE_DNS_OPTIONS'; + dns: IDnsOptions; +} + export type SettingsAction = | IUpdateGuiSettingsAction | IUpdateRelayAction @@ -116,7 +126,8 @@ export type SettingsAction = | IWireguardGenerateKey | IWireguardReplaceKey | IWireguardKeygenEvent - | IWireguardKeyVerifiedAction; + | IWireguardKeyVerifiedAction + | IUpdateDnsOptionsAction; function updateGuiSettings(guiSettings: IGuiSettingsState): IUpdateGuiSettingsAction { return { @@ -261,6 +272,13 @@ function completeWireguardKeyVerification(verified?: boolean): IWireguardKeyVeri }; } +function updateDnsOptions(dns: IDnsOptions): IUpdateDnsOptionsAction { + return { + type: 'UPDATE_DNS_OPTIONS', + dns, + }; +} + export default { updateGuiSettings, updateRelay, @@ -281,4 +299,5 @@ export default { replaceWireguardKey, verifyWireguardKey, completeWireguardKeyVerification, + updateDnsOptions, }; diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 6f24e871b6..980eea6cd5 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -133,6 +133,10 @@ export interface ISettingsReduxState { wireguard: { mtu?: number; }; + dns: { + custom: boolean; + addresses: string[]; + }; wireguardKeyState: WgKeyState; } @@ -173,6 +177,10 @@ const initialState: ISettingsReduxState = { wireguardKeyState: { type: 'key-not-set', }, + dns: { + custom: false, + addresses: [], + }, }; export default function ( @@ -300,6 +308,12 @@ export default function ( }, }; + case 'UPDATE_DNS_OPTIONS': + return { + ...state, + dns: action.dns, + }; + default: return state; } diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index ecc04eddb6..bac978f7d6 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -262,6 +262,12 @@ export interface ITunnelOptions { generic: { enableIpv6: boolean; }; + dns: IDnsOptions; +} + +export interface IDnsOptions { + custom: boolean; + addresses: string[]; } export type ProxySettings = ILocalProxySettings | IRemoteProxySettings | IShadowsocksProxySettings; diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index 5cd1d22098..a7410dd22c 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -13,6 +13,7 @@ import { BridgeState, IAccountData, IAppVersionInfo, + IDnsOptions, ILocation, IRelayList, ISettings, @@ -78,6 +79,7 @@ interface ISettingsMethods extends IReceiver<ISettings> { setWireguardMtu(mtu?: number): Promise<void>; updateRelaySettings(update: RelaySettingsUpdate): Promise<void>; updateBridgeSettings(bridgeSettings: BridgeSettings): Promise<void>; + setDnsOptions(dns: IDnsOptions): Promise<void>; } interface ISettingsHandlers extends ISender<ISettings> { @@ -90,6 +92,7 @@ interface ISettingsHandlers extends ISender<ISettings> { handleWireguardMtu(fn: (mtu?: number) => Promise<void>): void; handleUpdateRelaySettings(fn: (update: RelaySettingsUpdate) => Promise<void>): void; handleUpdateBridgeSettings(fn: (bridgeSettings: BridgeSettings) => Promise<void>): void; + handleDnsOptions(fn: (dns: IDnsOptions) => Promise<void>): void; } interface IGuiSettingsMethods extends IReceiver<IGuiSettingsState> { @@ -188,6 +191,7 @@ const SET_OPENVPN_MSSFIX = 'set-openvpn-mssfix'; const SET_WIREGUARD_MTU = 'set-wireguard-mtu'; const UPDATE_RELAY_SETTINGS = 'update-relay-settings'; const UPDATE_BRIDGE_SETTINGS = 'update-bridge-location'; +const SET_DNS_OPTIONS = 'set-dns-options'; const LOCATION_CHANGED = 'location-changed'; const RELAYS_CHANGED = 'relays-changed'; @@ -275,6 +279,7 @@ export class IpcRendererEventChannel { setWireguardMtu: requestSender(SET_WIREGUARD_MTU), updateRelaySettings: requestSender(UPDATE_RELAY_SETTINGS), updateBridgeSettings: requestSender(UPDATE_BRIDGE_SETTINGS), + setDnsOptions: requestSender(SET_DNS_OPTIONS), }; public static location: IReceiver<ILocation> = { @@ -385,6 +390,7 @@ export class IpcMainEventChannel { handleWireguardMtu: requestHandler(SET_WIREGUARD_MTU), handleUpdateRelaySettings: requestHandler(UPDATE_RELAY_SETTINGS), handleUpdateBridgeSettings: requestHandler(UPDATE_BRIDGE_SETTINGS), + handleDnsOptions: requestHandler(SET_DNS_OPTIONS), }; public static relays: ISender<IRelayListPair> = { |
