diff --git a/AGHTechDoc.md b/AGHTechDoc.md
index b3d52e1e..4f0c5c8d 100644
--- a/AGHTechDoc.md
+++ b/AGHTechDoc.md
@@ -942,7 +942,7 @@ Error response (Client not found):
### API: Find clients by IP
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:
@@ -968,11 +968,16 @@ Response:
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
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index b6235a66..26e7df08 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -582,5 +582,6 @@
"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 instruction0> on how to resolve this.",
"adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.",
+ "client_not_in_allowed_clients": "The client is not allowed because it is not in the \"Allowed clients\" list.",
"experimental": "Experimental"
}
diff --git a/client/src/__tests__/helpers.test.js b/client/src/__tests__/helpers.test.js
index bb371be4..309b40e6 100644
--- a/client/src/__tests__/helpers.test.js
+++ b/client/src/__tests__/helpers.test.js
@@ -1,136 +1,5 @@
-import {
- countClientsStatistics, findAddressType, getIpMatchListStatus, sortIp,
-} 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);
- });
- });
-});
+import { sortIp, countClientsStatistics, findAddressType } from '../helpers/helpers';
+import { ADDRESS_TYPES } from '../helpers/constants';
describe('sortIp', () => {
describe('ipv4', () => {
diff --git a/client/src/actions/access.js b/client/src/actions/access.js
index e04daac0..17acb5e8 100644
--- a/client/src/actions/access.js
+++ b/client/src/actions/access.js
@@ -3,7 +3,6 @@ import i18next from 'i18next';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './toasts';
-import { BLOCK_ACTIONS } from '../helpers/constants';
import { splitByNewLine } from '../helpers/helpers';
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 toggleClientBlockSuccess = createAction('TOGGLE_CLIENT_BLOCK_SUCCESS');
-export const toggleClientBlock = (type, ip) => async (dispatch) => {
+export const toggleClientBlock = (ip, disallowed, disallowed_rule) => async (dispatch) => {
dispatch(toggleClientBlockRequest());
try {
const {
- allowed_clients, disallowed_clients, blocked_hosts,
+ allowed_clients, blocked_hosts, disallowed_clients = [],
} = await apiClient.getAccessList();
- let updatedDisallowedClients = disallowed_clients || [];
- if (type === BLOCK_ACTIONS.UNBLOCK && updatedDisallowedClients.includes(ip)) {
- updatedDisallowedClients = updatedDisallowedClients.filter((client) => client !== ip);
- } else if (type === BLOCK_ACTIONS.BLOCK && !updatedDisallowedClients.includes(ip)) {
- updatedDisallowedClients.push(ip);
- }
+ const updatedDisallowedClients = disallowed
+ ? disallowed_clients.filter((client) => client !== disallowed_rule)
+ : disallowed_clients.concat(ip);
const values = {
allowed_clients,
@@ -72,9 +68,9 @@ export const toggleClientBlock = (type, ip) => async (dispatch) => {
await apiClient.setAccessList(values);
dispatch(toggleClientBlockSuccess(values));
- if (type === BLOCK_ACTIONS.UNBLOCK) {
- dispatch(addSuccessToast(i18next.t('client_unblocked', { ip })));
- } else if (type === BLOCK_ACTIONS.BLOCK) {
+ if (disallowed) {
+ dispatch(addSuccessToast(i18next.t('client_unblocked', { ip: disallowed_rule })));
+ } else {
dispatch(addSuccessToast(i18next.t('client_blocked', { ip })));
}
} catch (error) {
diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js
index bfb9a609..1c653fb3 100644
--- a/client/src/actions/queryLogs.js
+++ b/client/src/actions/queryLogs.js
@@ -7,6 +7,17 @@ import {
} from '../helpers/constants';
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 { older_than, filter, ...values } = config;
const rawLogs = await apiClient.getQueryLog({
@@ -14,13 +25,8 @@ const getLogsWithParams = async (config) => {
older_than,
});
const { data, oldest } = rawLogs;
- let logs = normalizeLogs(data);
- const clientsParams = getParamsForClientsSearch(logs, 'client');
-
- if (Object.keys(clientsParams).length > 0) {
- const clients = await apiClient.findClients(clientsParams);
- logs = addClientInfo(logs, clients, 'client');
- }
+ const normalizedLogs = normalizeLogs(data);
+ const logs = await enrichWithClientInfo(normalizedLogs);
return {
logs,
@@ -82,6 +88,23 @@ export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
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) => {
dispatch(getLogsRequest());
try {
diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js
index 2f25bda3..ccac7baf 100644
--- a/client/src/components/Dashboard/Clients.js
+++ b/client/src/components/Dashboard/Clients.js
@@ -8,10 +8,11 @@ import classNames from 'classnames';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
-import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
-import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
+import { getPercent, sortIp } from '../../helpers/helpers';
+import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants';
import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
+import { getStats } from '../../actions/stats';
const getClientsPercentColor = (percent) => {
if (percent > 50) {
@@ -33,59 +34,51 @@ const CountCell = (row) => {
return | ;
};
-const renderBlockingButton = (ip) => {
+const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
const dispatch = useDispatch();
const { t } = useTranslation();
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', {
- 'button-action--unblock': !isNotFound,
+ 'button-action--unblock': disallowed,
});
- const toggleClientStatus = (type, ip) => {
- const confirmMessage = type === BLOCK_ACTIONS.BLOCK
- ? `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`
- : t('client_confirm_unblock', { ip });
+ const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
+ const confirmMessage = disallowed
+ ? t('client_confirm_unblock', { ip: disallowed_rule })
+ : `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`;
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
-
-
;
+
+ ;
};
const ClientCell = (row) => {
- const { value, original: { info } } = row;
+ const { value, original: { info, info: { disallowed, disallowed_rule } } } = row;
return <>
{renderFormattedClientCell(value, info, true)}
- {renderBlockingButton(value)}
+ {renderBlockingButton(value, disallowed, disallowed_rule)}
>;
};
@@ -96,7 +89,6 @@ const Clients = ({
}) => {
const { t } = useTranslation();
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
- const disallowedClients = useSelector((state) => state.access.disallowed_clients, shallowEqual);
return
;
diff --git a/client/src/components/Logs/Cells/ClientCell.js b/client/src/components/Logs/Cells/ClientCell.js
index a468bfcb..56a3440c 100644
--- a/client/src/components/Logs/Cells/ClientCell.js
+++ b/client/src/components/Logs/Cells/ClientCell.js
@@ -11,12 +11,16 @@ import IconTooltip from './IconTooltip';
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo } from './helpers';
+import { getStats } from '../../../actions/stats';
+import { updateLogs } from '../../../actions/queryLogs';
const ClientCell = ({
client,
domain,
info,
- info: { name, whois_info },
+ info: {
+ name, whois_info, disallowed, disallowed_rule,
+ },
reason,
}) => {
const { t } = useTranslation();
@@ -26,11 +30,6 @@ const ClientCell = ({
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const [isOptionsOpened, setOptionsOpened] = useState(false);
- const disallowed_clients = useSelector(
- (state) => state.access.disallowed_clients,
- shallowEqual,
- );
-
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
const source = autoClient?.source;
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
@@ -66,43 +65,50 @@ const ClientCell = ({
const {
confirmMessage,
buttonKey: blockingClientKey,
- type,
- } = getBlockClientInfo(client, disallowed_clients);
+ isNotInAllowedList,
+ } = getBlockClientInfo(client, disallowed, disallowed_rule);
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client);
- const BUTTON_OPTIONS_TO_ACTION_MAP = {
- [blockingForClientKey]: () => {
- dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
+ const BUTTON_OPTIONS = [
+ {
+ 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 })}`;
- if (window.confirm(message)) {
- dispatch(toggleClientBlock(type, client));
- }
+ {
+ name: blockingClientKey,
+ onClick: async () => {
+ if (window.confirm(confirmMessage)) {
+ await dispatch(toggleClientBlock(client, disallowed, disallowed_rule));
+ await dispatch(updateLogs());
+ }
+ },
+ disabled: isNotInAllowedList,
},
- };
+ ];
const onClick = async () => {
await dispatch(toggleBlocking(buttonType, domain));
+ await dispatch(getStats());
};
- const getOptions = (optionToActionMap) => {
- const options = Object.entries(optionToActionMap);
+ const getOptions = (options) => {
if (options.length === 0) {
return null;
}
- return <>{options
- .map(([name, onClick]) => {options.map(({ name, onClick, disabled }) =>
)}>;
+ )}>;
};
- const content = getOptions(BUTTON_OPTIONS_TO_ACTION_MAP);
+ const content = getOptions(BUTTON_OPTIONS);
const buttonClass = classNames('button-action button-action--main', {
'button-action--unblock': isFiltered,
@@ -168,6 +174,8 @@ ClientCell.propTypes = {
city: propTypes.string,
orgname: propTypes.string,
}),
+ disallowed: propTypes.bool.isRequired,
+ disallowed_rule: propTypes.string.isRequired,
}),
]),
reason: propTypes.string.isRequired,
diff --git a/client/src/components/Logs/Cells/helpers/index.js b/client/src/components/Logs/Cells/helpers/index.js
index 61e7ff5c..b843ed99 100644
--- a/client/src/components/Logs/Cells/helpers/index.js
+++ b/client/src/components/Logs/Cells/helpers/index.js
@@ -1,19 +1,18 @@
-import { getIpMatchListStatus } from '../../../../helpers/helpers';
-import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS } from '../../../../helpers/constants';
+import i18next from 'i18next';
export const BUTTON_PREFIX = 'btn_';
-export const getBlockClientInfo = (client, disallowed_clients) => {
- const ipMatchListStatus = getIpMatchListStatus(client, disallowed_clients);
+export const getBlockClientInfo = (ip, disallowed, disallowed_rule) => {
+ 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 type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
+ const buttonKey = i18next.t(disallowed ? 'allow_this_client' : 'disallow_this_client');
+ const isNotInAllowedList = disallowed && disallowed_rule === '';
- const confirmMessage = isNotFound ? 'client_confirm_block' : 'client_confirm_unblock';
- const buttonKey = isNotFound ? 'disallow_this_client' : 'allow_this_client';
return {
confirmMessage,
buttonKey,
- type,
+ isNotInAllowedList,
};
};
diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js
index 4f96a5e9..2e2635d9 100644
--- a/client/src/components/Logs/Cells/index.js
+++ b/client/src/components/Logs/Cells/index.js
@@ -32,6 +32,7 @@ import ClientCell from './ClientCell';
import '../Logs.css';
import { toggleClientBlock } from '../../../actions/access';
import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
+import { updateLogs } from '../../../actions/queryLogs';
const Row = memo(({
style,
@@ -49,21 +50,19 @@ const Row = memo(({
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, 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 onClick = () => {
- if (!isSmallScreen) { return; }
+ if (!isSmallScreen) {
+ return;
+ }
const {
answer_dnssec,
client,
domain,
elapsedMs,
info,
+ info: { disallowed, disallowed_rule },
reason,
response,
time,
@@ -94,7 +93,7 @@ const Row = memo(({
const isFiltered = checkFiltered(reason);
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 onToggleBlock = () => {
@@ -113,8 +112,8 @@ const Row = memo(({
const {
confirmMessage,
buttonKey: blockingClientKey,
- type: blockType,
- } = getBlockClientInfo(client, disallowed_clients);
+ isNotInAllowedList,
+ } = getBlockClientInfo(client, disallowed, disallowed_rule);
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client);
@@ -123,13 +122,33 @@ const Row = memo(({
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
};
- const onBlockingClientClick = () => {
- const message = `${blockType === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`;
- if (window.confirm(message)) {
- dispatch(toggleClientBlock(blockType, client));
+ const onBlockingClientClick = async () => {
+ if (window.confirm(confirmMessage)) {
+ await dispatch(toggleClientBlock(client, disallowed, disallowed_rule));
+ await dispatch(updateLogs());
+ setModalOpened(false);
}
};
+ const blockButton = ;
+
+ const blockForClientButton = ;
+
+ const blockClientButton = ;
+
const detailedData = {
time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
@@ -144,12 +163,12 @@ const Row = memo(({
table_name: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category),
tracker_source: hasTracker && sourceData
- && {sourceData.name}
- ,
+ && {sourceData.name}
+ ,
response_details: 'title',
install_settings_dns: upstream,
elapsed: formattedElapsedMs,
@@ -166,12 +185,9 @@ const Row = memo(({
source_label: source,
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
original_response: originalResponse?.join('\n'),
- [BUTTON_PREFIX + buttonType]: {t(buttonType)}
,
- [BUTTON_PREFIX + blockingForClientKey]: {t(blockingForClientKey)}
,
- [BUTTON_PREFIX + blockingClientKey]: {t(blockingClientKey)}
,
+ [BUTTON_PREFIX + buttonType]: blockButton,
+ [BUTTON_PREFIX + blockingForClientKey]: blockForClientButton,
+ [BUTTON_PREFIX + blockingClientKey]: blockClientButton,
};
setDetailedDataCurrent(processContent(detailedData));
diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css
index b230ab8b..901c3ab0 100644
--- a/client/src/components/Logs/Logs.css
+++ b/client/src/components/Logs/Logs.css
@@ -96,7 +96,7 @@
}
.bg--danger {
- color: var(--danger);
+ color: var(--danger) !important;
}
.form-control--search {
@@ -293,7 +293,20 @@
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;
background: var(--gray-f3);
overflow: hidden;
@@ -327,7 +340,7 @@
}
.logs__row--red {
- background-color: var(--red);
+ background-color: var(--red) !important;
}
.logs__row--white {
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 08fd1529..70f9e317 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -479,12 +479,6 @@ export const DNS_REQUEST_OPTIONS = {
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 = {
DHCPv4: 'dhcpv4',
DHCPv6: 'dhcpv6',
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index b0bbea5a..934268ac 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -25,7 +25,6 @@ import {
DHCP_VALUES_PLACEHOLDERS,
FILTERED,
FILTERED_STATUS,
- IP_MATCH_LIST_STATUS,
SERVICES_ID_NAME_MAP,
STANDARD_DNS_PORT,
STANDARD_HTTPS_PORT,
@@ -133,16 +132,14 @@ export const normalizeTopStats = (stats) => (
}))
);
-export const addClientInfo = (data, clients, param) => (
- data.map((row) => {
- const clientIp = row[param];
- const info = clients.find((item) => item[clientIp]) || '';
- return {
- ...row,
- info: info?.[clientIp] ?? '',
- };
- })
-);
+export const addClientInfo = (data, clients, param) => data.map((row) => {
+ const clientIp = row[param];
+ const info = clients.find((item) => item[clientIp]) || '';
+ return {
+ ...row,
+ info: info?.[clientIp] ?? '',
+ };
+});
export const normalizeFilters = (filters) => (
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
@@ -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[]}
* @returns {Object}
diff --git a/client/src/reducers/dhcp.js b/client/src/reducers/dhcp.js
index 5f9f0f58..d6e2868a 100644
--- a/client/src/reducers/dhcp.js
+++ b/client/src/reducers/dhcp.js
@@ -2,7 +2,6 @@ import { handleActions } from 'redux-actions';
import * as actions from '../actions';
import { enrichWithConcatenatedIpAddresses } from '../helpers/helpers';
-// todo: figure out if we cat get rid of redux-form state duplication
const dhcp = handleActions(
{
[actions.getDhcpStatusRequest]: (state) => ({
diff --git a/client/webpack.dev.js b/client/webpack.dev.js
index afcc3506..3d34cb28 100644
--- a/client/webpack.dev.js
+++ b/client/webpack.dev.js
@@ -44,6 +44,7 @@ const getDevServerConfig = (proxyUrl = BASE_URL) => {
proxy: {
[proxyUrl]: `http://${devServerHost}:${port}`,
},
+ open: true,
};
};
diff --git a/dnsforward/access.go b/dnsforward/access.go
index eadd1141..dbe60de5 100644
--- a/dnsforward/access.go
+++ b/dnsforward/access.go
@@ -80,43 +80,46 @@ func processIPCIDRArray(dst *map[string]bool, dstIPNet *[]net.IPNet, src []strin
}
// 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()
defer a.lock.Unlock()
if len(a.allowedClients) != 0 || len(a.allowedClientsIPNet) != 0 {
_, ok := a.allowedClients[ip]
if ok {
- return false
+ return false, ""
}
if len(a.allowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip)
for _, ipnet := range a.allowedClientsIPNet {
if ipnet.Contains(ipAddr) {
- return false
+ return false, ""
}
}
}
- return true
+ return true, ""
}
_, ok := a.disallowedClients[ip]
if ok {
- return true
+ return true, ip
}
if len(a.disallowedClientsIPNet) != 0 {
ipAddr := net.ParseIP(ip)
for _, ipnet := range a.disallowedClientsIPNet {
if ipnet.Contains(ipAddr) {
- return true
+ return true, ipnet.String()
}
}
}
- return false
+ return false, ""
}
// IsBlockedDomain - return TRUE if this domain should be blocked
diff --git a/dnsforward/access_test.go b/dnsforward/access_test.go
new file mode 100644
index 00000000..b760d554
--- /dev/null
+++ b/dnsforward/access_test.go
@@ -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"))
+}
diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go
index 0cb645dd..1d2c3d7b 100644
--- a/dnsforward/dnsforward.go
+++ b/dnsforward/dnsforward.go
@@ -301,3 +301,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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)
+}
diff --git a/dnsforward/dnsforward_test.go b/dnsforward/dnsforward_test.go
index 889321b1..80336055 100644
--- a/dnsforward/dnsforward_test.go
+++ b/dnsforward/dnsforward_test.go
@@ -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) {
invalidUpstreams := []string{"1.2.3.4.5",
"123.3.7m",
diff --git a/dnsforward/filter.go b/dnsforward/filter.go
index 4ef3979d..d2e5535a 100644
--- a/dnsforward/filter.go
+++ b/dnsforward/filter.go
@@ -12,7 +12,8 @@ import (
func (s *Server) beforeRequestHandler(_ *proxy.Proxy, d *proxy.DNSContext) (bool, error) {
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)
return false, nil
}
diff --git a/home/clients.go b/home/clients.go
index 252c6e2b..fce33cb3 100644
--- a/home/clients.go
+++ b/home/clients.go
@@ -80,6 +80,9 @@ type clientsContainer struct {
// dhcpServer is used for looking up clients IP addresses by MAC addresses
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
testing bool // if TRUE, this object is used for internal tests
diff --git a/home/clients_http.go b/home/clients_http.go
index c48e4937..42fa6f2a 100644
--- a/home/clients_http.go
+++ b/home/clients_http.go
@@ -21,6 +21,17 @@ type clientJSON struct {
BlockedServices []string `json:"blocked_services"`
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 {
@@ -38,7 +49,7 @@ type clientListJSON struct {
}
// 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{}
clients.lock.Lock()
@@ -123,15 +134,9 @@ func clientToJSON(c *Client) clientJSON {
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
-func clientHostToJSON(ip string, ch ClientHost) clientHostJSONWithID {
- cj := clientHostJSONWithID{
+func clientHostToJSON(ip string, ch ClientHost) clientJSON {
+ cj := clientJSON{
Name: ch.Host,
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
}
cj := clientHostToJSON(ip, ch)
+
+ cj.Disallowed, cj.DisallowedRule = clients.dnsServer.IsBlockedIP(ip)
el[ip] = cj
} else {
cj := clientToJSON(&c)
+
+ cj.Disallowed, cj.DisallowedRule = clients.dnsServer.IsBlockedIP(ip)
el[ip] = cj
}
diff --git a/home/dns.go b/home/dns.go
index 820b441a..bd70ce38 100644
--- a/home/dns.go
+++ b/home/dns.go
@@ -70,6 +70,7 @@ func initDNSServer() error {
p.DHCPServer = Context.dhcpServer
}
Context.dnsServer = dnsforward.NewServer(p)
+ Context.clients.dnsServer = Context.dnsServer
dnsConfig := generateServerConfig()
err = Context.dnsServer.Prepare(&dnsConfig)
if err != nil {
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index d7a5d2cd..c50d9485 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -1765,7 +1765,61 @@ components:
properties:
1.2.3.4:
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:
type: object
properties: