+ clients: improve logic when processing "disallowed IP" property for a client

#1925

* commit 'c72cd58f69b71e4981d8b182b2f7d53ea5e30868':
  + client: Move the client access check to the server-side
  Improve the clients/find API response
  + GET /control/clients/find: add "disallowed" property
This commit is contained in:
Simon Zolin 2020-09-24 16:30:10 +03:00
commit 6222d17d86
23 changed files with 348 additions and 443 deletions

View File

@ -942,7 +942,7 @@ Error response (Client not found):
### API: Find clients by IP ### API: Find clients by IP
This method returns the list of clients (manual and auto-clients) matching the IP list. This method returns the list of clients (manual and auto-clients) matching the IP list.
For auto-clients only `name`, `ids` and `whois_info` fields are set. Other fields are empty. For auto-clients only `name`, `ids`, `whois_info`, `disallowed`, and `disallowed_rule` fields are set. Other fields are empty.
Request: Request:
@ -968,11 +968,16 @@ Response:
key: "value" key: "value"
... ...
} }
"disallowed": false,
"disallowed_rule": "..."
} }
} }
... ...
] ]
* `disallowed` - whether the client's IP is blocked or not.
* `disallowed_rule` - the rule due to which the client is disallowed. If `disallowed` is `true`, and this string is empty - it means that the client IP is disallowed by the "allowed IP list", i.e. it is not included in allowed.
## DNS general settings ## DNS general settings

View File

@ -582,5 +582,6 @@
"click_to_view_queries": "Click to view queries", "click_to_view_queries": "Click to view queries",
"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.", "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.", "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"
} }

View File

@ -1,136 +1,5 @@
import { import { sortIp, countClientsStatistics, findAddressType } from '../helpers/helpers';
countClientsStatistics, findAddressType, getIpMatchListStatus, sortIp, import { ADDRESS_TYPES } from '../helpers/constants';
} from '../helpers/helpers';
import { ADDRESS_TYPES, IP_MATCH_LIST_STATUS } from '../helpers/constants';
describe('getIpMatchListStatus', () => {
describe('IPv4', () => {
test('should return EXACT on find the exact ip match', () => {
const list = `127.0.0.2
2001:db8:11a3:9d7:0:0:0:0
192.168.0.1/8
127.0.0.1
127.0.0.3`;
expect(getIpMatchListStatus('127.0.0.1', list))
.toEqual(IP_MATCH_LIST_STATUS.EXACT);
});
test('should return CIDR on find the cidr match', () => {
const list = `127.0.0.2
2001:db8:11a3:9d7:0:0:0:0
192.168.0.1/8
127.0.0.0/24
127.0.0.3`;
expect(getIpMatchListStatus('127.0.0.1', list))
.toEqual(IP_MATCH_LIST_STATUS.CIDR);
});
test('should return NOT_FOUND if the ip is not in the list', () => {
const list = `127.0.0.1
2001:db8:11a3:9d7:0:0:0:0
192.168.0.1/8
127.0.0.2
127.0.0.3`;
expect(getIpMatchListStatus('127.0.0.4', list))
.toEqual(IP_MATCH_LIST_STATUS.NOT_FOUND);
});
test('should return the first EXACT or CIDR match in the list', () => {
const list1 = `2001:db8:11a3:9d7:0:0:0:0
127.0.0.1
127.0.0.8/24
127.0.0.3`;
expect(getIpMatchListStatus('127.0.0.1', list1))
.toEqual(IP_MATCH_LIST_STATUS.EXACT);
const list2 = `2001:db8:11a3:9d7:ffff:ffff:ffff:ffff
2001:0db8:11a3:09d7:0000:0000:0000:0000/64
127.0.0.0/24
127.0.0.1
127.0.0.8/24
127.0.0.3`;
expect(getIpMatchListStatus('127.0.0.1', list2))
.toEqual(IP_MATCH_LIST_STATUS.CIDR);
});
});
describe('IPv6', () => {
test('should return EXACT on find the exact ip match', () => {
const list = `127.0.0.0
2001:db8:11a3:9d7:0:0:0:0
2001:db8:11a3:9d7:ffff:ffff:ffff:ffff
127.0.0.1`;
expect(getIpMatchListStatus('2001:db8:11a3:9d7:0:0:0:0', list))
.toEqual(IP_MATCH_LIST_STATUS.EXACT);
});
test('should return EXACT on find the exact ip match of short and long notation', () => {
const list = `127.0.0.0
192.168.0.1/8
2001:db8::
127.0.0.2`;
expect(getIpMatchListStatus('2001:db8:0:0:0:0:0:0', list))
.toEqual(IP_MATCH_LIST_STATUS.EXACT);
});
test('should return CIDR on find the cidr match', () => {
const list1 = `2001:0db8:11a3:09d7:0000:0000:0000:0000/64
127.0.0.1
127.0.0.2`;
expect(getIpMatchListStatus('2001:db8:11a3:9d7:0:0:0:0', list1))
.toEqual(IP_MATCH_LIST_STATUS.CIDR);
const list2 = `2001:0db8::/16
127.0.0.0
2001:db8:11a3:9d7:0:0:0:0
2001:db8::
2001:db8:11a3:9d7:ffff:ffff:ffff:ffff
127.0.0.1`;
expect(getIpMatchListStatus('2001:db1::', list2))
.toEqual(IP_MATCH_LIST_STATUS.CIDR);
});
test('should return NOT_FOUND if the ip is not in the list', () => {
const list = `2001:db8:11a3:9d7:0:0:0:0
2001:0db8:11a3:09d7:0000:0000:0000:0000/64
127.0.0.1
127.0.0.2`;
expect(getIpMatchListStatus('::', list))
.toEqual(IP_MATCH_LIST_STATUS.NOT_FOUND);
});
test('should return the first EXACT or CIDR match in the list', () => {
const list1 = `2001:db8:11a3:9d7:0:0:0:0
2001:0db8:11a3:09d7:0000:0000:0000:0000/64
127.0.0.3`;
expect(getIpMatchListStatus('2001:db8:11a3:9d7:0:0:0:0', list1))
.toEqual(IP_MATCH_LIST_STATUS.EXACT);
const list2 = `2001:0db8:11a3:09d7:0000:0000:0000:0000/64
2001:db8:11a3:9d7:0:0:0:0
127.0.0.3`;
expect(getIpMatchListStatus('2001:db8:11a3:9d7:0:0:0:0', list2))
.toEqual(IP_MATCH_LIST_STATUS.CIDR);
});
});
describe('Empty list or IP', () => {
test('should return NOT_FOUND on empty ip', () => {
const list = `127.0.0.0
2001:db8:11a3:9d7:0:0:0:0
2001:db8:11a3:9d7:ffff:ffff:ffff:ffff
127.0.0.1`;
expect(getIpMatchListStatus('', list))
.toEqual(IP_MATCH_LIST_STATUS.NOT_FOUND);
});
test('should return NOT_FOUND on empty list', () => {
const list = '';
expect(getIpMatchListStatus('127.0.0.1', list))
.toEqual(IP_MATCH_LIST_STATUS.NOT_FOUND);
});
});
});
describe('sortIp', () => { describe('sortIp', () => {
describe('ipv4', () => { describe('ipv4', () => {

View File

@ -3,7 +3,6 @@ import i18next from 'i18next';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
import { BLOCK_ACTIONS } from '../helpers/constants';
import { splitByNewLine } from '../helpers/helpers'; import { splitByNewLine } from '../helpers/helpers';
export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST'); export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');
@ -49,19 +48,16 @@ export const toggleClientBlockRequest = createAction('TOGGLE_CLIENT_BLOCK_REQUES
export const toggleClientBlockFailure = createAction('TOGGLE_CLIENT_BLOCK_FAILURE'); export const toggleClientBlockFailure = createAction('TOGGLE_CLIENT_BLOCK_FAILURE');
export const toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS'); export const toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS');
export const toggleClientBlock = (type, ip) => async (dispatch) => { export const toggleClientBlock = (ip, disallowed, disallowed_rule) => async (dispatch) => {
dispatch(toggleClientBlockRequest()); dispatch(toggleClientBlockRequest());
try { try {
const { const {
allowed_clients, disallowed_clients, blocked_hosts, allowed_clients, blocked_hosts, disallowed_clients = [],
} = await apiClient.getAccessList(); } = await apiClient.getAccessList();
let updatedDisallowedClients = disallowed_clients || [];
if (type === BLOCK_ACTIONS.UNBLOCK && updatedDisallowedClients.includes(ip)) { const updatedDisallowedClients = disallowed
updatedDisallowedClients = updatedDisallowedClients.filter((client) => client !== ip); ? disallowed_clients.filter((client) => client !== disallowed_rule)
} else if (type === BLOCK_ACTIONS.BLOCK && !updatedDisallowedClients.includes(ip)) { : disallowed_clients.concat(ip);
updatedDisallowedClients.push(ip);
}
const values = { const values = {
allowed_clients, allowed_clients,
@ -72,9 +68,9 @@ export const toggleClientBlock = (type, ip) => async (dispatch) => {
await apiClient.setAccessList(values); await apiClient.setAccessList(values);
dispatch(toggleClientBlockSuccess(values)); dispatch(toggleClientBlockSuccess(values));
if (type === BLOCK_ACTIONS.UNBLOCK) { if (disallowed) {
dispatch(addSuccessToast(i18next.t('client_unblocked', { ip }))); dispatch(addSuccessToast(i18next.t('client_unblocked', { ip: disallowed_rule })));
} else if (type === BLOCK_ACTIONS.BLOCK) { } else {
dispatch(addSuccessToast(i18next.t('client_blocked', { ip }))); dispatch(addSuccessToast(i18next.t('client_blocked', { ip })));
} }
} catch (error) { } catch (error) {

View File

@ -7,6 +7,17 @@ import {
} from '../helpers/constants'; } from '../helpers/constants';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
const enrichWithClientInfo = async (logs) => {
const clientsParams = getParamsForClientsSearch(logs, 'client');
if (Object.keys(clientsParams).length > 0) {
const clients = await apiClient.findClients(clientsParams);
return addClientInfo(logs, clients, 'client');
}
return logs;
};
const getLogsWithParams = async (config) => { const getLogsWithParams = async (config) => {
const { older_than, filter, ...values } = config; const { older_than, filter, ...values } = config;
const rawLogs = await apiClient.getQueryLog({ const rawLogs = await apiClient.getQueryLog({
@ -14,13 +25,8 @@ const getLogsWithParams = async (config) => {
older_than, older_than,
}); });
const { data, oldest } = rawLogs; const { data, oldest } = rawLogs;
let logs = normalizeLogs(data); const normalizedLogs = normalizeLogs(data);
const clientsParams = getParamsForClientsSearch(logs, 'client'); const logs = await enrichWithClientInfo(normalizedLogs);
if (Object.keys(clientsParams).length > 0) {
const clients = await apiClient.findClients(clientsParams);
logs = addClientInfo(logs, clients, 'client');
}
return { return {
logs, logs,
@ -82,6 +88,23 @@ export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsFailure = createAction('GET_LOGS_FAILURE');
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const updateLogs = () => async (dispatch, getState) => {
try {
const { logs, oldest, older_than } = getState().queryLogs;
const enrichedLogs = await enrichWithClientInfo(logs);
dispatch(getLogsSuccess({
logs: enrichedLogs,
oldest,
older_than,
}));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error));
}
};
export const getLogs = () => async (dispatch, getState) => { export const getLogs = () => async (dispatch, getState) => {
dispatch(getLogsRequest()); dispatch(getLogsRequest());
try { try {

View File

@ -8,10 +8,11 @@ import classNames from 'classnames';
import Card from '../ui/Card'; import Card from '../ui/Card';
import Cell from '../ui/Cell'; import Cell from '../ui/Cell';
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers'; import { getPercent, sortIp } from '../../helpers/helpers';
import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants'; import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants';
import { toggleClientBlock } from '../../actions/access'; import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell'; import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
import { getStats } from '../../actions/stats';
const getClientsPercentColor = (percent) => { const getClientsPercentColor = (percent) => {
if (percent > 50) { if (percent > 50) {
@ -33,59 +34,51 @@ const CountCell = (row) => {
return <Cell value={value} percent={percent} color={percentColor} search={ip} />; return <Cell value={value} percent={percent} color={percentColor} search={ip} />;
}; };
const renderBlockingButton = (ip) => { const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const processingSet = useSelector((state) => state.access.processingSet); const processingSet = useSelector((state) => state.access.processingSet);
const disallowed_clients = useSelector(
(state) => state.access.disallowed_clients, shallowEqual,
);
const ipMatchListStatus = getIpMatchListStatus(ip, disallowed_clients);
if (ipMatchListStatus === IP_MATCH_LIST_STATUS.CIDR) {
return null;
}
const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND;
const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
const text = type;
const buttonClass = classNames('button-action button-action--main', { const buttonClass = classNames('button-action button-action--main', {
'button-action--unblock': !isNotFound, 'button-action--unblock': disallowed,
}); });
const toggleClientStatus = (type, ip) => { const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
const confirmMessage = type === BLOCK_ACTIONS.BLOCK const confirmMessage = disallowed
? `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}` ? t('client_confirm_unblock', { ip: disallowed_rule })
: t('client_confirm_unblock', { ip }); : `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`;
if (window.confirm(confirmMessage)) { if (window.confirm(confirmMessage)) {
dispatch(toggleClientBlock(type, ip)); await dispatch(toggleClientBlock(ip, disallowed, disallowed_rule));
await dispatch(getStats());
} }
}; };
const onClick = () => toggleClientStatus(type, ip); const onClick = () => toggleClientStatus(ip, disallowed, disallowed_rule);
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const isNotInAllowedList = disallowed && disallowed_rule === '';
return <div className="table__action pl-4"> return <div className="table__action pl-4">
<button <button
type="button" type="button"
className={buttonClass} className={buttonClass}
onClick={onClick} onClick={isNotInAllowedList ? undefined : onClick}
disabled={processingSet} disabled={isNotInAllowedList || processingSet}
> title={t(isNotInAllowedList ? 'client_not_in_allowed_clients' : text)}
<Trans>{text}</Trans> >
</button> <Trans>{text}</Trans>
</div>; </button>
</div>;
}; };
const ClientCell = (row) => { const ClientCell = (row) => {
const { value, original: { info } } = row; const { value, original: { info, info: { disallowed, disallowed_rule } } } = row;
return <> return <>
<div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center"> <div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
{renderFormattedClientCell(value, info, true)} {renderFormattedClientCell(value, info, true)}
{renderBlockingButton(value)} {renderBlockingButton(value, disallowed, disallowed_rule)}
</div> </div>
</>; </>;
}; };
@ -96,7 +89,6 @@ const Clients = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const topClients = useSelector((state) => state.stats.topClients, shallowEqual); const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
const disallowedClients = useSelector((state) => state.access.disallowed_clients, shallowEqual);
return <Card return <Card
title={t('top_clients')} title={t('top_clients')}
@ -138,9 +130,9 @@ const Clients = ({
return {}; return {};
} }
const { ip } = rowInfo.original; const { info: { disallowed } } = rowInfo.original;
return getIpMatchListStatus(ip, disallowedClients) === IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'logs__row--red' }; return disallowed ? { className: 'logs__row--red' } : {};
}} }}
/> />
</Card>; </Card>;

View File

@ -11,12 +11,16 @@ import IconTooltip from './IconTooltip';
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell'; import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
import { toggleClientBlock } from '../../../actions/access'; import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo } from './helpers'; import { getBlockClientInfo } from './helpers';
import { getStats } from '../../../actions/stats';
import { updateLogs } from '../../../actions/queryLogs';
const ClientCell = ({ const ClientCell = ({
client, client,
domain, domain,
info, info,
info: { name, whois_info }, info: {
name, whois_info, disallowed, disallowed_rule,
},
reason, reason,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -26,11 +30,6 @@ const ClientCell = ({
const isDetailed = useSelector((state) => state.queryLogs.isDetailed); const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const [isOptionsOpened, setOptionsOpened] = useState(false); const [isOptionsOpened, setOptionsOpened] = useState(false);
const disallowed_clients = useSelector(
(state) => state.access.disallowed_clients,
shallowEqual,
);
const autoClient = autoClients.find((autoClient) => autoClient.name === client); const autoClient = autoClients.find((autoClient) => autoClient.name === client);
const source = autoClient?.source; const source = autoClient?.source;
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0; const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
@ -66,43 +65,50 @@ const ClientCell = ({
const { const {
confirmMessage, confirmMessage,
buttonKey: blockingClientKey, buttonKey: blockingClientKey,
type, isNotInAllowedList,
} = getBlockClientInfo(client, disallowed_clients); } = getBlockClientInfo(client, disallowed, disallowed_rule);
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only'; const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client); const clientNameBlockingFor = getBlockingClientName(clients, client);
const BUTTON_OPTIONS_TO_ACTION_MAP = { const BUTTON_OPTIONS = [
[blockingForClientKey]: () => { {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor)); name: blockingForClientKey,
onClick: () => {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
},
}, },
[blockingClientKey]: () => { {
const message = `${type === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`; name: blockingClientKey,
if (window.confirm(message)) { onClick: async () => {
dispatch(toggleClientBlock(type, client)); if (window.confirm(confirmMessage)) {
} await dispatch(toggleClientBlock(client, disallowed, disallowed_rule));
await dispatch(updateLogs());
}
},
disabled: isNotInAllowedList,
}, },
}; ];
const onClick = async () => { const onClick = async () => {
await dispatch(toggleBlocking(buttonType, domain)); await dispatch(toggleBlocking(buttonType, domain));
await dispatch(getStats());
}; };
const getOptions = (optionToActionMap) => { const getOptions = (options) => {
const options = Object.entries(optionToActionMap);
if (options.length === 0) { if (options.length === 0) {
return null; return null;
} }
return <>{options return <>{options.map(({ name, onClick, disabled }) => <button
.map(([name, onClick]) => <div
key={name} key={name}
className="button-action--arrow-option px-4 py-2" className="button-action--arrow-option px-4 py-2"
onClick={onClick} onClick={onClick}
disabled={disabled}
>{t(name)} >{t(name)}
</div>)}</>; </button>)}</>;
}; };
const content = getOptions(BUTTON_OPTIONS_TO_ACTION_MAP); const content = getOptions(BUTTON_OPTIONS);
const buttonClass = classNames('button-action button-action--main', { const buttonClass = classNames('button-action button-action--main', {
'button-action--unblock': isFiltered, 'button-action--unblock': isFiltered,
@ -168,6 +174,8 @@ ClientCell.propTypes = {
city: propTypes.string, city: propTypes.string,
orgname: propTypes.string, orgname: propTypes.string,
}), }),
disallowed: propTypes.bool.isRequired,
disallowed_rule: propTypes.string.isRequired,
}), }),
]), ]),
reason: propTypes.string.isRequired, reason: propTypes.string.isRequired,

View File

@ -1,19 +1,18 @@
import { getIpMatchListStatus } from '../../../../helpers/helpers'; import i18next from 'i18next';
import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS } from '../../../../helpers/constants';
export const BUTTON_PREFIX = 'btn_'; export const BUTTON_PREFIX = 'btn_';
export const getBlockClientInfo = (client, disallowed_clients) => { export const getBlockClientInfo = (ip, disallowed, disallowed_rule) => {
const ipMatchListStatus = getIpMatchListStatus(client, disallowed_clients); const confirmMessage = disallowed
? i18next.t('client_confirm_unblock', { ip: disallowed_rule })
: `${i18next.t('adg_will_drop_dns_queries')} ${i18next.t('client_confirm_block', { ip })}`;
const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND; const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client');
const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK; const isNotInAllowedList = disallowed && disallowed_rule === '';
const confirmMessage = isNotFound ? 'client_confirm_block' : 'client_confirm_unblock';
const buttonKey = isNotFound ? 'disallow_this_client' : 'allow_this_client';
return { return {
confirmMessage, confirmMessage,
buttonKey, buttonKey,
type, isNotInAllowedList,
}; };
}; };

View File

@ -32,6 +32,7 @@ import ClientCell from './ClientCell';
import '../Logs.css'; import '../Logs.css';
import { toggleClientBlock } from '../../../actions/access'; import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo, BUTTON_PREFIX } from './helpers'; import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
import { updateLogs } from '../../../actions/queryLogs';
const Row = memo(({ const Row = memo(({
style, style,
@ -49,21 +50,19 @@ const Row = memo(({
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual); const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
const disallowed_clients = useSelector(
(state) => state.access.disallowed_clients,
shallowEqual,
);
const clients = useSelector((state) => state.dashboard.clients); const clients = useSelector((state) => state.dashboard.clients);
const onClick = () => { const onClick = () => {
if (!isSmallScreen) { return; } if (!isSmallScreen) {
return;
}
const { const {
answer_dnssec, answer_dnssec,
client, client,
domain, domain,
elapsedMs, elapsedMs,
info, info,
info: { disallowed, disallowed_rule },
reason, reason,
response, response,
time, time,
@ -94,7 +93,7 @@ const Row = memo(({
const isFiltered = checkFiltered(reason); const isFiltered = checkFiltered(reason);
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE; || reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK; const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const onToggleBlock = () => { const onToggleBlock = () => {
@ -113,8 +112,8 @@ const Row = memo(({
const { const {
confirmMessage, confirmMessage,
buttonKey: blockingClientKey, buttonKey: blockingClientKey,
type: blockType, isNotInAllowedList,
} = getBlockClientInfo(client, disallowed_clients); } = getBlockClientInfo(client, disallowed, disallowed_rule);
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only'; const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client); const clientNameBlockingFor = getBlockingClientName(clients, client);
@ -123,13 +122,33 @@ const Row = memo(({
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor)); dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
}; };
const onBlockingClientClick = () => { const onBlockingClientClick = async () => {
const message = `${blockType === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`; if (window.confirm(confirmMessage)) {
if (window.confirm(message)) { await dispatch(toggleClientBlock(client, disallowed, disallowed_rule));
dispatch(toggleClientBlock(blockType, client)); await dispatch(updateLogs());
setModalOpened(false);
} }
}; };
const blockButton = <button
className={classNames('title--border text-center button-action--arrow-option', { 'bg--danger': !isBlocked })}
onClick={onToggleBlock}>
{t(buttonType)}
</button>;
const blockForClientButton = <button
className='text-center font-weight-bold py-2 button-action--arrow-option'
onClick={onBlockingForClientClick}>
{t(blockingForClientKey)}
</button>;
const blockClientButton = <button
className='text-center font-weight-bold py-2 button-action--arrow-option'
onClick={onBlockingClientClick}
disabled={isNotInAllowedList}>
{t(blockingClientKey)}
</button>;
const detailedData = { const detailedData = {
time_table_header: formatTime(time, LONG_TIME_FORMAT), time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS), date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
@ -144,12 +163,12 @@ const Row = memo(({
table_name: tracker?.name, table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
tracker_source: hasTracker && sourceData tracker_source: hasTracker && sourceData
&& <a && <a
href={sourceData.url} href={sourceData.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="link--green">{sourceData.name} className="link--green">{sourceData.name}
</a>, </a>,
response_details: 'title', response_details: 'title',
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
@ -166,12 +185,9 @@ const Row = memo(({
source_label: source, source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false, validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
original_response: originalResponse?.join('\n'), original_response: originalResponse?.join('\n'),
[BUTTON_PREFIX + buttonType]: <div onClick={onToggleBlock} [BUTTON_PREFIX + buttonType]: blockButton,
className={classNames('title--border text-center', { [BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,
'bg--danger': isBlocked, [BUTTON_PREFIX + blockingClientKey]: blockClientButton,
})}>{t(buttonType)}</div>,
[BUTTON_PREFIX + blockingForClientKey]: <div onClick={onBlockingForClientClick} className='text-center font-weight-bold py-2'>{t(blockingForClientKey)}</div>,
[BUTTON_PREFIX + blockingClientKey]: <div onClick={onBlockingClientClick} className='text-center font-weight-bold py-2'>{t(blockingClientKey)}</div>,
}; };
setDetailedDataCurrent(processContent(detailedData)); setDetailedDataCurrent(processContent(detailedData));

View File

@ -96,7 +96,7 @@
} }
.bg--danger { .bg--danger {
color: var(--danger); color: var(--danger) !important;
} }
.form-control--search { .form-control--search {
@ -293,7 +293,20 @@
background: var(--btn-unblock-disabled); background: var(--btn-unblock-disabled);
} }
.button-action--arrow-option:hover { .button-action--arrow-option {
background: transparent;
border: 0;
display: block;
width: 100%;
text-align: left;
color: inherit;
}
.button-action--arrow-option:disabled {
display: none;
}
.tooltip-custom__container .button-action--arrow-option:not(:disabled):hover {
cursor: pointer; cursor: pointer;
background: var(--gray-f3); background: var(--gray-f3);
overflow: hidden; overflow: hidden;
@ -327,7 +340,7 @@
} }
.logs__row--red { .logs__row--red {
background-color: var(--red); background-color: var(--red) !important;
} }
.logs__row--white { .logs__row--white {

View File

@ -479,12 +479,6 @@ export const DNS_REQUEST_OPTIONS = {
LOAD_BALANCING: '', LOAD_BALANCING: '',
}; };
export const IP_MATCH_LIST_STATUS = {
NOT_FOUND: 'NOT_FOUND', // not found in the list
EXACT: 'EXACT', // found exact match (including the match of short and long forms)
CIDR: 'CIDR', // the ip is in the specified CIDR range
};
export const DHCP_FORM_NAMES = { export const DHCP_FORM_NAMES = {
DHCPv4: 'dhcpv4', DHCPv4: 'dhcpv4',
DHCPv6: 'dhcpv6', DHCPv6: 'dhcpv6',

View File

@ -25,7 +25,6 @@ import {
DHCP_VALUES_PLACEHOLDERS, DHCP_VALUES_PLACEHOLDERS,
FILTERED, FILTERED,
FILTERED_STATUS, FILTERED_STATUS,
IP_MATCH_LIST_STATUS,
SERVICES_ID_NAME_MAP, SERVICES_ID_NAME_MAP,
STANDARD_DNS_PORT, STANDARD_DNS_PORT,
STANDARD_HTTPS_PORT, STANDARD_HTTPS_PORT,
@ -133,16 +132,14 @@ export const normalizeTopStats = (stats) => (
})) }))
); );
export const addClientInfo = (data, clients, param) => ( export const addClientInfo = (data, clients, param) => data.map((row) => {
data.map((row) => { const clientIp = row[param];
const clientIp = row[param]; const info = clients.find((item) => item[clientIp]) || '';
const info = clients.find((item) => item[clientIp]) || ''; return {
return { ...row,
...row, info: info?.[clientIp] ?? '',
info: info?.[clientIp] ?? '', };
}; });
})
);
export const normalizeFilters = (filters) => ( export const normalizeFilters = (filters) => (
filters ? filters.map((filter) => { filters ? filters.map((filter) => {
@ -530,75 +527,6 @@ export const isIpInCidr = (ip, cidr) => {
} }
}; };
/**
* The purpose of this method is to quickly check
* if this IP can possibly be in the specified CIDR range.
*
* @param ip {string}
* @param listItem {string}
* @returns {boolean}
*/
const isIpQuickMatchCIDR = (ip, listItem) => {
const ipv6 = ip.indexOf(':') !== -1;
const cidrIpv6 = listItem.indexOf(':') !== -1;
if (ipv6 !== cidrIpv6) {
// CIDR is for a different IP type
return false;
}
if (cidrIpv6) {
// We don't do quick check for IPv6 addresses
return true;
}
const idx = listItem.indexOf('/');
if (idx === -1) {
// Not a CIDR, return false immediately
return false;
}
const cidrIp = listItem.substring(0, idx);
const cidrRange = parseInt(listItem.substring(idx + 1), 10);
if (Number.isNaN(cidrRange)) {
// Not a valid CIDR
return false;
}
const parts = cidrIp.split('.');
if (parts.length !== 4) {
// Invalid IP, return immediately
return false;
}
// Now depending on the range we check if the IP can possibly be in that range
if (cidrRange < 8) {
// Use the slow approach
return true;
}
if (cidrRange < 16) {
// Check the first part
// Example: 0.0.0.0/8 matches 0.*.*.*
return ip.indexOf(`${parts[0]}.`) === 0;
}
if (cidrRange < 24) {
// Check the first two parts
// Example: 0.0.0.0/16 matches 0.0.*.*
return ip.indexOf(`${parts[0]}.${parts[1]}.`) === 0;
}
if (cidrRange <= 32) {
// Check the first two parts
// Example: 0.0.0.0/16 matches 0.0.*.*
return ip.indexOf(`${parts[0]}.${parts[1]}.${parts[2]}.`) === 0;
}
// range for IPv4 CIDR cannot be more than 32
// no need to check further, this CIDR is invalid
return false;
};
/** /**
* *
* @param ipOrCidr * @param ipOrCidr
@ -622,50 +550,6 @@ export const findAddressType = (address) => {
} }
}; };
/**
* @param ip {string}
* @param list {string}
* @returns {'EXACT' | 'CIDR' | 'NOT_FOND'}
*/
export const getIpMatchListStatus = (ip, list) => {
if (!ip || !list) {
return IP_MATCH_LIST_STATUS.NOT_FOUND;
}
const listArr = list.trim()
.split('\n');
try {
for (let i = 0; i < listArr.length; i += 1) {
const listItem = listArr[i];
if (ip === listItem.trim()) {
return IP_MATCH_LIST_STATUS.EXACT;
}
// Using ipaddr.js is quite slow so we first do a quick check
// to see if it's possible that this IP may be in the specified CIDR range
if (isIpQuickMatchCIDR(ip, listItem)) {
const parsedIp = ipaddr.parse(ip);
const isItemAnIp = ipaddr.isValid(listItem);
const parsedItem = isItemAnIp ? ipaddr.parse(listItem) : ipaddr.parseCIDR(listItem);
if (isItemAnIp && parsedIp.toString() === parsedItem.toString()) {
return IP_MATCH_LIST_STATUS.EXACT;
}
if (!isItemAnIp && isIpMatchCidr(parsedIp, parsedItem)) {
return IP_MATCH_LIST_STATUS.CIDR;
}
}
}
return IP_MATCH_LIST_STATUS.NOT_FOUND;
} catch (e) {
console.error(e);
return IP_MATCH_LIST_STATUS.NOT_FOUND;
}
};
/** /**
* @param ids {string[]} * @param ids {string[]}
* @returns {Object} * @returns {Object}

View File

@ -2,7 +2,6 @@ import { handleActions } from 'redux-actions';
import * as actions from '../actions'; import * as actions from '../actions';
import { enrichWithConcatenatedIpAddresses } from '../helpers/helpers'; import { enrichWithConcatenatedIpAddresses } from '../helpers/helpers';
// todo: figure out if we cat get rid of redux-form state duplication
const dhcp = handleActions( const dhcp = handleActions(
{ {
[actions.getDhcpStatusRequest]: (state) => ({ [actions.getDhcpStatusRequest]: (state) => ({

View File

@ -44,6 +44,7 @@ const getDevServerConfig = (proxyUrl = BASE_URL) => {
proxy: { proxy: {
[proxyUrl]: `http://${devServerHost}:${port}`, [proxyUrl]: `http://${devServerHost}:${port}`,
}, },
open: true,
}; };
}; };

View File

@ -80,43 +80,46 @@ func processIPCIDRArray(dst *map[string]bool, dstIPNet *[]net.IPNet, src []strin
} }
// IsBlockedIP - return TRUE if this client should be blocked // IsBlockedIP - return TRUE if this client should be blocked
func (a *accessCtx) IsBlockedIP(ip string) bool { // Returns the item from the "disallowedClients" list that lead to blocking IP.
// If it returns TRUE and an empty string, it means that the "allowedClients" is not empty,
// but the ip does not belong to it.
func (a *accessCtx) IsBlockedIP(ip string) (bool, string) {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
if len(a.allowedClients) != 0 || len(a.allowedClientsIPNet) != 0 { if len(a.allowedClients) != 0 || len(a.allowedClientsIPNet) != 0 {
_, ok := a.allowedClients[ip] _, ok := a.allowedClients[ip]
if ok { if ok {
return false return false, ""
} }
if len(a.allowedClientsIPNet) != 0 { if len(a.allowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip) ipAddr := net.ParseIP(ip)
for _, ipnet := range a.allowedClientsIPNet { for _, ipnet := range a.allowedClientsIPNet {
if ipnet.Contains(ipAddr) { if ipnet.Contains(ipAddr) {
return false return false, ""
} }
} }
} }
return true return true, ""
} }
_, ok := a.disallowedClients[ip] _, ok := a.disallowedClients[ip]
if ok { if ok {
return true return true, ip
} }
if len(a.disallowedClientsIPNet) != 0 { if len(a.disallowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip) ipAddr := net.ParseIP(ip)
for _, ipnet := range a.disallowedClientsIPNet { for _, ipnet := range a.disallowedClientsIPNet {
if ipnet.Contains(ipAddr) { if ipnet.Contains(ipAddr) {
return true return true, ipnet.String()
} }
} }
} }
return false return false, ""
} }
// IsBlockedDomain - return TRUE if this domain should be blocked // IsBlockedDomain - return TRUE if this domain should be blocked

73
dnsforward/access_test.go Normal file
View File

@ -0,0 +1,73 @@
package dnsforward
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsBlockedIPAllowed(t *testing.T) {
a := &accessCtx{}
assert.True(t, a.Init([]string{"1.1.1.1", "2.2.0.0/16"}, nil, nil) == nil)
disallowed, disallowedRule := a.IsBlockedIP("1.1.1.1")
assert.False(t, disallowed)
assert.Equal(t, "", disallowedRule)
disallowed, disallowedRule = a.IsBlockedIP("1.1.1.2")
assert.True(t, disallowed)
assert.Equal(t, "", disallowedRule)
disallowed, disallowedRule = a.IsBlockedIP("2.2.1.1")
assert.False(t, disallowed)
assert.Equal(t, "", disallowedRule)
disallowed, disallowedRule = a.IsBlockedIP("2.3.1.1")
assert.True(t, disallowed)
assert.Equal(t, "", disallowedRule)
}
func TestIsBlockedIPDisallowed(t *testing.T) {
a := &accessCtx{}
assert.True(t, a.Init(nil, []string{"1.1.1.1", "2.2.0.0/16"}, nil) == nil)
disallowed, disallowedRule := a.IsBlockedIP("1.1.1.1")
assert.True(t, disallowed)
assert.Equal(t, "1.1.1.1", disallowedRule)
disallowed, disallowedRule = a.IsBlockedIP("1.1.1.2")
assert.False(t, disallowed)
assert.Equal(t, "", disallowedRule)
disallowed, disallowedRule = a.IsBlockedIP("2.2.1.1")
assert.True(t, disallowed)
assert.Equal(t, "2.2.0.0/16", disallowedRule)
disallowed, disallowedRule = a.IsBlockedIP("2.3.1.1")
assert.False(t, disallowed)
assert.Equal(t, "", disallowedRule)
}
func TestIsBlockedIPBlockedDomain(t *testing.T) {
a := &accessCtx{}
assert.True(t, a.Init(nil, nil, []string{"host1",
"host2",
"*.host.com",
"||host3.com^",
}) == nil)
// match by "host2.com"
assert.True(t, a.IsBlockedDomain("host1"))
assert.True(t, a.IsBlockedDomain("host2"))
assert.True(t, !a.IsBlockedDomain("host3"))
// match by wildcard "*.host.com"
assert.True(t, !a.IsBlockedDomain("host.com"))
assert.True(t, a.IsBlockedDomain("asdf.host.com"))
assert.True(t, a.IsBlockedDomain("qwer.asdf.host.com"))
assert.True(t, !a.IsBlockedDomain("asdf.zhost.com"))
// match by wildcard "||host3.com^"
assert.True(t, a.IsBlockedDomain("host3.com"))
assert.True(t, a.IsBlockedDomain("asdf.host3.com"))
}

View File

@ -301,3 +301,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.ServeHTTP(w, r) p.ServeHTTP(w, r)
} }
} }
// IsBlockedIP - return TRUE if this client should be blocked
func (s *Server) IsBlockedIP(ip string) (bool, string) {
return s.access.IsBlockedIP(ip)
}

View File

@ -912,50 +912,6 @@ func publicKey(priv interface{}) interface{} {
} }
} }
func TestIsBlockedIPAllowed(t *testing.T) {
a := &accessCtx{}
assert.True(t, a.Init([]string{"1.1.1.1", "2.2.0.0/16"}, nil, nil) == nil)
assert.True(t, !a.IsBlockedIP("1.1.1.1"))
assert.True(t, a.IsBlockedIP("1.1.1.2"))
assert.True(t, !a.IsBlockedIP("2.2.1.1"))
assert.True(t, a.IsBlockedIP("2.3.1.1"))
}
func TestIsBlockedIPDisallowed(t *testing.T) {
a := &accessCtx{}
assert.True(t, a.Init(nil, []string{"1.1.1.1", "2.2.0.0/16"}, nil) == nil)
assert.True(t, a.IsBlockedIP("1.1.1.1"))
assert.True(t, !a.IsBlockedIP("1.1.1.2"))
assert.True(t, a.IsBlockedIP("2.2.1.1"))
assert.True(t, !a.IsBlockedIP("2.3.1.1"))
}
func TestIsBlockedIPBlockedDomain(t *testing.T) {
a := &accessCtx{}
assert.True(t, a.Init(nil, nil, []string{"host1",
"host2",
"*.host.com",
"||host3.com^",
}) == nil)
// match by "host2.com"
assert.True(t, a.IsBlockedDomain("host1"))
assert.True(t, a.IsBlockedDomain("host2"))
assert.True(t, !a.IsBlockedDomain("host3"))
// match by wildcard "*.host.com"
assert.True(t, !a.IsBlockedDomain("host.com"))
assert.True(t, a.IsBlockedDomain("asdf.host.com"))
assert.True(t, a.IsBlockedDomain("qwer.asdf.host.com"))
assert.True(t, !a.IsBlockedDomain("asdf.zhost.com"))
// match by wildcard "||host3.com^"
assert.True(t, a.IsBlockedDomain("host3.com"))
assert.True(t, a.IsBlockedDomain("asdf.host3.com"))
}
func TestValidateUpstream(t *testing.T) { func TestValidateUpstream(t *testing.T) {
invalidUpstreams := []string{"1.2.3.4.5", invalidUpstreams := []string{"1.2.3.4.5",
"123.3.7m", "123.3.7m",

View File

@ -12,7 +12,8 @@ import (
func (s *Server) beforeRequestHandler(_ *proxy.Proxy, d *proxy.DNSContext) (bool, error) { func (s *Server) beforeRequestHandler(_ *proxy.Proxy, d *proxy.DNSContext) (bool, error) {
ip := ipFromAddr(d.Addr) ip := ipFromAddr(d.Addr)
if s.access.IsBlockedIP(ip) { disallowed, _ := s.access.IsBlockedIP(ip)
if disallowed {
log.Tracef("Client IP %s is blocked by settings", ip) log.Tracef("Client IP %s is blocked by settings", ip)
return false, nil return false, nil
} }

View File

@ -80,6 +80,9 @@ type clientsContainer struct {
// dhcpServer is used for looking up clients IP addresses by MAC addresses // dhcpServer is used for looking up clients IP addresses by MAC addresses
dhcpServer *dhcpd.Server dhcpServer *dhcpd.Server
// dnsServer is used for checking clients IP status access list status
dnsServer *dnsforward.Server
autoHosts *util.AutoHosts // get entries from system hosts-files autoHosts *util.AutoHosts // get entries from system hosts-files
testing bool // if TRUE, this object is used for internal tests testing bool // if TRUE, this object is used for internal tests

View File

@ -21,6 +21,17 @@ type clientJSON struct {
BlockedServices []string `json:"blocked_services"` BlockedServices []string `json:"blocked_services"`
Upstreams []string `json:"upstreams"` Upstreams []string `json:"upstreams"`
WhoisInfo map[string]interface{} `json:"whois_info"`
// Disallowed - if true -- client's IP is not disallowed
// Otherwise, it is blocked.
Disallowed bool `json:"disallowed"`
// DisallowedRule - the rule due to which the client is disallowed
// If Disallowed is true, and this string is empty - it means that the client IP
// is disallowed by the "allowed IP list", i.e. it is not included in allowed.
DisallowedRule string `json:"disallowed_rule"`
} }
type clientHostJSON struct { type clientHostJSON struct {
@ -38,7 +49,7 @@ type clientListJSON struct {
} }
// respond with information about configured clients // respond with information about configured clients
func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, _ *http.Request) {
data := clientListJSON{} data := clientListJSON{}
clients.lock.Lock() clients.lock.Lock()
@ -123,15 +134,9 @@ func clientToJSON(c *Client) clientJSON {
return cj return cj
} }
type clientHostJSONWithID struct {
IDs []string `json:"ids"`
Name string `json:"name"`
WhoisInfo map[string]interface{} `json:"whois_info"`
}
// Convert ClientHost object to JSON // Convert ClientHost object to JSON
func clientHostToJSON(ip string, ch ClientHost) clientHostJSONWithID { func clientHostToJSON(ip string, ch ClientHost) clientJSON {
cj := clientHostJSONWithID{ cj := clientJSON{
Name: ch.Host, Name: ch.Host,
IDs: []string{ip}, IDs: []string{ip},
} }
@ -255,9 +260,13 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
continue // a client with this IP isn't found continue // a client with this IP isn't found
} }
cj := clientHostToJSON(ip, ch) cj := clientHostToJSON(ip, ch)
cj.Disallowed, cj.DisallowedRule = clients.dnsServer.IsBlockedIP(ip)
el[ip] = cj el[ip] = cj
} else { } else {
cj := clientToJSON(&c) cj := clientToJSON(&c)
cj.Disallowed, cj.DisallowedRule = clients.dnsServer.IsBlockedIP(ip)
el[ip] = cj el[ip] = cj
} }

View File

@ -70,6 +70,7 @@ func initDNSServer() error {
p.DHCPServer = Context.dhcpServer p.DHCPServer = Context.dhcpServer
} }
Context.dnsServer = dnsforward.NewServer(p) Context.dnsServer = dnsforward.NewServer(p)
Context.clients.dnsServer = Context.dnsServer
dnsConfig := generateServerConfig() dnsConfig := generateServerConfig()
err = Context.dnsServer.Prepare(&dnsConfig) err = Context.dnsServer.Prepare(&dnsConfig)
if err != nil { if err != nil {

View File

@ -1765,7 +1765,61 @@ components:
properties: properties:
1.2.3.4: 1.2.3.4:
items: items:
$ref: "#/components/schemas/Client" $ref: "#/components/schemas/ClientFindSubEntry"
ClientFindSubEntry:
type: object
properties:
name:
type: string
description: Name
example: localhost
ids:
type: array
description: IP, CIDR or MAC address
items:
type: string
use_global_settings:
type: boolean
filtering_enabled:
type: boolean
parental_enabled:
type: boolean
safebrowsing_enabled:
type: boolean
safesearch_enabled:
type: boolean
use_global_blocked_services:
type: boolean
blocked_services:
type: array
items:
type: string
upstreams:
type: array
items:
type: string
whois_info:
type: array
items:
$ref: "#/components/schemas/WhoisInfo"
disallowed:
type: boolean
description: >
Whether the client's IP is blocked or not.
disallowed_rule:
type: string
description: >
The rule due to which the client is disallowed.
If `disallowed` is `true`, and this string is empty - it means that
the client IP is disallowed by the "allowed IP list", i.e. it is not included in allowed list.
WhoisInfo:
type: object
properties:
key:
type: string
Clients: Clients:
type: object type: object
properties: properties: