diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d933e462..9889db34 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,6 +1,12 @@ #!/bin/bash set -e; -git diff --cached --name-only | grep -q '.js$' && make lint-js; + +found=0 +git diff --cached --name-only | grep -q '.js$' && found=1 +if [ $found == 1 ]; then + npm --prefix client run lint || exit 1 + npm run test --prefix client || exit 1 +fi found=0 git diff --cached --name-only | grep -q '.go$' && found=1 diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 0ac594ef..68721c25 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -743,6 +743,7 @@ Response: "server_name":"...", "port_https":443, "port_dns_over_tls":853, + "port_dns_over_quic":784, "certificate_chain":"...", "private_key":"...", "certificate_path":"...", @@ -774,6 +775,7 @@ Request: "force_https":false, "port_https":443, "port_dns_over_tls":853, + "port_dns_over_quic":784, "certificate_chain":"...", "private_key":"...", "certificate_path":"...", // if set, certificate_chain must be empty @@ -991,11 +993,12 @@ Response: { "upstream_dns": ["tls://...", ...], + "upstream_dns_file": "", "bootstrap_dns": ["1.2.3.4", ...], "protection_enabled": true | false, "ratelimit": 1234, - "blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip", + "blocking_mode": "default" | "refused" | "nxdomain" | "null_ip" | "custom_ip", "blocking_ipv4": "1.2.3.4", "blocking_ipv6": "1:2:3::4", "edns_cs_enabled": true | false, @@ -1016,11 +1019,12 @@ Request: { "upstream_dns": ["tls://...", ...], + "upstream_dns_file": "", "bootstrap_dns": ["1.2.3.4", ...], "protection_enabled": true | false, "ratelimit": 1234, - "blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip", + "blocking_mode": "default" | "refused" | "nxdomain" | "null_ip" | "custom_ip", "blocking_ipv4": "1.2.3.4", "blocking_ipv6": "1:2:3::4", "edns_cs_enabled": true | false, diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 56b6988f..a5e0208b 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -132,7 +132,8 @@ "encryption_settings": "Encryption settings", "dhcp_settings": "DHCP settings", "upstream_dns": "Upstream DNS servers", - "upstream_dns_hint": "If you keep this field empty, AdGuard Home will use Quad9 as an upstream.", + "upstream_dns_help": "Enter servers addresses one per line. <0>Learn more about configuring upstream DNS servers.", + "upstream_dns_configured_in_file": "Configured in {{path}}", "test_upstream_btn": "Test upstreams", "upstreams": "Upstreams", "apply_btn": "Apply", @@ -186,6 +187,7 @@ "example_upstream_regular": "regular DNS (over UDP)", "example_upstream_dot": "encrypted <0>DNS-over-TLS", "example_upstream_doh": "encrypted <0>DNS-over-HTTPS", + "example_upstream_doq": "encrypted <0>DNS-over-QUIC", "example_upstream_sdns": "you can use <0>DNS Stamps for <1>DNSCrypt or <2>DNS-over-HTTPS resolvers", "example_upstream_tcp": "regular DNS (over TCP)", "all_lists_up_to_date_toast": "All lists are already up-to-date", @@ -239,6 +241,7 @@ "blocking_mode": "Blocking mode", "default": "Default", "nxdomain": "NXDOMAIN", + "refused": "REFUSED", "null_ip": "Null IP", "custom_ip": "Custom IP", "blocking_ipv4": "Blocking IPv4", @@ -250,10 +253,11 @@ "rate_limit": "Rate limit", "edns_enable": "Enable EDNS Client Subnet", "edns_cs_desc": "If enabled, AdGuard Home will be sending clients' subnets to the DNS servers.", - "rate_limit_desc": "The number of requests per second that a single client is allowed to make (0: unlimited)", + "rate_limit_desc": "The number of requests per second that a single client is allowed to make (setting it to 0 means unlimited)", "blocking_ipv4_desc": "IP address to be returned for a blocked A request", "blocking_ipv6_desc": "IP address to be returned for a blocked AAAA request", - "blocking_mode_default": "Default: Respond with NXDOMAIN when blocked by Adblock-style rule; respond with the IP address specified in the rule when blocked by /etc/hosts-style rule", + "blocking_mode_default": "Default: Respond with REFUSED when blocked by Adblock-style rule; respond with the IP address specified in the rule when blocked by /etc/hosts-style rule", + "blocking_mode_refused": "REFUSED: Respond with REFUSED code", "blocking_mode_nxdomain": "NXDOMAIN: Respond with NXDOMAIN code", "blocking_mode_null_ip": "Null IP: Respond with zero IP address (0.0.0.0 for A; :: for AAAA)", "blocking_mode_custom_ip": "Custom IP: Respond with a manually set IP address", @@ -330,6 +334,8 @@ "encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '/dns-query' location.", "encryption_dot": "DNS-over-TLS port", "encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.", + "encryption_doq": "DNS-over-QUIC port", + "encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port. It's experimental and may not be reliable. Also, there are not too many clients that support it at the moment.", "encryption_certificates": "Certificates", "encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}} or you can buy it from one of the trusted Certificate Authorities.", "encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.", @@ -558,10 +564,9 @@ "enter_cache_size": "Enter cache size", "enter_cache_ttl_min_override": "Enter minimum TTL", "enter_cache_ttl_max_override": "Enter maximum TTL", - "cache_ttl_min_override_desc": "Override TTL value (minimum) received from upstream server. This value can't larger than 3600 (1 hour)", + "cache_ttl_min_override_desc": "Override TTL value (minimum) received from upstream server", "cache_ttl_max_override_desc": "Override TTL value (maximum) received from upstream server", - "min_exceeds_max_value": "Minimum value exceeds maximum value", - "value_not_larger_than": "Value can't be larger than {{maximum}}", + "ttl_cache_validation": "Minimum cache TTL value must be less than or equal to the maximum value", "filter_category_general": "General", "filter_category_security": "Security", "filter_category_regional": "Regional", @@ -574,5 +579,6 @@ "original_response": "Original response", "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 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.", + "experimental": "Experimental" } diff --git a/client/src/__tests__/helpers.test.js b/client/src/__tests__/helpers.test.js index f974cca6..bb371be4 100644 --- a/client/src/__tests__/helpers.test.js +++ b/client/src/__tests__/helpers.test.js @@ -1,5 +1,7 @@ -import { getIpMatchListStatus, sortIp } from '../helpers/helpers'; -import { IP_MATCH_LIST_STATUS } from '../helpers/constants'; +import { + countClientsStatistics, findAddressType, getIpMatchListStatus, sortIp, +} from '../helpers/helpers'; +import { ADDRESS_TYPES, IP_MATCH_LIST_STATUS } from '../helpers/constants'; describe('getIpMatchListStatus', () => { describe('IPv4', () => { @@ -482,3 +484,56 @@ describe('sortIp', () => { }); }); }); + +describe('findAddressType', () => { + describe('ip', () => { + expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP); + }); + describe('cidr', () => { + expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR); + }); + describe('mac', () => { + expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN); + }); +}); + +describe('countClientsStatistics', () => { + test('single ip', () => { + expect(countClientsStatistics(['127.0.0.1'], { + '127.0.0.1': 1, + })).toStrictEqual(1); + }); + test('multiple ip', () => { + expect(countClientsStatistics(['127.0.0.1', '127.0.0.2'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr', () => { + expect(countClientsStatistics(['127.0.0.0/8'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr and multiple ip', () => { + expect(countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(1 + 2 + 3); + }); + test('mac', () => { + expect(countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(2 + 3); + }); + test('not found', () => { + expect(countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(0); + }); +}); diff --git a/client/src/actions/encryption.js b/client/src/actions/encryption.js index 0e743323..36faf2ec 100644 --- a/client/src/actions/encryption.js +++ b/client/src/actions/encryption.js @@ -34,6 +34,7 @@ export const setTlsConfig = (config) => async (dispatch, getState) => { values.private_key = btoa(values.private_key); values.port_https = values.port_https || 0; values.port_dns_over_tls = values.port_dns_over_tls || 0; + values.port_dns_over_quic = values.port_dns_over_quic || 0; const response = await apiClient.setTlsConfig(values); response.certificate_chain = atob(response.certificate_chain); @@ -59,6 +60,7 @@ export const validateTlsConfig = (config) => async (dispatch) => { values.private_key = btoa(values.private_key); values.port_https = values.port_https || 0; values.port_dns_over_tls = values.port_dns_over_tls || 0; + values.port_dns_over_quic = values.port_dns_over_quic || 0; const response = await apiClient.validateTlsConfig(values); response.certificate_chain = atob(response.certificate_chain); diff --git a/client/src/actions/index.js b/client/src/actions/index.js index ff039af1..d39975ea 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -571,10 +571,10 @@ export const toggleBlocking = ( const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule); if (matchPreparedBlockingRule) { - dispatch(setRules(userRules.replace(`${blockingRule}`, ''))); + await dispatch(setRules(userRules.replace(`${blockingRule}`, ''))); dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule }))); } else if (!matchPreparedUnblockingRule) { - dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`)); + await dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`)); dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); } else if (matchPreparedUnblockingRule) { dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); diff --git a/client/src/components/Dashboard/Dashboard.css b/client/src/components/Dashboard/Dashboard.css index 04522ef6..6d3aef7f 100644 --- a/client/src/components/Dashboard/Dashboard.css +++ b/client/src/components/Dashboard/Dashboard.css @@ -26,3 +26,10 @@ left: -20px; width: calc(100% + 20px); } + +@media (max-width: 1279.98px) { + .table__action { + position: absolute; + right: 0; + } +} diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js index 07d8f94f..4d229ffb 100644 --- a/client/src/components/Dashboard/index.js +++ b/client/src/components/Dashboard/index.js @@ -82,7 +82,7 @@ const Dashboard = ({ {statsProcessing && } - {!statsProcessing &&
+ {!statsProcessing &&
diff --git a/client/src/components/Logs/Cells/ClientCell.js b/client/src/components/Logs/Cells/ClientCell.js index fd9875bf..a468bfcb 100644 --- a/client/src/components/Logs/Cells/ClientCell.js +++ b/client/src/components/Logs/Cells/ClientCell.js @@ -84,7 +84,9 @@ const ClientCell = ({ }, }; - const onClick = () => dispatch(toggleBlocking(buttonType, domain)); + const onClick = async () => { + await dispatch(toggleBlocking(buttonType, domain)); + }; const getOptions = (optionToActionMap) => { const options = Object.entries(optionToActionMap); diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index cb7f2914..ac613164 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next'; import ReactTable from 'react-table'; import { MODAL_TYPE } from '../../../helpers/constants'; -import { splitByNewLine } from '../../../helpers/helpers'; +import { splitByNewLine, countClientsStatistics } from '../../../helpers/helpers'; import Card from '../../ui/Card'; import Modal from './Modal'; import CellWrap from '../../ui/CellWrap'; @@ -204,7 +204,10 @@ class ClientsTable extends Component { { Header: this.props.t('requests_count'), id: 'statistics', - accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0, + accessor: (row) => countClientsStatistics( + row.ids, + this.props.normalizedTopClients.auto, + ), sortMethod: (a, b) => b - a, minWidth: 120, Cell: (row) => { diff --git a/client/src/components/Settings/Dhcp/FormDHCPv4.js b/client/src/components/Settings/Dhcp/FormDHCPv4.js index b938b6f0..873e7696 100644 --- a/client/src/components/Settings/Dhcp/FormDHCPv4.js +++ b/client/src/components/Settings/Dhcp/FormDHCPv4.js @@ -8,10 +8,9 @@ import { renderInputField, toNumber, } from '../../../helpers/form'; -import { FORM_NAME } from '../../../helpers/constants'; +import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants'; import { validateIpv4, - validateIsPositiveValue, validateRequiredValue, validateIpv4RangeEnd, } from '../../../helpers/validators'; @@ -110,9 +109,10 @@ const FormDHCPv4 = ({ type="number" className="form-control" placeholder={t(ipv4placeholders.lease_duration)} - validate={[validateIsPositiveValue, validateRequired]} + validate={validateRequired} normalize={toNumber} - min={0} + min={1} + max={UINT32_RANGE.MAX} disabled={!isInterfaceIncludesIpv4} />
diff --git a/client/src/components/Settings/Dhcp/FormDHCPv6.js b/client/src/components/Settings/Dhcp/FormDHCPv6.js index 80219fd6..e37a6213 100644 --- a/client/src/components/Settings/Dhcp/FormDHCPv6.js +++ b/client/src/components/Settings/Dhcp/FormDHCPv6.js @@ -8,12 +8,8 @@ import { renderInputField, toNumber, } from '../../../helpers/form'; -import { FORM_NAME } from '../../../helpers/constants'; -import { - validateIpv6, - validateIsPositiveValue, - validateRequiredValue, -} from '../../../helpers/validators'; +import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants'; +import { validateIpv6, validateRequiredValue } from '../../../helpers/validators'; const FormDHCPv6 = ({ handleSubmit, @@ -86,9 +82,10 @@ const FormDHCPv6 = ({ type="number" className="form-control" placeholder={t(ipv6placeholders.lease_duration)} - validate={[validateIsPositiveValue, validateRequired]} + validate={validateRequired} normalizeOnBlur={toNumber} - min={0} + min={1} + max={UINT32_RANGE.MAX} disabled={!isInterfaceIncludesIpv6} />
diff --git a/client/src/components/Settings/Dns/Cache/Form.js b/client/src/components/Settings/Dns/Cache/Form.js index c7b2d6ed..f17310c9 100644 --- a/client/src/components/Settings/Dns/Cache/Form.js +++ b/client/src/components/Settings/Dns/Cache/Form.js @@ -4,32 +4,30 @@ import { Field, reduxForm } from 'redux-form'; import { Trans, useTranslation } from 'react-i18next'; import { shallowEqual, useSelector } from 'react-redux'; import { renderInputField, toNumber } from '../../../../helpers/form'; -import { validateBiggerOrEqualZeroValue, getMaxValueValidator, validateRequiredValue } from '../../../../helpers/validators'; -import { FORM_NAME, SECONDS_IN_HOUR } from '../../../../helpers/constants'; +import { validateRequiredValue } from '../../../../helpers/validators'; +import { CACHE_CONFIG_FIELDS, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants'; -const validateMaxValue3600 = getMaxValueValidator(SECONDS_IN_HOUR); - -const getInputFields = ({ validateRequiredValue, validateMaxValue3600 }) => [{ - name: 'cache_size', - title: 'cache_size', - description: 'cache_size_desc', - placeholder: 'enter_cache_size', - validate: validateRequiredValue, -}, -{ - name: 'cache_ttl_min', - title: 'cache_ttl_min_override', - description: 'cache_ttl_min_override_desc', - placeholder: 'enter_cache_ttl_min_override', - max: SECONDS_IN_HOUR, - validate: validateMaxValue3600, -}, -{ - name: 'cache_ttl_max', - title: 'cache_ttl_max_override', - description: 'cache_ttl_max_override_desc', - placeholder: 'enter_cache_ttl_max_override', -}]; +const getInputFields = (validateRequiredValue) => [ + { + name: CACHE_CONFIG_FIELDS.cache_size, + title: 'cache_size', + description: 'cache_size_desc', + placeholder: 'enter_cache_size', + validate: validateRequiredValue, + }, + { + name: CACHE_CONFIG_FIELDS.cache_ttl_min, + title: 'cache_ttl_min_override', + description: 'cache_ttl_min_override_desc', + placeholder: 'enter_cache_ttl_min_override', + }, + { + name: CACHE_CONFIG_FIELDS.cache_ttl_max, + title: 'cache_ttl_max_override', + description: 'cache_ttl_max_override_desc', + placeholder: 'enter_cache_ttl_max_override', + }, +]; const Form = ({ handleSubmit, submitting, invalid, @@ -41,17 +39,16 @@ const Form = ({ cache_ttl_max, cache_ttl_min, } = useSelector((state) => state.form[FORM_NAME.CACHE].values, shallowEqual); - const minExceedsMax = cache_ttl_min > cache_ttl_max; + const minExceedsMax = typeof cache_ttl_min === 'number' + && typeof cache_ttl_max === 'number' + && cache_ttl_min > cache_ttl_max; - const INPUTS_FIELDS = getInputFields({ - validateRequiredValue, - validateMaxValue3600, - }); + const INPUTS_FIELDS = getInputFields(validateRequiredValue); return
{INPUTS_FIELDS.map(({ - name, title, description, placeholder, validate, max, + name, title, description, placeholder, validate, min = 0, max = UINT32_RANGE.MAX, }) =>
@@ -66,15 +63,15 @@ const Form = ({ disabled={processingSetConfig} normalize={toNumber} className="form-control" - validate={[validateBiggerOrEqualZeroValue].concat(validate || [])} - min={0} + validate={validate} + min={min} max={max} />
)} {minExceedsMax - && {t('min_exceeds_max_value')}} + && {t('ttl_cache_validation')}}