diff --git a/AGHTechDoc.md b/AGHTechDoc.md
index c6f615f3..f4e4d4c7 100644
--- a/AGHTechDoc.md
+++ b/AGHTechDoc.md
@@ -24,6 +24,9 @@ Contents:
* "Enable DHCP" command
* Static IP check/set
* Add a static lease
+* DNS access settings
+ * List access settings
+ * Set access settings
## First startup
@@ -626,3 +629,47 @@ Response:
Error response (Client not found):
400
+
+
+## DNS access settings
+
+There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.
+
+There are 3 types of access settings:
+* allowed_clients: Only these clients are allowed to make DNS requests.
+* disallowed_clients: These clients are not allowed to make DNS requests.
+* blocked_hosts: These hosts are not allowed to be resolved by a DNS request.
+
+
+### List access settings
+
+Request:
+
+ GET /control/access/list
+
+Response:
+
+ 200 OK
+
+ {
+ allowed_clients: ["127.0.0.1", ...]
+ disallowed_clients: ["127.0.0.1", ...]
+ blocked_hosts: ["host.com", ...]
+ }
+
+
+### Set access settings
+
+Request:
+
+ POST /control/access/set
+
+ {
+ allowed_clients: ["127.0.0.1", ...]
+ disallowed_clients: ["127.0.0.1", ...]
+ blocked_hosts: ["host.com", ...]
+ }
+
+Response:
+
+ 200 OK
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 697dbbcd..d53593c5 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -298,5 +298,14 @@
"clients_not_found": "No clients found",
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
"auto_clients_title": "Clients (runtime)",
- "auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration"
-}
\ No newline at end of file
+ "auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration",
+ "access_title": "Access settings",
+ "access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
+ "access_allowed_title": "Allowed clients",
+ "access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
+ "access_disallowed_title": "Disallowed clients",
+ "access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
+ "access_blocked_title": "Blocked domains",
+ "access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
+ "access_settings_saved": "Access settings successfully saved"
+}
diff --git a/client/src/actions/access.js b/client/src/actions/access.js
new file mode 100644
index 00000000..b10062cb
--- /dev/null
+++ b/client/src/actions/access.js
@@ -0,0 +1,45 @@
+import { createAction } from 'redux-actions';
+import Api from '../api/Api';
+import { addErrorToast, addSuccessToast } from './index';
+import { normalizeTextarea } from '../helpers/helpers';
+
+const apiClient = new Api();
+
+export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');
+export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE');
+export const getAccessListSuccess = createAction('GET_ACCESS_LIST_SUCCESS');
+
+export const getAccessList = () => async (dispatch) => {
+ dispatch(getAccessListRequest());
+ try {
+ const data = await apiClient.getAccessList();
+ dispatch(getAccessListSuccess(data));
+ } catch (error) {
+ dispatch(addErrorToast({ error }));
+ dispatch(getAccessListFailure());
+ }
+};
+
+export const setAccessListRequest = createAction('SET_ACCESS_LIST_REQUEST');
+export const setAccessListFailure = createAction('SET_ACCESS_LIST_FAILURE');
+export const setAccessListSuccess = createAction('SET_ACCESS_LIST_SUCCESS');
+
+export const setAccessList = config => async (dispatch) => {
+ dispatch(setAccessListRequest());
+ try {
+ const { allowed_clients, disallowed_clients, blocked_hosts } = config;
+
+ const values = {
+ allowed_clients: (allowed_clients && normalizeTextarea(allowed_clients)) || [],
+ disallowed_clients: (disallowed_clients && normalizeTextarea(disallowed_clients)) || [],
+ blocked_hosts: (blocked_hosts && normalizeTextarea(blocked_hosts)) || [],
+ };
+
+ await apiClient.setAccessList(values);
+ dispatch(setAccessListSuccess());
+ dispatch(addSuccessToast('access_settings_saved'));
+ } catch (error) {
+ dispatch(addErrorToast({ error }));
+ dispatch(setAccessListFailure());
+ }
+};
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index 81bce7cf..1fa852f2 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -460,4 +460,22 @@ export default class Api {
};
return this.makeRequest(path, method, parameters);
}
+
+ // DNS access settings
+ ACCESS_LIST = { path: 'access/list', method: 'GET' };
+ ACCESS_SET = { path: 'access/set', method: 'POST' };
+
+ getAccessList() {
+ const { path, method } = this.ACCESS_LIST;
+ return this.makeRequest(path, method);
+ }
+
+ setAccessList(config) {
+ const { path, method } = this.ACCESS_SET;
+ const parameters = {
+ data: config,
+ headers: { 'Content-Type': 'application/json' },
+ };
+ return this.makeRequest(path, method, parameters);
+ }
}
diff --git a/client/src/components/Settings/Access/Form.js b/client/src/components/Settings/Access/Form.js
new file mode 100644
index 00000000..9096102d
--- /dev/null
+++ b/client/src/components/Settings/Access/Form.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Field, reduxForm } from 'redux-form';
+import { Trans, withNamespaces } from 'react-i18next';
+import flow from 'lodash/flow';
+
+const Form = (props) => {
+ const { handleSubmit, submitting, invalid } = props;
+
+ return (
+
+ );
+};
+
+Form.propTypes = {
+ handleSubmit: PropTypes.func,
+ submitting: PropTypes.bool,
+ invalid: PropTypes.bool,
+ initialValues: PropTypes.object,
+ t: PropTypes.func,
+};
+
+export default flow([withNamespaces(), reduxForm({ form: 'accessForm' })])(Form);
diff --git a/client/src/components/Settings/Access/index.js b/client/src/components/Settings/Access/index.js
new file mode 100644
index 00000000..77ccc265
--- /dev/null
+++ b/client/src/components/Settings/Access/index.js
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withNamespaces } from 'react-i18next';
+
+import Form from './Form';
+import Card from '../../ui/Card';
+
+class Access extends Component {
+ handleFormSubmit = (values) => {
+ this.props.setAccessList(values);
+ };
+
+ render() {
+ const { t, access } = this.props;
+
+ const {
+ processing,
+ processingSet,
+ ...values
+ } = access;
+
+ return (
+
+
+
+ );
+ }
+}
+
+Access.propTypes = {
+ access: PropTypes.object.isRequired,
+ setAccessList: PropTypes.func.isRequired,
+ t: PropTypes.func.isRequired,
+};
+
+export default withNamespaces()(Access);
diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css
index 281330df..7e410a0c 100644
--- a/client/src/components/Settings/Settings.css
+++ b/client/src/components/Settings/Settings.css
@@ -63,6 +63,10 @@
font-weight: 700;
}
+.form__label--with-desc {
+ margin-bottom: 0;
+}
+
.form__status {
margin-top: 10px;
font-size: 14px;
diff --git a/client/src/components/Settings/Upstream/Form.js b/client/src/components/Settings/Upstream/Form.js
index 8ef916f5..37990e42 100644
--- a/client/src/components/Settings/Upstream/Form.js
+++ b/client/src/components/Settings/Upstream/Form.js
@@ -62,7 +62,7 @@ let Form = (props) => {
-