Merge: + DNS: new settings (ratelimit, blocking mode, edns_client_subnet)

Close #1091 Close #1154 Close #1022

* commit '97e77cab643d6784067ce97c0f03ec3e4612c2c9':
  + client: handle EDNS Client Subnet setting
  + dns: add "edns_client_subnet" setting
  + client: handle DNS config
  * DNS: remove /enable_protection and /disable_protection
  + openapi: /dns_info, /dns_config
  * /control/set_upstreams_config: allow empty upstream list
  + dns: support blocking_mode=custom_ip
  + DNS: Get/Set DNS general settings
This commit is contained in:
Simon Zolin 2019-12-10 16:10:31 +03:00
commit 2b14a043a9
19 changed files with 611 additions and 49 deletions

View File

@ -29,6 +29,9 @@ Contents:
* Static IP check/set * Static IP check/set
* Add a static lease * Add a static lease
* API: Reset DHCP configuration * API: Reset DHCP configuration
* DNS general settings
* API: Get DNS general settings
* API: Set DNS general settings
* DNS access settings * DNS access settings
* List access settings * List access settings
* Set access settings * Set access settings
@ -801,6 +804,50 @@ Response:
] ]
## DNS general settings
### API: Get DNS general settings
Request:
GET /control/dns_info
Response:
200 OK
{
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "nxdomain" | "null_ip" | "custom_ip",
"blocking_ipv4": "1.2.3.4",
"blocking_ipv6": "1:2:3::4",
"edns_cs_enabled": true | false,
}
### API: Set DNS general settings
Request:
POST /control/dns_config
{
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "nxdomain" | "null_ip" | "custom_ip",
"blocking_ipv4": "1.2.3.4",
"blocking_ipv6": "1:2:3::4",
"edns_cs_enabled": true | false,
}
Response:
200 OK
`blocking_ipv4` and `blocking_ipv6` values are active when `blocking_mode` is set to `custom_ip`.
## DNS access settings ## DNS access settings
There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request. There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.

View File

@ -24,6 +24,7 @@
"form_error_ip_format": "Invalid IP format", "form_error_ip_format": "Invalid IP format",
"form_error_mac_format": "Invalid MAC format", "form_error_mac_format": "Invalid MAC format",
"form_error_positive": "Must be greater than 0", "form_error_positive": "Must be greater than 0",
"form_error_negative": "Must be equal to 0 or greater",
"dhcp_form_gateway_input": "Gateway IP", "dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask", "dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Range of IP addresses", "dhcp_form_range_title": "Range of IP addresses",
@ -187,6 +188,22 @@
"query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>", "query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>",
"query_log_strict_search": "Use double quotes for strict search", "query_log_strict_search": "Use double quotes for strict search",
"query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost", "query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost",
"dns_config": "DNS server configuration",
"blocking_mode": "Blocking mode",
"nxdomain": "NXDOMAIN",
"null_ip": "Null IP",
"custom_ip": "Custom IP",
"blocking_ipv4": "Blocking IPv4",
"blocking_ipv6": "Blocking IPv6",
"form_enter_rate_limit": "Enter rate limit",
"rate_limit": "Rate limit",
"edns_enable": "Enable EDNS Client Subnet",
"edns_cs_desc": "If enabled, AdGuard Home will be sending clients' subnets to the DNS servers.",
"rate_limit_desc": "The number of requests per second that a single client is allowed to make (0: unlimited)",
"blocking_ipv4_desc": "IP address to be returned for a blocked A request",
"blocking_ipv6_desc": "IP address to be returned for a blocked AAAA request",
"blocking_mode_desc": "<0>NXDOMAIN Respond with NXDOMAIN code;</0> <0>Null IP Respond with zero IP address (0.0.0.0 for A; :: for AAAA);</0> <0>Custom IP - Respond with a manually set IP address.</0>",
"upstream_dns_client_desc": "If you keep this field empty, AdGuard Home will use the servers configured in the <0>DNS settings</0>.",
"source_label": "Source", "source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.", "found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category", "category_label": "Category",

View File

@ -0,0 +1,35 @@
import { createAction } from 'redux-actions';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');
export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE');
export const getDnsConfigSuccess = createAction('GET_DNS_CONFIG_SUCCESS');
export const getDnsConfig = () => async (dispatch) => {
dispatch(getDnsConfigRequest());
try {
const data = await apiClient.getDnsConfig();
dispatch(getDnsConfigSuccess(data));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getDnsConfigFailure());
}
};
export const setDnsConfigRequest = createAction('SET_DNS_CONFIG_REQUEST');
export const setDnsConfigFailure = createAction('SET_DNS_CONFIG_FAILURE');
export const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS');
export const setDnsConfig = config => async (dispatch) => {
dispatch(setDnsConfigRequest());
try {
await apiClient.setDnsConfig(config);
dispatch(addSuccessToast('config_successfully_saved'));
dispatch(setDnsConfigSuccess(config));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDnsConfigFailure());
}
};

View File

@ -91,17 +91,9 @@ export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS')
export const toggleProtection = status => async (dispatch) => { export const toggleProtection = status => async (dispatch) => {
dispatch(toggleProtectionRequest()); dispatch(toggleProtectionRequest());
let successMessage = '';
try { try {
if (status) { const successMessage = status ? 'disabled_protection' : 'enabled_protection';
successMessage = 'disabled_protection'; await apiClient.setDnsConfig({ protection_enabled: !status });
await apiClient.disableGlobalProtection();
} else {
successMessage = 'enabled_protection';
await apiClient.enableGlobalProtection();
}
dispatch(addSuccessToast(successMessage)); dispatch(addSuccessToast(successMessage));
dispatch(toggleProtectionSuccess()); dispatch(toggleProtectionSuccess());
} catch (error) { } catch (error) {

View File

@ -30,8 +30,6 @@ class Api {
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' }; GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' }; GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'POST' }; GLOBAL_VERSION = { path: 'version.json', method: 'POST' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
GLOBAL_UPDATE = { path: 'update', method: 'POST' }; GLOBAL_UPDATE = { path: 'update', method: 'POST' };
startGlobalFiltering() { startGlobalFiltering() {
@ -76,16 +74,6 @@ class Api {
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
enableGlobalProtection() {
const { path, method } = this.GLOBAL_ENABLE_PROTECTION;
return this.makeRequest(path, method);
}
disableGlobalProtection() {
const { path, method } = this.GLOBAL_DISABLE_PROTECTION;
return this.makeRequest(path, method);
}
getUpdate() { getUpdate() {
const { path, method } = this.GLOBAL_UPDATE; const { path, method } = this.GLOBAL_UPDATE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
@ -546,6 +534,24 @@ class Api {
const { path, method } = this.GET_PROFILE; const { path, method } = this.GET_PROFILE;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
// DNS config
GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };
SET_DNS_CONFIG = { path: 'dns_config', method: 'POST' };
getDnsConfig() {
const { path, method } = this.GET_DNS_CONFIG;
return this.makeRequest(path, method);
}
setDnsConfig(data) {
const { path, method } = this.SET_DNS_CONFIG;
const config = {
data,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
} }
const apiClient = new Api(); const apiClient = new Api();

View File

@ -225,6 +225,11 @@ let Form = (props) => {
</div> </div>
</div> </div>
<div label="upstream" title={props.t('upstream_dns')}> <div label="upstream" title={props.t('upstream_dns')}>
<div className="form__desc mb-3">
<Trans components={[<a href="#dns" key="0">link</a>]}>
upstream_dns_client_desc
</Trans>
</div>
<Field <Field
id="upstreams" id="upstreams"
name="upstreams" name="upstreams"

View File

@ -0,0 +1,153 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import {
renderField,
renderRadioField,
renderSelectField,
required,
ipv4,
ipv6,
biggerOrEqualZero,
toNumber,
} from '../../../../helpers/form';
import { BLOCKING_MODES } from '../../../../helpers/constants';
const getFields = (processing, t) => Object.values(BLOCKING_MODES).map(mode => (
<Field
key={mode}
name="blocking_mode"
type="radio"
component={renderRadioField}
value={mode}
placeholder={t(mode)}
disabled={processing}
/>
));
let Form = ({
handleSubmit, submitting, invalid, processing, blockingMode, t,
}) => (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit" className="form__label form__label--with-desc">
<Trans>rate_limit</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>rate_limit_desc</Trans>
</div>
<Field
name="ratelimit"
type="number"
component={renderField}
className="form-control"
placeholder={t('form_enter_rate_limit')}
normalize={toNumber}
validate={[required, biggerOrEqualZero]}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="edns_cs_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('edns_enable')}
disabled={processing}
subtitle={t('edns_cs_desc')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings mb-4">
<label className="form__label form__label--with-desc">
<Trans>blocking_mode</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans components={[<div key="0">text</div>]}>blocking_mode_desc</Trans>
</div>
<div className="custom-controls-stacked">
{getFields(processing, t)}
</div>
</div>
</div>
{blockingMode === BLOCKING_MODES.custom_ip && (
<Fragment>
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="blocking_ipv4" className="form__label form__label--with-desc">
<Trans>blocking_ipv4</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocking_ipv4_desc</Trans>
</div>
<Field
name="blocking_ipv4"
component={renderField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv4, required]}
/>
</div>
</div>
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ip_address" className="form__label form__label--with-desc">
<Trans>blocking_ipv6</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocking_ipv6_desc</Trans>
</div>
<Field
name="blocking_ipv6"
component={renderField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv6, required]}
/>
</div>
</div>
</Fragment>
)}
</div>
<button
type="submit"
className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing}
>
<Trans>save_btn</Trans>
</button>
</form>
);
Form.propTypes = {
blockingMode: PropTypes.string.isRequired,
handleSubmit: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
const selector = formValueSelector('blockingModeForm');
Form = connect((state) => {
const blockingMode = selector(state, 'blocking_mode');
return {
blockingMode,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({
form: 'blockingModeForm',
}),
])(Form);

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import Card from '../../../ui/Card';
import Form from './Form';
const Config = ({ t, dnsConfig, setDnsConfig }) => {
const handleFormSubmit = (values) => {
setDnsConfig(values);
};
const {
blocking_mode,
ratelimit,
blocking_ipv4,
blocking_ipv6,
edns_cs_enabled,
processingSetConfig,
} = dnsConfig;
return (
<Card
title={t('dns_config')}
bodyType="card-body box-body--settings"
id="dns-config"
>
<div className="form">
<Form
initialValues={{
ratelimit,
blocking_mode,
blocking_ipv4,
blocking_ipv6,
edns_cs_enabled,
}}
onSubmit={handleFormSubmit}
processing={processingSetConfig}
/>
</div>
</Card>
);
};
Config.propTypes = {
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Config);

View File

@ -5,6 +5,7 @@ import { withNamespaces } from 'react-i18next';
import Upstream from './Upstream'; import Upstream from './Upstream';
import Access from './Access'; import Access from './Access';
import Rewrites from './Rewrites'; import Rewrites from './Rewrites';
import Config from './Config';
import PageTitle from '../../ui/PageTitle'; import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
@ -13,6 +14,7 @@ class Dns extends Component {
this.props.getDnsSettings(); this.props.getDnsSettings();
this.props.getAccessList(); this.props.getAccessList();
this.props.getRewritesList(); this.props.getRewritesList();
this.props.getDnsConfig();
} }
render() { render() {
@ -29,12 +31,18 @@ class Dns extends Component {
addRewrite, addRewrite,
deleteRewrite, deleteRewrite,
toggleRewritesModal, toggleRewritesModal,
dnsConfig,
setDnsConfig,
} = this.props; } = this.props;
const isDataLoading = const isDataLoading = dashboard.processingDnsSettings
dashboard.processingDnsSettings || access.processing || rewrites.processing; || access.processing
const isDataReady = || rewrites.processing
!dashboard.processingDnsSettings && !access.processing && !rewrites.processing; || dnsConfig.processingGetConfig;
const isDataReady = !dashboard.processingDnsSettings
&& !access.processing
&& !rewrites.processing
&& !dnsConfig.processingGetConfig;
return ( return (
<Fragment> <Fragment>
@ -42,6 +50,10 @@ class Dns extends Component {
{isDataLoading && <Loading />} {isDataLoading && <Loading />}
{isDataReady && ( {isDataReady && (
<Fragment> <Fragment>
<Config
dnsConfig={dnsConfig}
setDnsConfig={setDnsConfig}
/>
<Upstream <Upstream
upstreamDns={dashboard.upstreamDns} upstreamDns={dashboard.upstreamDns}
bootstrapDns={dashboard.bootstrapDns} bootstrapDns={dashboard.bootstrapDns}
@ -80,6 +92,9 @@ Dns.propTypes = {
deleteRewrite: PropTypes.func.isRequired, deleteRewrite: PropTypes.func.isRequired,
toggleRewritesModal: PropTypes.func.isRequired, toggleRewritesModal: PropTypes.func.isRequired,
getDnsSettings: PropTypes.func.isRequired, getDnsSettings: PropTypes.func.isRequired,
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
getDnsConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
}; };

View File

@ -6,6 +6,7 @@ import Services from './Services';
import StatsConfig from './StatsConfig'; import StatsConfig from './StatsConfig';
import LogsConfig from './LogsConfig'; import LogsConfig from './LogsConfig';
import FiltersConfig from './FiltersConfig'; import FiltersConfig from './FiltersConfig';
import Checkbox from '../ui/Checkbox'; import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';

View File

@ -7,17 +7,19 @@ import {
deleteRewrite, deleteRewrite,
toggleRewritesModal, toggleRewritesModal,
} from '../actions/rewrites'; } from '../actions/rewrites';
import { getDnsConfig, setDnsConfig } from '../actions/dnsConfig';
import Dns from '../components/Settings/Dns'; import Dns from '../components/Settings/Dns';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { const {
dashboard, settings, access, rewrites, dashboard, settings, access, rewrites, dnsConfig,
} = state; } = state;
const props = { const props = {
dashboard, dashboard,
settings, settings,
access, access,
rewrites, rewrites,
dnsConfig,
}; };
return props; return props;
}; };
@ -33,6 +35,8 @@ const mapDispatchToProps = {
deleteRewrite, deleteRewrite,
toggleRewritesModal, toggleRewritesModal,
getDnsSettings, getDnsSettings,
getDnsConfig,
setDnsConfig,
}; };
export default connect( export default connect(

View File

@ -341,6 +341,12 @@ export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168]; export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
export const BLOCKING_MODES = {
nxdomain: 'nxdomain',
null_ip: 'null_ip',
custom_ip: 'custom_ip',
};
export const WHOIS_ICONS = { export const WHOIS_ICONS = {
location: 'location', location: 'location',
orgname: 'network', orgname: 'network',

View File

@ -189,6 +189,13 @@ export const isPositive = (value) => {
return false; return false;
}; };
export const biggerOrEqualZero = (value) => {
if (value < 0) {
return <Trans>form_error_negative</Trans>;
}
return false;
};
export const port = (value) => { export const port = (value) => {
if ((value || value === 0) && (value < 80 || value > 65535)) { if ((value || value === 0) && (value < 80 || value > 65535)) {
return <Trans>form_error_port_range</Trans>; return <Trans>form_error_port_range</Trans>;

View File

@ -0,0 +1,50 @@
import { handleActions } from 'redux-actions';
import * as actions from '../actions/dnsConfig';
import { BLOCKING_MODES } from '../helpers/constants';
const DEFAULT_BLOCKING_IPV4 = '0.0.0.0';
const DEFAULT_BLOCKING_IPV6 = '::';
const dnsConfig = handleActions(
{
[actions.getDnsConfigRequest]: state => ({ ...state, processingGetConfig: true }),
[actions.getDnsConfigFailure]: state =>
({ ...state, processingGetConfig: false }),
[actions.getDnsConfigSuccess]: (state, { payload }) => {
const {
blocking_ipv4,
blocking_ipv6,
...values
} = payload;
return {
...state,
...values,
blocking_ipv4: blocking_ipv4 || DEFAULT_BLOCKING_IPV4,
blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6,
processingGetConfig: false,
};
},
[actions.setDnsConfigRequest]: state => ({ ...state, processingSetConfig: true }),
[actions.setDnsConfigFailure]: state =>
({ ...state, processingSetConfig: false }),
[actions.setDnsConfigSuccess]: (state, { payload }) => ({
...state,
...payload,
processingSetConfig: false,
}),
},
{
processingGetConfig: false,
processingSetConfig: false,
blocking_mode: BLOCKING_MODES.nxdomain,
ratelimit: 20,
blocking_ipv4: DEFAULT_BLOCKING_IPV4,
blocking_ipv6: DEFAULT_BLOCKING_IPV6,
edns_cs_enabled: false,
},
);
export default dnsConfig;

View File

@ -13,6 +13,7 @@ import rewrites from './rewrites';
import services from './services'; import services from './services';
import stats from './stats'; import stats from './stats';
import queryLogs from './queryLogs'; import queryLogs from './queryLogs';
import dnsConfig from './dnsConfig';
import filtering from './filtering'; import filtering from './filtering';
const settings = handleActions( const settings = handleActions(
@ -369,6 +370,7 @@ export default combineReducers({
rewrites, rewrites,
services, services,
stats, stats,
dnsConfig,
loadingBar: loadingBarReducer, loadingBar: loadingBarReducer,
form: formReducer, form: formReducer,
}); });

View File

@ -99,14 +99,21 @@ type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
BlockingIPv4 string `yaml:"blocking_ipv4"` // IP address to be returned for a blocked A request
BlockingIPv6 string `yaml:"blocking_ipv6"` // IP address to be returned for a blocked AAAA request
BlockingIPAddrv4 net.IP `yaml:"-"`
BlockingIPAddrv6 net.IP `yaml:"-"`
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600) BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
Ratelimit int `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable) Ratelimit uint32 `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` // a list of whitelisted client IP addresses RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` // a list of whitelisted client IP addresses
RefuseAny bool `yaml:"refuse_any"` // if true, refuse ANY requests RefuseAny bool `yaml:"refuse_any"` // if true, refuse ANY requests
BootstrapDNS []string `yaml:"bootstrap_dns"` // a list of bootstrap DNS for DoH and DoT (plain DNS only) BootstrapDNS []string `yaml:"bootstrap_dns"` // a list of bootstrap DNS for DoH and DoT (plain DNS only)
AllServers bool `yaml:"all_servers"` // if true, parallel queries to all configured upstream servers are enabled AllServers bool `yaml:"all_servers"` // if true, parallel queries to all configured upstream servers are enabled
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
AllowedClients []string `yaml:"allowed_clients"` // IP addresses of whitelist clients AllowedClients []string `yaml:"allowed_clients"` // IP addresses of whitelist clients
DisallowedClients []string `yaml:"disallowed_clients"` // IP addresses of clients that should be blocked DisallowedClients []string `yaml:"disallowed_clients"` // IP addresses of clients that should be blocked
BlockedHosts []string `yaml:"blocked_hosts"` // hosts that should be blocked BlockedHosts []string `yaml:"blocked_hosts"` // hosts that should be blocked
@ -214,7 +221,7 @@ func (s *Server) prepare(config *ServerConfig) error {
proxyConfig := proxy.Config{ proxyConfig := proxy.Config{
UDPListenAddr: s.conf.UDPListenAddr, UDPListenAddr: s.conf.UDPListenAddr,
TCPListenAddr: s.conf.TCPListenAddr, TCPListenAddr: s.conf.TCPListenAddr,
Ratelimit: s.conf.Ratelimit, Ratelimit: int(s.conf.Ratelimit),
RatelimitWhitelist: s.conf.RatelimitWhitelist, RatelimitWhitelist: s.conf.RatelimitWhitelist,
RefuseAny: s.conf.RefuseAny, RefuseAny: s.conf.RefuseAny,
CacheEnabled: true, CacheEnabled: true,
@ -224,6 +231,7 @@ func (s *Server) prepare(config *ServerConfig) error {
BeforeRequestHandler: s.beforeRequestHandler, BeforeRequestHandler: s.beforeRequestHandler,
RequestHandler: s.handleDNSRequest, RequestHandler: s.handleDNSRequest,
AllServers: s.conf.AllServers, AllServers: s.conf.AllServers,
EnableEDNSClientSubnet: s.conf.EnableEDNSClientSubnet,
} }
s.access = &accessCtx{} s.access = &accessCtx{}
@ -657,6 +665,14 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu
case dns.TypeAAAA: case dns.TypeAAAA:
return s.genAAAARecord(m, net.IPv6zero) return s.genAAAARecord(m, net.IPv6zero)
} }
} else if s.conf.BlockingMode == "custom_ip" {
switch m.Question[0].Qtype {
case dns.TypeA:
return s.genARecord(m, s.conf.BlockingIPAddrv4)
case dns.TypeAAAA:
return s.genAAAARecord(m, s.conf.BlockingIPAddrv6)
}
} }
return s.genNXDomain(m) return s.genNXDomain(m)

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/jsonutil"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/utils" "github.com/AdguardTeam/golibs/utils"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -20,14 +21,112 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string,
http.Error(w, text, code) http.Error(w, text, code)
} }
func (s *Server) handleProtectionEnable(w http.ResponseWriter, r *http.Request) { type dnsConfigJSON struct {
s.conf.ProtectionEnabled = true ProtectionEnabled bool `json:"protection_enabled"`
s.conf.ConfigModified() RateLimit uint32 `json:"ratelimit"`
BlockingMode string `json:"blocking_mode"`
BlockingIPv4 string `json:"blocking_ipv4"`
BlockingIPv6 string `json:"blocking_ipv6"`
EDNSCSEnabled bool `json:"edns_cs_enabled"`
} }
func (s *Server) handleProtectionDisable(w http.ResponseWriter, r *http.Request) { func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
s.conf.ProtectionEnabled = false resp := dnsConfigJSON{}
s.RLock()
resp.ProtectionEnabled = s.conf.ProtectionEnabled
resp.BlockingMode = s.conf.BlockingMode
resp.BlockingIPv4 = s.conf.BlockingIPv4
resp.BlockingIPv6 = s.conf.BlockingIPv6
resp.RateLimit = s.conf.Ratelimit
resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet
s.RUnlock()
js, err := json.Marshal(resp)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
}
func checkBlockingMode(req dnsConfigJSON) bool {
bm := req.BlockingMode
if !(bm == "nxdomain" || bm == "null_ip" || bm == "custom_ip") {
return false
}
if bm == "custom_ip" {
ip := net.ParseIP(req.BlockingIPv4)
if ip == nil || ip.To4() == nil {
return false
}
ip = net.ParseIP(req.BlockingIPv6)
if ip == nil {
return false
}
}
return true
}
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
req := dnsConfigJSON{}
js, err := jsonutil.DecodeObject(&req, r.Body)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
if js.Exists("blocking_mode") && !checkBlockingMode(req) {
httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")
return
}
restart := false
s.Lock()
if js.Exists("protection_enabled") {
s.conf.ProtectionEnabled = req.ProtectionEnabled
}
if js.Exists("blocking_mode") {
s.conf.BlockingMode = req.BlockingMode
if req.BlockingMode == "custom_ip" {
if js.Exists("blocking_ipv4") {
s.conf.BlockingIPv4 = req.BlockingIPv4
s.conf.BlockingIPAddrv4 = net.ParseIP(req.BlockingIPv4)
}
if js.Exists("blocking_ipv6") {
s.conf.BlockingIPv6 = req.BlockingIPv6
s.conf.BlockingIPAddrv6 = net.ParseIP(req.BlockingIPv6)
}
}
}
if js.Exists("ratelimit") {
if s.conf.Ratelimit != req.RateLimit {
restart = true
}
s.conf.Ratelimit = req.RateLimit
}
if js.Exists("edns_cs_enabled") {
s.conf.EnableEDNSClientSubnet = req.EDNSCSEnabled
restart = true
}
s.Unlock()
s.conf.ConfigModified() s.conf.ConfigModified()
if restart {
err = s.Restart()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "%s", err)
return
}
}
} }
type upstreamJSON struct { type upstreamJSON struct {
@ -44,10 +143,12 @@ func (s *Server) handleSetUpstreamConfig(w http.ResponseWriter, r *http.Request)
return return
} }
err = ValidateUpstreams(req.Upstreams) if len(req.Upstreams) != 0 {
if err != nil { err = ValidateUpstreams(req.Upstreams)
httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err) if err != nil {
return httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
return
}
} }
newconf := FilteringConfig{} newconf := FilteringConfig{}
@ -270,12 +371,11 @@ func checkDNS(input string, bootstrap []string) error {
} }
func (s *Server) registerHandlers() { func (s *Server) registerHandlers() {
s.conf.HTTPRegister("POST", "/control/enable_protection", s.handleProtectionEnable) s.conf.HTTPRegister("GET", "/control/dns_info", s.handleGetConfig)
s.conf.HTTPRegister("POST", "/control/disable_protection", s.handleProtectionDisable) s.conf.HTTPRegister("POST", "/control/dns_config", s.handleSetConfig)
s.conf.HTTPRegister("POST", "/control/set_upstreams_config", s.handleSetUpstreamConfig) s.conf.HTTPRegister("POST", "/control/set_upstreams_config", s.handleSetUpstreamConfig)
s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS) s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS)
s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList) s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList)
s.conf.HTTPRegister("POST", "/control/access/set", s.handleAccessSet) s.conf.HTTPRegister("POST", "/control/access/set", s.handleAccessSet)
} }

View File

@ -147,6 +147,30 @@ Response:
} }
### Set DNS general settings: POST /control/dns_config
Replaces these API methods:
POST /control/enable_protection
POST /control/disable_protection
Request:
POST /control/dns_config
{
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "nxdomain" | "null_ip" | "custom_ip",
"blocking_ipv4": "1.2.3.4",
"blocking_ipv6": "1:2:3::4",
}
Response:
200 OK
## v0.99: incompatible API changes ## v0.99: incompatible API changes
* A note about web user authentication * A note about web user authentication

View File

@ -70,22 +70,31 @@ paths:
schema: schema:
$ref: "#/definitions/ServerStatus" $ref: "#/definitions/ServerStatus"
/enable_protection: /dns_info:
post: get:
tags: tags:
- global - global
operationId: enableProtection operationId: dnsInfo
summary: "Enable protection (turns on dnsfilter module in coredns)" summary: 'Get general DNS parameters'
responses: responses:
200: 200:
description: OK description: OK
schema:
$ref: "#/definitions/DNSConfig"
/disable_protection: /dns_config:
post: post:
tags: tags:
- global - global
operationId: disableProtection operationId: dnsConfig
summary: "Disable protection (turns off filtering, sb, parental, safesearch temporarily by disabling dnsfilter module in coredns)" summary: "Set general DNS parameters"
consumes:
- application/json
parameters:
- in: "body"
name: "body"
schema:
$ref: "#/definitions/DNSConfig"
responses: responses:
200: 200:
description: OK description: OK
@ -1054,6 +1063,28 @@ definitions:
language: language:
type: "string" type: "string"
example: "en" example: "en"
DNSConfig:
type: "object"
description: "Query log configuration"
properties:
protection_enabled:
type: "boolean"
ratelimit:
type: "integer"
blocking_mode:
type: "string"
enum:
- "nxdomain"
- "null_ip"
- "custom_ip"
blocking_ipv4:
type: "string"
blocking_ipv6:
type: "string"
edns_cs_enabled:
type: "boolean"
UpstreamsConfig: UpstreamsConfig:
type: "object" type: "object"
description: "Upstreams configuration" description: "Upstreams configuration"