diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index d933e462..4fd4e4f5 100755
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -1,6 +1,10 @@
#!/bin/bash
set -e;
-git diff --cached --name-only | grep -q '.js$' && make lint-js;
+git diff --cached --name-only | grep -q '.js$' && found=1
+if [ $found == 1 ]; then
+ make lint-js || 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 50ad9d90..1a90fd91 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
diff --git a/client/package-lock.json b/client/package-lock.json
index 84c8e186..42cac10f 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -12377,9 +12377,9 @@
}
},
"react-i18next": {
- "version": "11.4.0",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz",
- "integrity": "sha512-lyOZSSQkif4H9HnHN3iEKVkryLI+WkdZSEw3VAZzinZLopfYRMHVY5YxCopdkXPLEHs6S5GjKYPh3+j0j336Fg==",
+ "version": "11.7.2",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.2.tgz",
+ "integrity": "sha512-Djj3K3hh5Tecla2CI9rLO3TZBYGMFrGilm0JY4cLofAQONCi5TK6nVmUPKoB59n1ZffgjfgJt6zlbE9aGF6Q0Q==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
diff --git a/client/package.json b/client/package.json
index fdc19c9d..4ad8d5eb 100644
--- a/client/package.json
+++ b/client/package.json
@@ -28,7 +28,7 @@
"react": "^16.13.1",
"react-click-outside": "^3.0.1",
"react-dom": "^16.13.1",
- "react-i18next": "^11.4.0",
+ "react-i18next": "^11.7.2",
"react-modal": "^3.11.2",
"react-popper-tooltip": "^2.11.1",
"react-redux": "^7.2.0",
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 674332d7..2fe1e760 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -133,6 +133,7 @@
"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_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-TLS0>",
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS0>",
+ "example_upstream_doq": "encrypted <0>DNS-over-QUIC0>",
"example_upstream_sdns": "you can use <0>DNS Stamps0> for <1>DNSCrypt1> or <2>DNS-over-HTTPS2> resolvers",
"example_upstream_tcp": "regular DNS (over TCP)",
"all_lists_up_to_date_toast": "All lists are already up-to-date",
@@ -330,6 +332,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}}0> or you can buy it from one of the trusted Certificate Authorities.",
"encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.",
@@ -363,7 +367,7 @@
"fix": "Fix",
"dns_providers": "Here is a <0>list of known DNS providers0> to choose from.",
"update_now": "Update now",
- "update_failed": "Auto-update failed. Please follow the steps to update manually.",
+ "update_failed": "Auto-update failed. Please follow these steps to update manually.",
"processing_update": "Please wait, AdGuard Home is being updated",
"clients_title": "Clients",
"clients_desc": "Configure devices connected to AdGuard Home",
@@ -575,6 +579,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.",
- "configured_in": "Configured in {{path}}",
- "please_read_wiki": "Please read the wiki"
+ "please_read_wiki": "Please read the wiki",
+ "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 d4018bb0..ff039af1 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -4,9 +4,10 @@ import axios from 'axios';
import endsWith from 'lodash/endsWith';
import escapeRegExp from 'lodash/escapeRegExp';
+import React from 'react';
import { splitByNewLine, sortClients } from '../helpers/helpers';
import {
- BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME,
+ BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, GETTING_STARTED_LINK,
} from '../helpers/constants';
import { areEqualVersions } from '../helpers/version';
import { getTlsStatus } from './encryption';
@@ -184,7 +185,14 @@ export const getUpdate = () => async (dispatch, getState) => {
dispatch(getUpdateRequest());
const handleRequestError = () => {
- dispatch(addNoticeToast({ error: 'update_failed' }));
+ const options = {
+ components: {
+ a: ,
+ },
+ };
+
+ dispatch(addNoticeToast({ error: 'update_failed', options }));
dispatch(getUpdateFailure());
};
diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css
index fd088b79..b230ab8b 100644
--- a/client/src/components/Logs/Logs.css
+++ b/client/src/components/Logs/Logs.css
@@ -388,3 +388,28 @@
.logs__table .loading:before {
min-height: 100%;
}
+
+.logs__whois {
+ display: inline;
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.logs__whois::after {
+ content: "|";
+ padding: 0 5px;
+ opacity: 0.3;
+}
+
+.logs__whois:last-child::after {
+ content: "";
+}
+
+.logs__whois-icon.icons {
+ position: relative;
+ top: -2px;
+ width: 12px;
+ height: 12px;
+ margin-right: 1px;
+ opacity: 0.5;
+}
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/Clients/whoisCell.js b/client/src/components/Settings/Clients/whoisCell.js
index 94afd6cc..1a8b0484 100644
--- a/client/src/components/Settings/Clients/whoisCell.js
+++ b/client/src/components/Settings/Clients/whoisCell.js
@@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
{icon && (
-
+
+
+
+
+
+ encryption_doq_desc
+
+
+
diff --git a/client/src/components/Settings/Encryption/index.js b/client/src/components/Settings/Encryption/index.js
index 7c2cccc8..f7ca52e0 100644
--- a/client/src/components/Settings/Encryption/index.js
+++ b/client/src/components/Settings/Encryption/index.js
@@ -66,6 +66,7 @@ class Encryption extends Component {
force_https,
port_https,
port_dns_over_tls,
+ port_dns_over_quic,
certificate_chain,
private_key,
certificate_path,
@@ -78,6 +79,7 @@ class Encryption extends Component {
force_https,
port_https,
port_dns_over_tls,
+ port_dns_over_quic,
certificate_chain,
private_key,
certificate_path,
diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css
index 3bf1a121..4efb0868 100644
--- a/client/src/components/Settings/Settings.css
+++ b/client/src/components/Settings/Settings.css
@@ -54,7 +54,7 @@
}
.form__message--error {
- color: var(--red);
+ color: #cd201f;
}
.form__message--left-pad {
diff --git a/client/src/components/Toasts/Toast.js b/client/src/components/Toasts/Toast.js
index a4c58aad..4c46078a 100644
--- a/client/src/components/Toasts/Toast.js
+++ b/client/src/components/Toasts/Toast.js
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
+import { Trans } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { TOAST_TIMEOUTS } from '../../helpers/constants';
import { removeToast } from '../../actions';
@@ -9,8 +9,8 @@ const Toast = ({
id,
message,
type,
+ options,
}) => {
- const { t } = useTranslation();
const dispatch = useDispatch();
const [timerId, setTimerId] = useState(null);
@@ -30,7 +30,12 @@ const Toast = ({
return
-
{t(message)}
+
+
+