Skip to content

Commit d698175

Browse files
committed
Add custom DNS setting
1 parent fbf3cd5 commit d698175

File tree

3 files changed

+195
-6
lines changed

3 files changed

+195
-6
lines changed

gui/src/renderer/components/AdvancedSettings.tsx

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import * as React from 'react';
22
import { sprintf } from 'sprintf-js';
3-
import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types';
3+
import { colors } from '../../config.json';
4+
import {
5+
BridgeState,
6+
IDnsOptions,
7+
RelayProtocol,
8+
TunnelProtocol,
9+
} from '../../shared/daemon-rpc-types';
410
import { messages } from '../../shared/gettext';
11+
import consumePromise from '../../shared/promise';
512
import { WgKeyState } from '../redux/settings/reducers';
613
import {
7-
StyledBottomCellGroup,
14+
StyledButtonCellGroup,
815
StyledContainer,
916
StyledInputFrame,
1017
StyledNavigationScrollbars,
@@ -13,10 +20,15 @@ import {
1320
StyledSelectorContainer,
1421
StyledTunnelProtocolSelector,
1522
StyledTunnelProtocolContainer,
23+
StyledCustomDnsSwitchContainer,
24+
StyledCustomDnsFotter,
25+
StyledAddCustomDnsLabel,
26+
StyledAddCustomDnsButton,
1627
} from './AdvancedSettingsStyles';
1728
import * as AppButton from './AppButton';
1829
import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup';
1930
import * as Cell from './cell';
31+
import CellList, { ICellListItem } from './cell/List';
2032
import { Layout } from './Layout';
2133
import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal';
2234
import {
@@ -28,6 +40,7 @@ import {
2840
} from './NavigationBar';
2941
import Selector, { ISelectorItem } from './cell/Selector';
3042
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
43+
import Accordion from './Accordion';
3144

3245
const MIN_MSSFIX_VALUE = 1000;
3346
const MAX_MSSFIX_VALUE = 1450;
@@ -59,6 +72,7 @@ interface IProps {
5972
mssfix?: number;
6073
wireguardMtu?: number;
6174
bridgeState: BridgeState;
75+
dns: IDnsOptions;
6276
setBridgeState: (value: BridgeState) => void;
6377
setEnableIpv6: (value: boolean) => void;
6478
setBlockWhenDisconnected: (value: boolean) => void;
@@ -67,20 +81,29 @@ interface IProps {
6781
setWireguardMtu: (value: number | undefined) => void;
6882
setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void;
6983
setWireguardRelayPort: (port?: number) => void;
84+
setDnsOptions: (dns: IDnsOptions) => Promise<void>;
7085
onViewWireguardKeys: () => void;
7186
onViewLinuxSplitTunneling: () => void;
7287
onClose: () => void;
7388
}
7489

7590
interface IState {
7691
showConfirmBlockWhenDisconnectedAlert: boolean;
92+
showAddCustomDns: boolean;
93+
invalidDnsIp: boolean;
7794
}
7895

7996
export default class AdvancedSettings extends React.Component<IProps, IState> {
8097
public state = {
8198
showConfirmBlockWhenDisconnectedAlert: false,
99+
showAddCustomDns: false,
100+
invalidDnsIp: false,
82101
};
83102

103+
private customDnsSwitchRef = React.createRef<HTMLDivElement>();
104+
private customDnsAddButtonRef = React.createRef<HTMLButtonElement>();
105+
private customDnsInputContainerRef = React.createRef<HTMLDivElement>();
106+
84107
private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> };
85108
private protocolItems: Array<ISelectorItem<OptionalRelayProtocol>>;
86109
private bridgeStateItems: Array<ISelectorItem<BridgeState>>;
@@ -395,7 +418,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
395418
</Cell.Footer>
396419
</AriaInputGroup>
397420

398-
<StyledBottomCellGroup>
421+
<StyledButtonCellGroup>
399422
<Cell.CellButton onClick={this.props.onViewWireguardKeys}>
400423
<Cell.Label>
401424
{messages.pgettext('advanced-settings-view', 'WireGuard key')}
@@ -411,7 +434,61 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
411434
<Cell.Icon height={12} width={7} source="icon-chevron" />
412435
</Cell.CellButton>
413436
)}
414-
</StyledBottomCellGroup>
437+
</StyledButtonCellGroup>
438+
439+
<StyledCustomDnsSwitchContainer>
440+
<Cell.InputLabel>
441+
{messages.pgettext('advanced-settings-view', 'Use custom DNS server')}
442+
</Cell.InputLabel>
443+
<Cell.Switch
444+
ref={this.customDnsSwitchRef}
445+
isOn={this.props.dns.custom}
446+
onChange={this.setCustomDnsEnabled}
447+
/>
448+
</StyledCustomDnsSwitchContainer>
449+
<Accordion expanded={this.props.dns.custom}>
450+
<CellList items={this.customDnsItems()} onRemove={this.removeDnsAddress} />
451+
452+
{this.state.showAddCustomDns && (
453+
<div ref={this.customDnsInputContainerRef}>
454+
<Cell.RowInput
455+
onSubmit={this.addDnsAddress}
456+
onChange={this.addDnsInputChange}
457+
invalid={this.state.invalidDnsIp}
458+
paddingLeft={32}
459+
onBlur={this.customDnsInputBlur}
460+
autofocus
461+
/>
462+
</div>
463+
)}
464+
465+
<StyledAddCustomDnsButton
466+
ref={this.customDnsAddButtonRef}
467+
onClick={this.showAddCustomDnsRow}
468+
disabled={this.state.showAddCustomDns}
469+
tabIndex={-1}>
470+
<StyledAddCustomDnsLabel tabIndex={-1}>
471+
{messages.pgettext('advanced-settings-view', 'Add a server')}
472+
</StyledAddCustomDnsLabel>
473+
<Cell.UntintedIcon
474+
source="icon-add"
475+
width={22}
476+
height={22}
477+
tintColor={colors.white60}
478+
tintHoverColor={colors.white80}
479+
tabIndex={-1}
480+
/>
481+
</StyledAddCustomDnsButton>
482+
</Accordion>
483+
484+
<StyledCustomDnsFotter>
485+
<Cell.FooterText>
486+
{messages.pgettext(
487+
'advanced-settings-view',
488+
'Enable to add at least one DNS server.',
489+
)}
490+
</Cell.FooterText>
491+
</StyledCustomDnsFotter>
415492
</StyledNavigationScrollbars>
416493
</NavigationContainer>
417494
</StyledContainer>
@@ -423,6 +500,81 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
423500
);
424501
}
425502

503+
private setCustomDnsEnabled = async (enabled: boolean) => {
504+
await this.props.setDnsOptions({
505+
custom: enabled,
506+
addresses: this.props.dns.addresses,
507+
});
508+
509+
if (enabled && this.props.dns.addresses.length === 0) {
510+
this.showAddCustomDnsRow();
511+
}
512+
513+
if (!enabled) {
514+
this.setState({ showAddCustomDns: false });
515+
}
516+
};
517+
518+
private customDnsItems(): ICellListItem<string>[] {
519+
return this.props.dns.addresses.map((address) => ({
520+
label: address,
521+
value: address,
522+
}));
523+
}
524+
525+
private showAddCustomDnsRow = () => {
526+
this.setState({ showAddCustomDns: true });
527+
};
528+
529+
// The input field should be hidden when it loses focus unless something on the same row or the
530+
// add-button is the new focused element.
531+
private customDnsInputBlur = (event?: React.FocusEvent<HTMLTextAreaElement>) => {
532+
const relatedTarget = event?.relatedTarget as Node | undefined;
533+
if (
534+
relatedTarget &&
535+
(this.customDnsSwitchRef.current?.contains(relatedTarget) ||
536+
this.customDnsAddButtonRef.current?.contains(relatedTarget) ||
537+
this.customDnsInputContainerRef.current?.contains(relatedTarget))
538+
) {
539+
event?.target.focus();
540+
} else {
541+
this.hideAddCustomDnsRow(false);
542+
}
543+
};
544+
545+
private hideAddCustomDnsRow(justAdded: boolean) {
546+
this.setState({ showAddCustomDns: false });
547+
if (!justAdded && this.props.dns.addresses.length === 0) {
548+
consumePromise(this.setCustomDnsEnabled(false));
549+
}
550+
}
551+
552+
private addDnsInputChange = (_value: string) => {
553+
this.setState({ invalidDnsIp: false });
554+
};
555+
556+
private addDnsAddress = async (address: string) => {
557+
try {
558+
await this.props.setDnsOptions({
559+
custom: this.props.dns.custom,
560+
addresses: [...this.props.dns.addresses, address],
561+
});
562+
this.hideAddCustomDnsRow(true);
563+
} catch (_e) {
564+
this.setState({ invalidDnsIp: true });
565+
}
566+
};
567+
568+
private removeDnsAddress = (address: string) => {
569+
const addresses = this.props.dns.addresses.filter((item) => item !== address);
570+
consumePromise(
571+
this.props.setDnsOptions({
572+
custom: addresses.length > 0 && this.props.dns.custom,
573+
addresses,
574+
}),
575+
);
576+
};
577+
426578
private tunnelProtocolItems = (
427579
hasWireguardKey: boolean,
428580
): Array<ISelectorItem<OptionalTunnelProtocol>> => {

gui/src/renderer/components/AdvancedSettingsStyles.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
2929
flex: 1,
3030
});
3131

32-
export const StyledBottomCellGroup = styled.div({
32+
export const StyledButtonCellGroup = styled.div({
3333
display: 'flex',
3434
flexDirection: 'column',
3535
flex: 1,
@@ -44,3 +44,29 @@ export const StyledNoWireguardKeyError = styled(Cell.FooterText)({
4444
fontWeight: 800,
4545
color: colors.red,
4646
});
47+
48+
export const StyledCustomDnsSwitchContainer = styled(Cell.Container)({
49+
marginBottom: '1px',
50+
});
51+
52+
export const StyledCustomDnsFotter = styled(Cell.Footer)({
53+
marginBottom: '2px',
54+
});
55+
56+
export const StyledAddCustomDnsButton = styled(Cell.CellButton)({
57+
backgroundColor: colors.blue40,
58+
});
59+
60+
export const StyledAddCustomDnsLabel = styled(Cell.Label)(
61+
{},
62+
(props: { paddingLeft?: number }) => ({
63+
fontFamily: 'Open Sans',
64+
fontWeight: 'normal',
65+
fontSize: '16px',
66+
paddingLeft: (props.paddingLeft ?? 32) + 'px',
67+
whiteSpace: 'pre-wrap',
68+
overflowWrap: 'break-word',
69+
width: '171px',
70+
marginRight: '25px',
71+
}),
72+
);

gui/src/renderer/containers/AdvancedSettingsPage.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import log from 'electron-log';
22
import { connect } from 'react-redux';
33
import { RouteComponentProps, withRouter } from 'react-router';
4-
import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types';
4+
import {
5+
BridgeState,
6+
IDnsOptions,
7+
RelayProtocol,
8+
TunnelProtocol,
9+
} from '../../shared/daemon-rpc-types';
510
import RelaySettingsBuilder from '../../shared/relay-settings-builder';
611
import AdvancedSettings from '../components/AdvancedSettings';
712

@@ -19,6 +24,7 @@ const mapStateToProps = (state: IReduxState) => {
1924
mssfix: state.settings.openVpn.mssfix,
2025
wireguardMtu: state.settings.wireguard.mtu,
2126
bridgeState: state.settings.bridgeState,
27+
dns: state.settings.dns,
2228
...protocolAndPort,
2329
};
2430
};
@@ -152,6 +158,11 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps
152158
log.error('Failed to update mtu value', e.message);
153159
}
154160
},
161+
162+
setDnsOptions: (dns: IDnsOptions) => {
163+
return props.app.setDnsOptions(dns);
164+
},
165+
155166
onViewWireguardKeys: () => props.history.push('/settings/advanced/wireguard-keys'),
156167
onViewLinuxSplitTunneling: () => props.history.push('/settings/advanced/linux-split-tunneling'),
157168
};

0 commit comments

Comments
 (0)