diff --git a/AGHTechDoc.md b/AGHTechDoc.md index bd526fc8..54becca5 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -31,6 +31,9 @@ Contents: * API: List rewrite entries * API: Add a rewrite entry * API: Remove a rewrite entry +* Services Filter + * API: Get blocked services list + * API: Set blocked services list ## First startup @@ -536,6 +539,8 @@ Notes: * If `use_global_settings` is false, then the client-specific settings are used to override (enable or disable) global settings. +* If `use_global_blocked_services` is false, then the client-specific settings are used to override (enable or disable) global Blocked Services settings. + ### Get list of clients @@ -558,6 +563,8 @@ Response: parental_enabled: false safebrowsing_enabled: false safesearch_enabled: false + use_global_blocked_services: true + blocked_services: [ "name1", ... ] } ] auto_clients: [ @@ -585,6 +592,8 @@ Request: parental_enabled: false safebrowsing_enabled: false safesearch_enabled: false + use_global_blocked_services: true + blocked_services: [ "name1", ... ] } Response: @@ -613,6 +622,8 @@ Request: parental_enabled: false safebrowsing_enabled: false safesearch_enabled: false + use_global_blocked_services: true + blocked_services: [ "name1", ... ] } } @@ -743,3 +754,49 @@ Request: Response: 200 OK + + +## Services Filter + +Allows to quickly block popular sites globally or for specific client only. +UI manages these settings via global or per-client API. +UI and server have the same list of the services supported and this list must always be in synchronization. +UI code also contains icons for each service: `client/src/components/ui/Icons.js`. + +How it works: +* UI presents the list of services which user may want to block +* Admin clicks on the checkboxes in front of the services to block and presses Save +* UI sends `Set blocked services list` or `Update client` message +* Server updates the internal configuration +* When a user sends a DNS request for a host which is blocked by these settings, he won't receive its IP address +* Query log will show that this request was blocked by "Blocked services" + +Internally, all supported services are stored as a map: + + service name -> list of rules + + +### API: Get blocked services list + +Request: + + GET /control/blocked_services/list + +Response: + + 200 OK + + [ "name1", ... ] + + +### API: Set blocked services list + +Request: + + POST /control/blocked_services/set + + [ "name1", ... ] + +Response: + + 200 OK diff --git a/client/package-lock.json b/client/package-lock.json index cedee41d..0c596c27 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -945,9 +945,9 @@ } }, "axios": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", - "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", "requires": { "follow-redirects": "1.5.10", "is-buffer": "^2.0.2" diff --git a/client/package.json b/client/package.json index dc64d590..10080eb5 100644 --- a/client/package.json +++ b/client/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@nivo/line": "^0.49.1", - "axios": "^0.18.1", + "axios": "^0.19.0", "classnames": "^2.2.6", "date-fns": "^1.29.0", "file-saver": "^1.3.8", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 4aaac0e0..6a013cf6 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -344,5 +344,14 @@ "form_answer": "Enter IP address or domain name", "form_error_domain_format": "Invalid domain format", "form_error_answer_format": "Invalid answer format", - "configure": "Configure" + "configure": "Configure", + "main_settings": "Main settings", + "block_services": "Block specific services", + "blocked_services": "Blocked services", + "blocked_services_desc": "Allows to quickly block popular sites and services.", + "blocked_services_saved": "Blocked services successfully saved", + "blocked_services_global": "Use global blocked services", + "blocked_service": "Blocked service", + "block_all": "Block all", + "unblock_all": "Unblock all" } diff --git a/client/src/actions/services.js b/client/src/actions/services.js new file mode 100644 index 00000000..7aae500f --- /dev/null +++ b/client/src/actions/services.js @@ -0,0 +1,37 @@ +import { createAction } from 'redux-actions'; +import Api from '../api/Api'; +import { addErrorToast, addSuccessToast } from './index'; + +const apiClient = new Api(); + +export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST'); +export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE'); +export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS'); + +export const getBlockedServices = () => async (dispatch) => { + dispatch(getBlockedServicesRequest()); + try { + const data = await apiClient.getBlockedServices(); + dispatch(getBlockedServicesSuccess(data)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getBlockedServicesFailure()); + } +}; + +export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST'); +export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE'); +export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS'); + +export const setBlockedServices = values => async (dispatch) => { + dispatch(setBlockedServicesRequest()); + try { + await apiClient.setBlockedServices(values); + dispatch(setBlockedServicesSuccess()); + dispatch(getBlockedServices()); + dispatch(addSuccessToast('blocked_services_saved')); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setBlockedServicesFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 766cd499..a857766c 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -509,4 +509,22 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // Blocked services + BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' }; + BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' }; + + getBlockedServices() { + const { path, method } = this.BLOCKED_SERVICES_LIST; + return this.makeRequest(path, method); + } + + setBlockedServices(config) { + const { path, method } = this.BLOCKED_SERVICES_SET; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } } diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 3205e424..cd105626 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -19,6 +19,11 @@ overflow: hidden; } +.logs__row--icons { + max-width: 180px; + flex-flow: row wrap; +} + .logs__row .list-unstyled { margin-bottom: 0; overflow: hidden; @@ -26,6 +31,7 @@ .logs__text, .logs__row .list-unstyled li { + padding: 0 1px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index f8891206..93a1e420 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -8,6 +8,7 @@ import { Trans, withNamespaces } from 'react-i18next'; import { HashLink as Link } from 'react-router-hash-link'; import { formatTime, getClientName } from '../../helpers/helpers'; +import { SERVICES } from '../../helpers/constants'; import { getTrackerData } from '../../helpers/trackers/trackers'; import PageTitle from '../ui/PageTitle'; import Card from '../ui/Card'; @@ -39,12 +40,8 @@ class Logs extends Component { } } - renderTooltip(isFiltered, rule, filter) { - if (rule) { - return (isFiltered && ); - } - return ''; - } + renderTooltip = (isFiltered, rule, filter, service) => + isFiltered && ; toggleBlocking = (type, domain) => { const { userRules } = this.props.filtering; @@ -146,6 +143,21 @@ class Logs extends Component { } } + if (reason === 'FilteredBlockedService') { + const getService = SERVICES + .find(service => service.id === row.original.serviceName); + const serviceName = getService && getService.name; + + return ( +
+ + {parsedFilteredReason} + + {this.renderTooltip(isFiltered, '', '', serviceName)} +
+ ); + } + if (isFiltered) { return (
diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index c12fa1e3..1e98da08 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -17,10 +17,19 @@ class ClientsTable extends Component { }; handleSubmit = (values) => { + let config = values; + + if (values && values.blocked_services) { + const blocked_services = Object + .keys(values.blocked_services) + .filter(service => values.blocked_services[service]); + config = { ...values, blocked_services }; + } + if (this.props.modalType === MODAL_TYPE.EDIT) { - this.handleFormUpdate(values, this.props.modalClientName); + this.handleFormUpdate(config, this.props.modalClientName); } else { - this.handleFormAdd(values); + this.handleFormAdd(config); } }; @@ -41,6 +50,7 @@ class ClientsTable extends Component { return { identifier, use_global_settings: true, + use_global_blocked_services: true, ...client, }; } @@ -48,6 +58,7 @@ class ClientsTable extends Component { return { identifier: CLIENT_ID.IP, use_global_settings: true, + use_global_blocked_services: true, }; }; @@ -116,6 +127,27 @@ class ClientsTable extends Component { ); }, }, + { + Header: this.props.t('blocked_services'), + accessor: 'blocked_services', + Cell: (row) => { + const { value, original } = row; + + if (original.use_global_blocked_services) { + return settings_global; + } + + return ( +
+ {value && value.length > 0 ? value.map(service => ( + + + + )) : '–'} +
+ ); + }, + }, { Header: this.props.t('table_statistics'), accessor: 'statistics', diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index b10487c4..58112401 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -5,18 +5,46 @@ import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form'; -import { CLIENT_ID } from '../../../helpers/constants'; +import Tabs from '../../ui/Tabs'; +import { toggleAllServices } from '../../../helpers/helpers'; +import { renderField, renderRadioField, renderSelectField, renderServiceField, ipv4, mac, required } from '../../../helpers/form'; +import { CLIENT_ID, SERVICES } from '../../../helpers/constants'; +import './Service.css'; + +const settingsCheckboxes = [ + { + name: 'use_global_settings', + placeholder: 'client_global_settings', + }, + { + name: 'filtering_enabled', + placeholder: 'block_domain_use_filters_and_hosts', + }, + { + name: 'safebrowsing_enabled', + placeholder: 'use_adguard_browsing_sec', + }, + { + name: 'parental_enabled', + placeholder: 'use_adguard_parental', + }, + { + name: 'safesearch_enabled', + placeholder: 'enforce_safe_search', + }, +]; let Form = (props) => { const { t, handleSubmit, reset, + change, pristine, submitting, clientIdentifier, useGlobalSettings, + useGlobalServices, toggleClientModal, processingAdding, processingUpdating, @@ -26,57 +54,70 @@ let Form = (props) => {
-
+
client_identifier -
- {clientIdentifier === CLIENT_ID.IP && ( -
+
+
+ {clientIdentifier === CLIENT_ID.IP && ( +
+ +
+ )} + {clientIdentifier === CLIENT_ID.MAC && ( +
+ +
+ )} +
+
- )} - {clientIdentifier === CLIENT_ID.MAC && ( -
- -
- )} +
{
-
- -
- -
- - settings - -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
+ +
+ {settingsCheckboxes.map(setting => ( +
+ +
+ ))} +
+
+
+ +
+
+ +
+
+ +
+
+
+ {SERVICES.map(service => ( + + ))} +
+
+
+
@@ -188,10 +224,12 @@ Form.propTypes = { pristine: PropTypes.bool.isRequired, handleSubmit: PropTypes.func.isRequired, reset: PropTypes.func.isRequired, + change: PropTypes.func.isRequired, submitting: PropTypes.bool.isRequired, toggleClientModal: PropTypes.func.isRequired, clientIdentifier: PropTypes.string, useGlobalSettings: PropTypes.bool, + useGlobalServices: PropTypes.bool, t: PropTypes.func.isRequired, processingAdding: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired, @@ -202,9 +240,11 @@ const selector = formValueSelector('clientForm'); Form = connect((state) => { const clientIdentifier = selector(state, 'identifier'); const useGlobalSettings = selector(state, 'use_global_settings'); + const useGlobalServices = selector(state, 'use_global_blocked_services'); return { clientIdentifier, useGlobalSettings, + useGlobalServices, }; })(Form); diff --git a/client/src/components/Settings/Clients/Modal.js b/client/src/components/Settings/Clients/Modal.js index 49e3483e..d5687344 100644 --- a/client/src/components/Settings/Clients/Modal.js +++ b/client/src/components/Settings/Clients/Modal.js @@ -6,6 +6,24 @@ import ReactModal from 'react-modal'; import { MODAL_TYPE } from '../../../helpers/constants'; import Form from './Form'; +const getInitialData = (initial) => { + if (initial && initial.blocked_services) { + const { blocked_services } = initial; + const blocked = {}; + + blocked_services.forEach((service) => { + blocked[service] = true; + }); + + return { + ...initial, + blocked_services: blocked, + }; + } + + return initial; +}; + const Modal = (props) => { const { isModalOpen, @@ -16,6 +34,7 @@ const Modal = (props) => { processingAdding, processingUpdating, } = props; + const initialData = getInitialData(currentClientData); return ( {
{ + const { + handleSubmit, + change, + pristine, + submitting, + processing, + processingSet, + } = props; + + return ( + +
+
+
+ +
+
+ +
+
+
+ {SERVICES.map(service => ( + + ))} +
+
+ +
+ +
+ + ); +}; + +Form.propTypes = { + pristine: PropTypes.bool.isRequired, + handleSubmit: PropTypes.func.isRequired, + change: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + processing: PropTypes.bool.isRequired, + processingSet: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, +}; + +export default flow([ + withNamespaces(), + reduxForm({ + form: 'servicesForm', + enableReinitialize: true, + }), +])(Form); diff --git a/client/src/components/Settings/Services/index.js b/client/src/components/Settings/Services/index.js new file mode 100644 index 00000000..362f1c96 --- /dev/null +++ b/client/src/components/Settings/Services/index.js @@ -0,0 +1,69 @@ +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 Services extends Component { + handleSubmit = (values) => { + let config = values; + + if (values && values.blocked_services) { + const blocked_services = Object + .keys(values.blocked_services) + .filter(service => values.blocked_services[service]); + config = blocked_services; + } + + this.props.setBlockedServices(config); + }; + + + getInitialDataForServices = (initial) => { + if (initial) { + const blocked = {}; + + initial.forEach((service) => { + blocked[service] = true; + }); + + return { + blocked_services: blocked, + }; + } + + return initial; + }; + + + render() { + const { services, t } = this.props; + const initialData = this.getInitialDataForServices(services.list); + + return ( + +
+
+
+
+ ); + } +} + +Services.propTypes = { + t: PropTypes.func.isRequired, + services: PropTypes.object.isRequired, + setBlockedServices: PropTypes.func.isRequired, +}; + +export default withNamespaces()(Services); diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 48acf4eb..7f12dbbe 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -11,11 +11,20 @@ margin-bottom: 20px; } +.form__inline { + display: flex; + justify-content: flex-start; +} + .btn-standard { padding-left: 20px; padding-right: 20px; } +.btn-large { + min-width: 150px; +} + .form-control--textarea { min-height: 110px; } diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 1e072311..7391cbaf 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { withNamespaces, Trans } from 'react-i18next'; +import Services from './Services'; import Checkbox from '../ui/Checkbox'; import Loading from '../ui/Loading'; import PageTitle from '../ui/PageTitle'; @@ -35,6 +36,7 @@ class Settings extends Component { componentDidMount() { this.props.initSettings(this.settings); + this.props.getBlockedServices(); } renderSettings = (settings) => { @@ -59,7 +61,9 @@ class Settings extends Component { }; render() { - const { settings, t } = this.props; + const { + settings, services, setBlockedServices, t, + } = this.props; return ( @@ -74,6 +78,12 @@ class Settings extends Component {
+
+ +
)} diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js index ac9a2a5b..14405ef8 100644 --- a/client/src/components/ui/Icons.js +++ b/client/src/components/ui/Icons.js @@ -63,6 +63,70 @@ const Icons = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/client/src/components/ui/Popover.css b/client/src/components/ui/Popover.css index f7e23836..707affbc 100644 --- a/client/src/components/ui/Popover.css +++ b/client/src/components/ui/Popover.css @@ -109,9 +109,11 @@ width: 20px; height: 20px; stroke: #9aa0ac; + color: #9aa0ac; } .popover__icon--green { + color: #66b574; stroke: #66b574; } diff --git a/client/src/components/ui/PopoverFilter.js b/client/src/components/ui/PopoverFilter.js index e54cf204..f10cc8a1 100644 --- a/client/src/components/ui/PopoverFilter.js +++ b/client/src/components/ui/PopoverFilter.js @@ -6,19 +6,36 @@ import './Popover.css'; class PopoverFilter extends Component { render() { + const { rule, filter, service } = this.props; + + if (!rule && !service) { + return ''; + } + return (
- + + +
-
- rule_label: {this.props.rule} -
- {this.props.filter &&
- filter_label: {this.props.filter} -
} + {rule && ( +
+ rule_label: {rule} +
+ )} + {filter && ( +
+ filter_label: {filter} +
+ )} + {service && ( +
+ blocked_service: {service} +
+ )}
@@ -27,8 +44,9 @@ class PopoverFilter extends Component { } PopoverFilter.propTypes = { - rule: PropTypes.string.isRequired, + rule: PropTypes.string, filter: PropTypes.string, + service: PropTypes.string, }; export default withNamespaces()(PopoverFilter); diff --git a/client/src/components/ui/Tabs.css b/client/src/components/ui/Tabs.css index cd1671e9..1e6f75c6 100644 --- a/client/src/components/ui/Tabs.css +++ b/client/src/components/ui/Tabs.css @@ -6,6 +6,20 @@ border-bottom: 1px solid #e8e8e8; } +.tabs__controls--form { + justify-content: flex-start; +} + +.tabs__controls--form .tab__control { + min-width: initial; + margin-right: 25px; + font-size: 14px; +} + +.tabs__controls--form .tab__icon { + display: none; +} + .tab__control { display: flex; flex-direction: column; diff --git a/client/src/components/ui/Tabs.js b/client/src/components/ui/Tabs.js index a15b0ee6..7da68fba 100644 --- a/client/src/components/ui/Tabs.js +++ b/client/src/components/ui/Tabs.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import classnames from 'classnames'; import Tab from './Tab'; import './Tabs.css'; @@ -16,6 +17,7 @@ class Tabs extends Component { render() { const { props: { + controlClass, children, }, state: { @@ -23,9 +25,14 @@ class Tabs extends Component { }, } = this; + const getControlClass = classnames({ + tabs__controls: true, + [`tabs__controls--${controlClass}`]: controlClass, + }); + return (
-
+
{children.map((child) => { const { label, title } = child.props; @@ -54,6 +61,7 @@ class Tabs extends Component { } Tabs.propTypes = { + controlClass: PropTypes.string, children: PropTypes.array.isRequired, }; diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js index e50290f1..054d1d1b 100644 --- a/client/src/containers/Settings.js +++ b/client/src/containers/Settings.js @@ -1,11 +1,13 @@ import { connect } from 'react-redux'; import { initSettings, toggleSetting } from '../actions'; +import { getBlockedServices, setBlockedServices } from '../actions/services'; import Settings from '../components/Settings'; const mapStateToProps = (state) => { - const { settings } = state; + const { settings, services } = state; const props = { settings, + services, }; return props; }; @@ -13,6 +15,8 @@ const mapStateToProps = (state) => { const mapDispatchToProps = { initSettings, toggleSetting, + getBlockedServices, + setBlockedServices, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 896d873e..a5c8e922 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -181,3 +181,66 @@ export const CLIENT_ID = { }; export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients']; + +export const SERVICES = [ + { + id: 'facebook', + name: 'Facebook', + }, + { + id: 'whatsapp', + name: 'WhatsApp', + }, + { + id: 'instagram', + name: 'Instagram', + }, + { + id: 'twitter', + name: 'Twitter', + }, + { + id: 'youtube', + name: 'YouTube', + }, + { + id: 'netflix', + name: 'Netflix', + }, + { + id: 'snapchat', + name: 'Snapchat', + }, + { + id: 'messenger', + name: 'Messenger', + }, + { + id: 'twitch', + name: 'Twitch', + }, + { + id: 'discord', + name: 'Discord', + }, + { + id: 'skype', + name: 'Skype', + }, + { + id: 'steam', + name: 'Steam', + }, + { + id: 'ok', + name: 'OK', + }, + { + id: 'vk', + name: 'VK', + }, + { + id: 'mail_ru', + name: 'mail.ru', + }, +]; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index c4ffcb73..e3b6e46c 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -27,6 +27,23 @@ export const renderField = ({ ); +export const renderRadioField = ({ + input, placeholder, disabled, meta: { touched, error }, +}) => ( + + + {!disabled && touched && (error && {error})} + +); + export const renderSelectField = ({ input, placeholder, disabled, meta: { touched, error }, }) => ( @@ -46,6 +63,28 @@ export const renderSelectField = ({ ); +export const renderServiceField = ({ + input, placeholder, disabled, modifier, icon, meta: { touched, error }, +}) => ( + + + {!disabled && touched && (error && {error})} + +); + export const required = (value) => { if (value || value === 0) { return false; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 031747f3..0fd5baea 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -27,6 +27,7 @@ export const normalizeLogs = logs => logs.map((log) => { client, filterId, rule, + service_name, } = log; const { host: domain, type } = question; const responsesArray = response ? response.map((response) => { @@ -42,6 +43,7 @@ export const normalizeLogs = logs => logs.map((log) => { client, filterId, rule, + serviceName: service_name, }; }); @@ -225,3 +227,7 @@ export const sortClients = (clients) => { return clients.sort(compare); }; + +export const toggleAllServices = (services, change, isSelected) => { + services.forEach(service => change(`blocked_services.${service.id}`, isSelected)); +}; diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 32026a08..2913f5cc 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -10,6 +10,7 @@ import encryption from './encryption'; import clients from './clients'; import access from './access'; import rewrites from './rewrites'; +import services from './services'; const settings = handleActions({ [actions.initSettingsRequest]: state => ({ ...state, processing: true }), @@ -424,6 +425,7 @@ export default combineReducers({ clients, access, rewrites, + services, loadingBar: loadingBarReducer, form: formReducer, }); diff --git a/client/src/reducers/services.js b/client/src/reducers/services.js new file mode 100644 index 00000000..d91cadf5 --- /dev/null +++ b/client/src/reducers/services.js @@ -0,0 +1,29 @@ +import { handleActions } from 'redux-actions'; + +import * as actions from '../actions/services'; + +const services = handleActions( + { + [actions.getBlockedServicesRequest]: state => ({ ...state, processing: true }), + [actions.getBlockedServicesFailure]: state => ({ ...state, processing: false }), + [actions.getBlockedServicesSuccess]: (state, { payload }) => ({ + ...state, + list: payload, + processing: false, + }), + + [actions.setBlockedServicesRequest]: state => ({ ...state, processingSet: true }), + [actions.setBlockedServicesFailure]: state => ({ ...state, processingSet: false }), + [actions.setBlockedServicesSuccess]: state => ({ + ...state, + processingSet: false, + }), + }, + { + processing: true, + processingSet: false, + list: [], + }, +); + +export default services; diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go index 20d17493..47ce1f0e 100644 --- a/dnsfilter/dnsfilter.go +++ b/dnsfilter/dnsfilter.go @@ -39,12 +39,19 @@ const defaultParentalURL = "%s://%s/check-parental-control-hash?prefixes=%s&sens const defaultParentalSensitivity = 13 // use "TEEN" by default const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control +// ServiceEntry - blocked service array element +type ServiceEntry struct { + Name string + Rules []*urlfilter.NetworkRule +} + // RequestFilteringSettings is custom filtering settings type RequestFilteringSettings struct { FilteringEnabled bool SafeSearchEnabled bool SafeBrowsingEnabled bool ParentalEnabled bool + ServicesRules []ServiceEntry } // RewriteEntry is a rewrite array element @@ -139,6 +146,8 @@ const ( FilteredInvalid // FilteredSafeSearch - the host was replaced with safesearch variant FilteredSafeSearch + // FilteredBlockedService - the host is blocked by "blocked services" settings + FilteredBlockedService // ReasonRewrite - rewrite rule was applied ReasonRewrite @@ -155,6 +164,7 @@ func (i Reason) String() string { "FilteredParental", "FilteredInvalid", "FilteredSafeSearch", + "FilteredBlockedService", "Rewrite", } @@ -185,6 +195,9 @@ type Result struct { // for ReasonRewrite: CanonName string `json:",omitempty"` // CNAME value IPList []net.IP `json:",omitempty"` // list of IP addresses + + // for FilteredBlockedService: + ServiceName string `json:",omitempty"` // Name of the blocked service } // Matched can be used to see if any match at all was found, no matter filtered or not @@ -193,7 +206,7 @@ func (r Reason) Matched() bool { } // CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled -func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Result, error) { +func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFilteringSettings) (Result, error) { // sometimes DNS clients will try to resolve ".", which is a request to get root servers if host == "" { return Result{Reason: NotFilteredNotFound}, nil @@ -204,15 +217,6 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res return Result{}, nil } - var setts RequestFilteringSettings - setts.FilteringEnabled = true - setts.SafeSearchEnabled = d.SafeSearchEnabled - setts.SafeBrowsingEnabled = d.SafeBrowsingEnabled - setts.ParentalEnabled = d.ParentalEnabled - if len(clientAddr) != 0 && d.FilterHandler != nil { - d.FilterHandler(clientAddr, &setts) - } - var result Result var err error @@ -232,6 +236,13 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Res } } + if len(setts.ServicesRules) != 0 { + result = matchBlockedServicesRules(host, setts.ServicesRules) + if result.Reason.Matched() { + return result, nil + } + } + // check safeSearch if no match if setts.SafeSearchEnabled { result, err = d.checkSafeSearch(host) @@ -326,6 +337,26 @@ func (d *Dnsfilter) processRewrites(host string, qtype uint16) Result { return res } +func matchBlockedServicesRules(host string, svcs []ServiceEntry) Result { + req := urlfilter.NewRequestForHostname(host) + res := Result{} + + for _, s := range svcs { + for _, rule := range s.Rules { + if rule.Match(req) { + res.Reason = FilteredBlockedService + res.IsFiltered = true + res.ServiceName = s.Name + res.Rule = rule.Text() + log.Debug("Blocked Services: matched rule: %s host: %s service: %s", + res.Rule, host, s.Name) + return res + } + } + } + return res +} + func setCacheResult(cache *fastcache.Cache, host string, res Result) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) diff --git a/dnsfilter/dnsfilter_test.go b/dnsfilter/dnsfilter_test.go index 7b78b7a5..d5eabafe 100644 --- a/dnsfilter/dnsfilter_test.go +++ b/dnsfilter/dnsfilter_test.go @@ -10,10 +10,14 @@ import ( "testing" "time" + "github.com/AdguardTeam/urlfilter" "github.com/bluele/gcache" "github.com/miekg/dns" + "github.com/stretchr/testify/assert" ) +var setts RequestFilteringSettings + // HELPERS // SAFE BROWSING // SAFE SEARCH @@ -44,10 +48,16 @@ func _Func() string { } func NewForTest(c *Config, filters map[int]string) *Dnsfilter { + setts = RequestFilteringSettings{} + setts.FilteringEnabled = true if c != nil { c.SafeBrowsingCacheSize = 1024 c.SafeSearchCacheSize = 1024 c.ParentalCacheSize = 1024 + + setts.SafeSearchEnabled = c.SafeSearchEnabled + setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled + setts.ParentalEnabled = c.ParentalEnabled } d := New(c, filters) purgeCaches() @@ -56,7 +66,7 @@ func NewForTest(c *Config, filters map[int]string) *Dnsfilter { func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } @@ -67,7 +77,7 @@ func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) { func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype uint16) { t.Helper() - ret, err := d.CheckHost(hostname, qtype, "") + ret, err := d.CheckHost(hostname, qtype, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } @@ -81,7 +91,7 @@ func (d *Dnsfilter) checkMatchIP(t *testing.T, hostname string, ip string, qtype func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) { t.Helper() - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", hostname, err) } @@ -212,7 +222,7 @@ func TestCheckHostSafeSearchYandex(t *testing.T) { // Check host for each domain for _, host := range yandex { - result, err := d.CheckHost(host, dns.TypeA, "") + result, err := d.CheckHost(host, dns.TypeA, &setts) if err != nil { t.Errorf("SafeSearch doesn't work for yandex domain `%s` cause %s", host, err) } @@ -232,7 +242,7 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) { // Check host for each domain for _, host := range googleDomains { - result, err := d.CheckHost(host, dns.TypeA, "") + result, err := d.CheckHost(host, dns.TypeA, &setts) if err != nil { t.Errorf("SafeSearch doesn't work for %s cause %s", host, err) } @@ -252,7 +262,7 @@ func TestSafeSearchCacheYandex(t *testing.T) { var err error // Check host with disabled safesearch - result, err = d.CheckHost(domain, dns.TypeA, "") + result, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("Cannot check host due to %s", err) } @@ -263,7 +273,7 @@ func TestSafeSearchCacheYandex(t *testing.T) { d = NewForTest(&Config{SafeSearchEnabled: true}, nil) defer d.Destroy() - result, err = d.CheckHost(domain, dns.TypeA, "") + result, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) } @@ -289,7 +299,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) { d := NewForTest(nil, nil) defer d.Destroy() domain := "www.google.ru" - result, err := d.CheckHost(domain, dns.TypeA, "") + result, err := d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("Cannot check host due to %s", err) } @@ -320,7 +330,7 @@ func TestSafeSearchCacheGoogle(t *testing.T) { } } - result, err = d.CheckHost(domain, dns.TypeA, "") + result, err = d.CheckHost(domain, dns.TypeA, &setts) if err != nil { t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) } @@ -433,7 +443,7 @@ func TestMatching(t *testing.T) { d := NewForTest(nil, filters) defer d.Destroy() - ret, err := d.CheckHost(test.hostname, dns.TypeA, "") + ret, err := d.CheckHost(test.hostname, dns.TypeA, &setts) if err != nil { t.Errorf("Error while matching host %s: %s", test.hostname, err) } @@ -449,10 +459,16 @@ func TestMatching(t *testing.T) { // CLIENT SETTINGS -func applyClientSettings(clientAddr string, setts *RequestFilteringSettings) { +func applyClientSettings(setts *RequestFilteringSettings) { setts.FilteringEnabled = false setts.ParentalEnabled = false setts.SafeBrowsingEnabled = true + + rule, _ := urlfilter.NewNetworkRule("||facebook.com^", 0) + s := ServiceEntry{} + s.Name = "facebook" + s.Rules = []*urlfilter.NetworkRule{rule} + setts.ServicesRules = append(setts.ServicesRules, s) } // Check behaviour without any per-client settings, @@ -468,43 +484,51 @@ func TestClientSettings(t *testing.T) { // no client settings: // blocked by filters - r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("example.org", dns.TypeA, &setts) if !r.IsFiltered || r.Reason != FilteredBlackList { t.Fatalf("CheckHost FilteredBlackList") } // blocked by parental - r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts) if !r.IsFiltered || r.Reason != FilteredParental { t.Fatalf("CheckHost FilteredParental") } // safesearch is disabled - r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts) if r.IsFiltered { t.Fatalf("CheckHost safesearch") } + // not blocked + r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts) + assert.True(t, !r.IsFiltered) + // override client settings: - d.FilterHandler = applyClientSettings + applyClientSettings(&setts) // override filtering settings - r, _ = d.CheckHost("example.org", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("example.org", dns.TypeA, &setts) if r.IsFiltered { t.Fatalf("CheckHost") } // override parental settings (force disable parental) - r, _ = d.CheckHost("pornhub.com", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts) if r.IsFiltered { t.Fatalf("CheckHost") } // override safesearch settings (force enable safesearch) - r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, "1.1.1.1") + r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts) if !r.IsFiltered || r.Reason != FilteredSafeBrowsing { t.Fatalf("CheckHost FilteredSafeBrowsing") } + + // blocked by additional rules + r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts) + assert.True(t, r.IsFiltered && r.Reason == FilteredBlockedService) } // BENCHMARKS @@ -514,7 +538,7 @@ func BenchmarkSafeBrowsing(b *testing.B) { defer d.Destroy() for n := 0; n < b.N; n++ { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } @@ -530,7 +554,7 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { hostname := "wmconvirus.narod.ru" - ret, err := d.CheckHost(hostname, dns.TypeA, "") + ret, err := d.CheckHost(hostname, dns.TypeA, &setts) if err != nil { b.Errorf("Error while matching host %s: %s", hostname, err) } diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index 1661f68b..a5aa81fd 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -96,6 +96,10 @@ type FilteringConfig struct { ParentalBlockHost string `yaml:"parental_block_host"` SafeBrowsingBlockHost string `yaml:"safebrowsing_block_host"` + // Names of services to block (globally). + // Per-client settings can override this configuration. + BlockedServices []string `json:"blocked_services"` + dnsfilter.Config `yaml:",inline"` } @@ -529,7 +533,17 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error if d.Addr != nil { clientAddr, _, _ = net.SplitHostPort(d.Addr.String()) } - res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, clientAddr) + + var setts dnsfilter.RequestFilteringSettings + setts.FilteringEnabled = true + setts.SafeSearchEnabled = s.conf.SafeSearchEnabled + setts.SafeBrowsingEnabled = s.conf.SafeBrowsingEnabled + setts.ParentalEnabled = s.conf.ParentalEnabled + if s.conf.FilterHandler != nil { + s.conf.FilterHandler(clientAddr, &setts) + } + + res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, &setts) if err != nil { // Return immediately if there's an error return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host) diff --git a/dnsforward/querylog.go b/dnsforward/querylog.go index aac4b25a..230cffd2 100644 --- a/dnsforward/querylog.go +++ b/dnsforward/querylog.go @@ -186,6 +186,10 @@ func (l *queryLog) getQueryLog() []map[string]interface{} { jsonEntry["filterId"] = entry.Result.FilterID } + if len(entry.Result.ServiceName) != 0 { + jsonEntry["service_name"] = entry.Result.ServiceName + } + answers := answerToMap(a) if answers != nil { jsonEntry["answer"] = answers diff --git a/home/blocked_services.go b/home/blocked_services.go new file mode 100644 index 00000000..23b74c51 --- /dev/null +++ b/home/blocked_services.go @@ -0,0 +1,119 @@ +package home + +import ( + "encoding/json" + "net/http" + + "github.com/AdguardTeam/AdGuardHome/dnsfilter" + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/urlfilter" +) + +var serviceRules map[string][]*urlfilter.NetworkRule // service name -> filtering rules + +type svc struct { + name string + rules []string +} + +// Keep in sync with: +// client/src/helpers/constants.js +// client/src/components/ui/Icons.js +var serviceRulesArray = []svc{ + {"whatsapp", []string{"||whatsapp.net^"}}, + {"facebook", []string{"||facebook.com^"}}, + {"twitter", []string{"||twitter.com^", "||t.co^", "||twimg.com^"}}, + {"youtube", []string{"||youtube.com^", "||ytimg.com^"}}, + {"messenger", []string{"||fb.com^", "||facebook.com^"}}, + {"twitch", []string{"||twitch.tv^", "||ttvnw.net^"}}, + {"netflix", []string{"||nflxext.com^", "||netflix.com^"}}, + {"instagram", []string{"||instagram.com^"}}, + {"snapchat", []string{"||snapchat.com^"}}, + {"discord", []string{"||discord.gg^", "||discordapp.net^", "||discordapp.com^"}}, + {"ok", []string{"||ok.ru^"}}, + {"skype", []string{"||skype.com^"}}, + {"vk", []string{"||vk.com^"}}, + {"steam", []string{"||steam.com^"}}, + {"mail_ru", []string{"||mail.ru^"}}, +} + +// convert array to map +func initServices() { + serviceRules = make(map[string][]*urlfilter.NetworkRule) + for _, s := range serviceRulesArray { + rules := []*urlfilter.NetworkRule{} + for _, text := range s.rules { + rule, err := urlfilter.NewNetworkRule(text, 0) + if err != nil { + log.Error("urlfilter.NewNetworkRule: %s rule: %s", err, text) + continue + } + rules = append(rules, rule) + } + serviceRules[s.name] = rules + } +} + +// ApplyBlockedServices - set blocked services settings for this DNS request +func ApplyBlockedServices(setts *dnsfilter.RequestFilteringSettings, list []string) { + setts.ServicesRules = []dnsfilter.ServiceEntry{} + for _, name := range list { + rules, ok := serviceRules[name] + + if !ok { + log.Error("unknown service name: %s", name) + continue + } + + s := dnsfilter.ServiceEntry{} + s.Name = name + s.Rules = rules + setts.ServicesRules = append(setts.ServicesRules, s) + } +} + +func handleBlockedServicesList(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + + config.RLock() + list := config.DNS.BlockedServices + config.RUnlock() + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(list) + if err != nil { + httpError(w, http.StatusInternalServerError, "json.Encode: %s", err) + return + } +} + +func handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) + + list := []string{} + err := json.NewDecoder(r.Body).Decode(&list) + if err != nil { + httpError(w, http.StatusBadRequest, "json.Decode: %s", err) + return + } + + config.Lock() + config.DNS.BlockedServices = list + config.Unlock() + + log.Debug("Updated blocked services list: %d", len(list)) + + err = writeAllConfigsAndReloadDNS() + if err != nil { + httpError(w, http.StatusBadRequest, "%s", err) + return + } + + returnOK(w) +} + +// RegisterBlockedServicesHandlers - register HTTP handlers +func RegisterBlockedServicesHandlers() { + http.HandleFunc("/control/blocked_services/list", postInstall(optionalAuth(ensureGET(handleBlockedServicesList)))) + http.HandleFunc("/control/blocked_services/set", postInstall(optionalAuth(ensurePOST(handleBlockedServicesSet)))) +} diff --git a/home/clients.go b/home/clients.go index 0c003a56..66253844 100644 --- a/home/clients.go +++ b/home/clients.go @@ -31,6 +31,9 @@ type Client struct { SafeSearchEnabled bool SafeBrowsingEnabled bool ParentalEnabled bool + + UseOwnBlockedServices bool // false: use global settings + BlockedServices []string } type clientJSON struct { @@ -42,6 +45,9 @@ type clientJSON struct { ParentalEnabled bool `json:"parental_enabled"` SafeSearchEnabled bool `json:"safebrowsing_enabled"` SafeBrowsingEnabled bool `json:"safesearch_enabled"` + + UseGlobalBlockedServices bool `json:"use_global_blocked_services"` + BlockedServices []string `json:"blocked_services"` } type clientSource uint @@ -391,6 +397,9 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) { ParentalEnabled: c.ParentalEnabled, SafeSearchEnabled: c.SafeSearchEnabled, SafeBrowsingEnabled: c.SafeBrowsingEnabled, + + UseGlobalBlockedServices: !c.UseOwnBlockedServices, + BlockedServices: c.BlockedServices, } if len(c.MAC) != 0 { @@ -438,6 +447,9 @@ func jsonToClient(cj clientJSON) (*Client, error) { ParentalEnabled: cj.ParentalEnabled, SafeSearchEnabled: cj.SafeSearchEnabled, SafeBrowsingEnabled: cj.SafeBrowsingEnabled, + + UseOwnBlockedServices: !cj.UseGlobalBlockedServices, + BlockedServices: cj.BlockedServices, } return &c, nil } diff --git a/home/config.go b/home/config.go index 9fa4cbad..f606ae03 100644 --- a/home/config.go +++ b/home/config.go @@ -37,6 +37,9 @@ type clientObject struct { ParentalEnabled bool `yaml:"parental_enabled"` SafeSearchEnabled bool `yaml:"safebrowsing_enabled"` SafeBrowsingEnabled bool `yaml:"safesearch_enabled"` + + UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"` + BlockedServices []string `yaml:"blocked_services"` } type HTTPSServer struct { @@ -266,6 +269,9 @@ func parseConfig() error { ParentalEnabled: cy.ParentalEnabled, SafeSearchEnabled: cy.SafeSearchEnabled, SafeBrowsingEnabled: cy.SafeBrowsingEnabled, + + UseOwnBlockedServices: !cy.UseGlobalBlockedServices, + BlockedServices: cy.BlockedServices, } _, err = config.clients.Add(cli) if err != nil { @@ -317,6 +323,9 @@ func (c *configuration) write() error { ParentalEnabled: cli.ParentalEnabled, SafeSearchEnabled: cli.SafeSearchEnabled, SafeBrowsingEnabled: cli.SafeBrowsingEnabled, + + UseGlobalBlockedServices: !cli.UseOwnBlockedServices, + BlockedServices: cli.BlockedServices, } config.Clients = append(config.Clients, cy) } diff --git a/home/control.go b/home/control.go index daaf5dbd..1b535e3b 100644 --- a/home/control.go +++ b/home/control.go @@ -1022,6 +1022,7 @@ func registerControlHandlers() { RegisterTLSHandlers() RegisterClientsHandlers() registerRewritesHandlers() + RegisterBlockedServicesHandlers() http.HandleFunc("/dns-query", postInstall(handleDOH)) } diff --git a/home/dns.go b/home/dns.go index b195bd40..b474bc5d 100644 --- a/home/dns.go +++ b/home/dns.go @@ -54,6 +54,7 @@ func initDNSServer(baseDir string) { log.Error("upstream.AddressToUpstream: %s", err) return } + config.dnsctx.rdnsIP = make(map[string]bool) config.dnsctx.rdnsChannel = make(chan string, 256) go asyncRDNSLoop() @@ -210,19 +211,35 @@ func generateServerConfig() (dnsforward.ServerConfig, error) { newconfig.Upstreams = upstreamConfig.Upstreams newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams newconfig.AllServers = config.DNS.AllServers - newconfig.FilterHandler = applyClientSettings + newconfig.FilterHandler = applyAdditionalFiltering newconfig.OnDNSRequest = onDNSRequest return newconfig, nil } // If a client has his own settings, apply them -func applyClientSettings(clientAddr string, setts *dnsfilter.RequestFilteringSettings) { +func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) { + + ApplyBlockedServices(setts, config.DNS.BlockedServices) + + if len(clientAddr) == 0 { + return + } + c, ok := config.clients.Find(clientAddr) - if !ok || !c.UseOwnSettings { + if !ok { return } log.Debug("Using settings for client with IP %s", clientAddr) + + if c.UseOwnBlockedServices { + ApplyBlockedServices(setts, c.BlockedServices) + } + + if !c.UseOwnSettings { + return + } + setts.FilteringEnabled = c.FilteringEnabled setts.SafeSearchEnabled = c.SafeSearchEnabled setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled diff --git a/home/home.go b/home/home.go index 4d711559..3064ad80 100644 --- a/home/home.go +++ b/home/home.go @@ -101,6 +101,7 @@ func run(args options) { initConfig() config.clients.Init() + initServices() if !config.firstRun { // Do the upgrade if necessary diff --git a/home/upgrade.go b/home/upgrade.go index 135fd05c..491e5a68 100644 --- a/home/upgrade.go +++ b/home/upgrade.go @@ -10,7 +10,7 @@ import ( yaml "gopkg.in/yaml.v2" ) -const currentSchemaVersion = 3 // used for upgrading from old configs to new config +const currentSchemaVersion = 4 // used for upgrading from old configs to new config // Performs necessary upgrade operations if needed func upgradeConfig() error { @@ -53,20 +53,28 @@ func upgradeConfig() error { func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error { switch oldVersion { case 0: - err := upgradeSchema0to3(diskConfig) + err := upgradeSchema0to1(diskConfig) if err != nil { return err } + fallthrough case 1: - err := upgradeSchema1to3(diskConfig) + err := upgradeSchema1to2(diskConfig) if err != nil { return err } + fallthrough case 2: err := upgradeSchema2to3(diskConfig) if err != nil { return err } + fallthrough + case 3: + err := upgradeSchema3to4(diskConfig) + if err != nil { + return err + } default: err := fmt.Errorf("configuration file contains unknown schema_version, abort") log.Println(err) @@ -173,22 +181,35 @@ func upgradeSchema2to3(diskConfig *map[string]interface{}) error { return nil } -// jump three schemas at once -- this time we just do it sequentially -func upgradeSchema0to3(diskConfig *map[string]interface{}) error { - err := upgradeSchema0to1(diskConfig) - if err != nil { - return err +// Add use_global_blocked_services=true setting for existing "clients" array +func upgradeSchema3to4(diskConfig *map[string]interface{}) error { + log.Printf("%s(): called", _Func()) + + (*diskConfig)["schema_version"] = 4 + + clients, ok := (*diskConfig)["clients"] + if !ok { + return nil } - return upgradeSchema1to3(diskConfig) -} + switch arr := clients.(type) { + case []interface{}: -// jump two schemas at once -- this time we just do it sequentially -func upgradeSchema1to3(diskConfig *map[string]interface{}) error { - err := upgradeSchema1to2(diskConfig) - if err != nil { - return err + for i := range arr { + + switch c := arr[i].(type) { + + case map[interface{}]interface{}: + c["use_global_blocked_services"] = true + + default: + continue + } + } + + default: + return nil } - return upgradeSchema2to3(diskConfig) + return nil } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d5ea137d..d3a50a08 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -786,6 +786,35 @@ paths: 200: description: OK + + /blocked_services/list: + get: + tags: + - blocked_services + operationId: blockedServicesList + summary: 'Get blocked services list' + responses: + 200: + description: OK + schema: + $ref: "#/definitions/BlockedServicesArray" + + /blocked_services/set: + post: + tags: + - blocked_services + operationId: blockedServicesSet + summary: 'Set blocked services list' + parameters: + - in: body + name: "body" + schema: + $ref: "#/definitions/BlockedServicesArray" + responses: + 200: + description: OK + + # -------------------------------------------------- # Rewrite methods # -------------------------------------------------- @@ -1572,6 +1601,12 @@ definitions: type: "boolean" safesearch_enabled: type: "boolean" + use_global_blocked_services: + type: "boolean" + blocked_services: + type: "array" + items: + type: "string" ClientAuto: type: "object" description: "Auto-Client information" @@ -1638,6 +1673,11 @@ definitions: description: "value of A, AAAA or CNAME DNS record" example: "127.0.0.1" + BlockedServicesArray: + type: "array" + items: + type: "string" + CheckConfigRequest: type: "object" description: "Configuration to be checked"