+ 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:
commit
6222d17d86
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,46 +34,38 @@ 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>
|
<Trans>{text}</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
@ -80,12 +73,12 @@ const renderBlockingButton = (ip) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
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>;
|
||||||
|
|
|
@ -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]: () => {
|
{
|
||||||
|
name: blockingForClientKey,
|
||||||
|
onClick: () => {
|
||||||
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
|
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)) {
|
name: blockingClientKey,
|
||||||
dispatch(toggleClientBlock(type, client));
|
onClick: async () => {
|
||||||
|
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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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));
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
|
@ -44,6 +44,7 @@ const getDevServerConfig = (proxyUrl = BASE_URL) => {
|
||||||
proxy: {
|
proxy: {
|
||||||
[proxyUrl]: `http://${devServerHost}:${port}`,
|
[proxyUrl]: `http://${devServerHost}:${port}`,
|
||||||
},
|
},
|
||||||
|
open: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"))
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue