Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DNS rebinding protection #2397

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion client/src/__locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -586,5 +586,13 @@
"port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction</0> on how to resolve this.",
"adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.",
"client_not_in_allowed_clients": "The client is not allowed because it is not in the \"Allowed clients\" list.",
"experimental": "Experimental"
"experimental": "Experimental",
"rebinding_title": "DNS Rebinding Protection",
"rebinding_desc": "Here you can configure protection against DNS rebinding attacks",
"rebinding_protection_enabled": "Enable protection from DNS rebinding attacks",
"rebinding_protection_enabled_desc": "If enabled, AdGuard Home will block responses containing host on the local network.",
"rebinding_allowed_hosts_title": "Allowed domains",
"rebinding_allowed_hosts_desc": "A list of domains. If configured, AdGuard Home will allow responses containing host on the local network from these domains. Here you can specify the exact domain names, wildcards and urlfilter-rules, e.g. 'example.org', '*.example.org' or '||example.org^'.",
"rebinding_applied": "DNS rebinding protection applied",
"blocked_rebind": "Blocked rebinding"
}
3 changes: 3 additions & 0 deletions client/src/actions/dnsConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const setDnsConfig = (config) => async (dispatch) => {
data.upstream_dns = splitByNewLine(config.upstream_dns);
hasDnsSettings = true;
}
if (Object.prototype.hasOwnProperty.call(data, 'rebinding_allowed_hosts')) {
data.rebinding_allowed_hosts = splitByNewLine(config.rebinding_allowed_hosts);
}

await apiClient.setDnsConfig(data);

Expand Down
1 change: 1 addition & 0 deletions client/src/components/Filters/Check/Info.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const getTitle = () => {
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason),
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason),
[FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason),
[FILTERED_STATUS.FILTERED_BLOCKED_REBIND]: t('rebinding_applied'),
};

if (Object.prototype.hasOwnProperty.call(REASON_TO_TITLE_MAP, reason)) {
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/Logs/Cells/ClientCell.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { nanoid } from 'nanoid';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import propTypes from 'prop-types';
import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
import { checkFiltered, checkBlockedRebind, getBlockingClientName } from '../../../helpers/helpers';
import { BLOCK_ACTIONS } from '../../../helpers/constants';
import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
import IconTooltip from './IconTooltip';
Expand Down Expand Up @@ -48,6 +48,7 @@ const ClientCell = ({
const processedData = Object.entries(data);

const isFiltered = checkFiltered(reason);
const isBlockedRebinding = checkBlockedRebind(reason);

const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
'mt-2': isDetailed && !name && !whoisAvailable,
Expand Down Expand Up @@ -125,7 +126,7 @@ const ClientCell = ({
'button-action__container--detailed': isDetailed,
});

return <div className={containerClass}>
return isBlockedRebinding || <div className={containerClass}>
<button type="button"
className={buttonClass}
onClick={onClick}
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Logs/Cells/ResponseCell.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const ResponseCell = ({
return formattedElapsedMs;
}
return getServiceName(service_name);
// case FILTERED_STATUS.FILTERED_BLOCKED_REBIND: // TODO??
case FILTERED_STATUS.FILTERED_BLACK_LIST:
case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
return getFilterNames(rules, filters, whitelistFilters).join(', ');
Expand Down
10 changes: 7 additions & 3 deletions client/src/components/Logs/Cells/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import propTypes from 'prop-types';
import {
captitalizeWords,
checkFiltered,
checkBlockedRebind,
getRulesToFilterList,
formatDateTime,
formatElapsedMs,
Expand Down Expand Up @@ -90,6 +91,7 @@ const Row = memo(({

const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
const isFiltered = checkFiltered(reason);
const isBlockedRebinding = checkBlockedRebind(reason);

const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
Expand Down Expand Up @@ -183,9 +185,11 @@ const Row = memo(({
source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
original_response: originalResponse?.join('\n'),
[BUTTON_PREFIX + buttonType]: blockButton,
[BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,
[BUTTON_PREFIX + blockingClientKey]: blockClientButton,
...(isBlockedRebinding || {
[BUTTON_PREFIX + buttonType]: blockButton,
[BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,
[BUTTON_PREFIX + blockingClientKey]: blockClientButton,
}),
};

setDetailedDataCurrent(processContent(detailedData));
Expand Down
91 changes: 91 additions & 0 deletions client/src/components/Settings/Dns/Rebinding/Form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useSelector } from 'react-redux';
import { renderTextareaField, CheckboxField } from '../../../../helpers/form';
import { removeEmptyLines } from '../../../../helpers/helpers';
import { FORM_NAME } from '../../../../helpers/constants';

const fields = [
{
id: 'rebinding_allowed_hosts',
title: 'rebinding_allowed_hosts_title',
subtitle: 'rebinding_allowed_hosts_desc',
normalizeOnBlur: removeEmptyLines,
},
];

const Form = ({
handleSubmit, submitting, invalid,
}) => {
const { t } = useTranslation();
const { processingSetConfig } = useSelector((state) => state.dnsConfig, shallowEqual);

const renderField = ({
id, title, subtitle, disabled = processingSetConfig, normalizeOnBlur,
}) => <div key={id} className="form__group mb-5">
<label className="form__label form__label--with-desc" htmlFor={id}>
<Trans>{title}</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>{subtitle}</Trans>
</div>
<Field
id={id}
name={id}
component={renderTextareaField}
type="text"
className="form-control form-control--textarea font-monospace"
disabled={disabled}
normalizeOnBlur={normalizeOnBlur}
/>
</div>;

renderField.propTypes = {
id: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
disabled: PropTypes.bool,
normalizeOnBlur: PropTypes.func,
};

return (
<form onSubmit={handleSubmit}>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name={'rebinding_protection_enabled'}
type="checkbox"
component={CheckboxField}
placeholder={t('rebinding_protection_enabled')}
subtitle={t('rebinding_protection_enabled_desc')}
disabled={processingSetConfig}
/>
</div>
</div>

{fields.map(renderField)}

<div className="card-actions">
<div className="btn-list">
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || invalid || processingSetConfig}
>
<Trans>save_config</Trans>
</button>
</div>
</div>
</form>
);
};

Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
};

export default reduxForm({ form: FORM_NAME.REBINDING })(Form);
36 changes: 36 additions & 0 deletions client/src/components/Settings/Dns/Rebinding/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../../ui/Card';
import { setDnsConfig } from '../../../../actions/dnsConfig';

const RebindingConfig = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
rebinding_protection_enabled, rebinding_allowed_hosts,
} = useSelector((state) => state.dnsConfig, shallowEqual);

const handleFormSubmit = (values) => {
dispatch(setDnsConfig(values));
};

return (
<Card
title={t('rebinding_title')}
subtitle={t('rebinding_desc')}
bodyType="card-body box-body--settings"
>
<Form
initialValues={{
rebinding_protection_enabled,
rebinding_allowed_hosts,
}}
onSubmit={handleFormSubmit}
/>
</Card>
);
};

export default RebindingConfig;
2 changes: 2 additions & 0 deletions client/src/components/Settings/Dns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Config from './Config';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';
import CacheConfig from './Cache';
import RebindingConfig from './Rebinding';
import { getDnsConfig } from '../../../actions/dnsConfig';
import { getAccessList } from '../../../actions/access';

Expand All @@ -33,6 +34,7 @@ const Dns = () => {
<Config />
<CacheConfig />
<Access />
<RebindingConfig />
</>}
</>;
};
Expand Down
10 changes: 10 additions & 0 deletions client/src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export const FILTERED_STATUS = {
FILTERED_BLACK_LIST: 'FilteredBlackList',
NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList',
NOT_FILTERED_NOT_FOUND: 'NotFilteredNotFound',
FILTERED_BLOCKED_REBIND: 'FilteredRebind',
FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',
REWRITE: 'Rewrite',
REWRITE_HOSTS: 'RewriteEtcHosts',
Expand All @@ -362,6 +363,10 @@ export const RESPONSE_FILTER = {
QUERY: 'blocked',
LABEL: 'show_blocked_responses',
},
BLOCKED_REBIND: {
QUERY: 'blocked_rebind',
LABEL: 'blocked_rebind',
},
BLOCKED_SERVICES: {
QUERY: 'blocked_services',
LABEL: 'blocked_services',
Expand Down Expand Up @@ -443,6 +448,10 @@ export const FILTERED_STATUS_TO_META_MAP = {
LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL,
COLOR: QUERY_STATUS_COLORS.YELLOW,
},
[FILTERED_STATUS.FILTERED_BLOCKED_REBIND]: {
LABEL: RESPONSE_FILTER.BLOCKED_REBIND.LABEL,
COLOR: QUERY_STATUS_COLORS.RED,
},
};

export const DEFAULT_TIME_FORMAT = 'HH:mm:ss';
Expand Down Expand Up @@ -514,6 +523,7 @@ export const FORM_NAME = {
INSTALL: 'install',
LOGIN: 'login',
CACHE: 'cache',
REBINDING: 'rebinding',
...DHCP_FORM_NAMES,
};

Expand Down
1 change: 1 addition & 0 deletions client/src/helpers/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ export const checkNotFilteredNotFound = (reason) => reason === FILTERED_STATUS.N
export const checkSafeSearch = (reason) => reason === FILTERED_STATUS.FILTERED_SAFE_SEARCH;
export const checkSafeBrowsing = (reason) => reason === FILTERED_STATUS.FILTERED_SAFE_BROWSING;
export const checkParental = (reason) => reason === FILTERED_STATUS.FILTERED_PARENTAL;
export const checkBlockedRebind = (reason) => reason === FILTERED_STATUS.FILTERED_BLOCKED_REBIND;
export const checkBlockedService = (reason) => reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;

export const getCurrentFilter = (url, filters) => {
Expand Down
2 changes: 2 additions & 0 deletions client/src/reducers/dnsConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const dnsConfig = handleActions(
blocking_ipv6,
upstream_dns,
bootstrap_dns,
rebinding_allowed_hosts,
...values
} = payload;

Expand All @@ -26,6 +27,7 @@ const dnsConfig = handleActions(
blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6,
upstream_dns: (upstream_dns && upstream_dns.join('\n')) || '',
bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\n')) || '',
rebinding_allowed_hosts: (rebinding_allowed_hosts && rebinding_allowed_hosts.join('\n')) || '',
processingGetConfig: false,
};
},
Expand Down
3 changes: 3 additions & 0 deletions internal/dnsfilter/dnsfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ const (
FilteredSafeSearch
// FilteredBlockedService - the host is blocked by "blocked services" settings
FilteredBlockedService
// FilteredRebind - the request was blocked due to DNS rebinding protection
FilteredRebind

// Rewritten is returned when there was a rewrite by a legacy DNS
// rewrite rule.
Expand Down Expand Up @@ -178,6 +180,7 @@ var reasonNames = []string{
FilteredInvalid: "FilteredInvalid",
FilteredSafeSearch: "FilteredSafeSearch",
FilteredBlockedService: "FilteredBlockedService",
FilteredRebind: "FilteredRebind",

Rewritten: "Rewrite",
RewrittenAutoHosts: "RewriteEtcHosts",
Expand Down
5 changes: 5 additions & 0 deletions internal/dnsforward/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ type FilteringConfig struct {
CacheMinTTL uint32 `yaml:"cache_ttl_min"` // override TTL value (minimum) received from upstream server
CacheMaxTTL uint32 `yaml:"cache_ttl_max"` // override TTL value (maximum) received from upstream server

// DNS rebinding protection settings
// --
RebindingProtectionEnabled bool `yaml:"rebinding_protection_enabled"`
RebindingAllowedHosts []string `yaml:"rebinding_allowed_hosts"`

// Other settings
// --

Expand Down
4 changes: 4 additions & 0 deletions internal/dnsforward/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
processFilteringBeforeRequest,
processUpstream,
processDNSSECAfterResponse,
processRebindingFilteringAfterResponse,
processFilteringAfterResponse,
s.ipset.process,
processQueryLogsAndStats,
Expand Down Expand Up @@ -390,6 +391,9 @@ func processFilteringAfterResponse(ctx *dnsContext) int {
d.Res.Answer = answer
}

case dnsfilter.FilteredRebind:
// nothing

case dnsfilter.NotFilteredAllowList:
// nothing

Expand Down
9 changes: 9 additions & 0 deletions internal/dnsforward/dnsforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Server struct {
queryLog querylog.QueryLog // Query log instance
stats stats.Stats
access *accessCtx
rebinding *dnsRebindChecker

ipset ipsetCtx

Expand Down Expand Up @@ -122,6 +123,7 @@ func (s *Server) WriteDiskConfig(c *FilteringConfig) {
c.DisallowedClients = stringArrayDup(sc.DisallowedClients)
c.BlockedHosts = stringArrayDup(sc.BlockedHosts)
c.UpstreamDNS = stringArrayDup(sc.UpstreamDNS)
c.RebindingAllowedHosts = stringArrayDup(sc.RebindingAllowedHosts)
s.RUnlock()
}

Expand Down Expand Up @@ -221,6 +223,13 @@ func (s *Server) Prepare(config *ServerConfig) error {
return err
}

// Initialize DNS rebinding module
// --
s.rebinding, err = newRebindChecker(s.conf.RebindingAllowedHosts)
if err != nil {
return err
}

// Register web handlers if necessary
// --
if !webRegistered && s.conf.HTTPRegister != nil {
Expand Down