summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-11-17 11:09:35 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-11-17 11:09:35 +0100
commit6d95d36bb37e73e65f7b4950993dc3e42d447a5c (patch)
treeee2d4b9f0de9cd08615c396f7157baa603e9194e /gui/src
parent765f777dd4399b334fe6641e5d427d379826501a (diff)
parent6332e991b2bfaf334df03d93d5bd20df06ae699f (diff)
downloadmullvadvpn-6d95d36bb37e73e65f7b4950993dc3e42d447a5c.tar.xz
mullvadvpn-6d95d36bb37e73e65f7b4950993dc3e42d447a5c.zip
Merge branch 'custom-dns-ui'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/daemon-rpc.ts13
-rw-r--r--gui/src/main/index.ts8
-rw-r--r--gui/src/renderer/app.tsx6
-rw-r--r--gui/src/renderer/components/AdvancedSettings.tsx209
-rw-r--r--gui/src/renderer/components/AdvancedSettingsStyles.tsx28
-rw-r--r--gui/src/renderer/components/cell/Input.tsx143
-rw-r--r--gui/src/renderer/components/cell/List.tsx123
-rw-r--r--gui/src/renderer/containers/AdvancedSettingsPage.tsx13
-rw-r--r--gui/src/renderer/redux/settings/actions.ts23
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts14
-rw-r--r--gui/src/shared/daemon-rpc-types.ts6
-rw-r--r--gui/src/shared/ipc-event-channel.ts6
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> = {