* : merge with master

This commit is contained in:
Andrey Meshkov
2020-09-09 14:35:11 +03:00
38 changed files with 1194 additions and 176 deletions

View File

@@ -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 <a href='https://www.quad9.net/' target='_blank'>Quad9</a> 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-TLS</0>",
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>",
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>",
"example_upstream_sdns": "you can use <0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> 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 providers</0> to choose from.",
"update_now": "Update now",
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps</a> to update manually.",
"update_failed": "Auto-update failed. Please <a>follow these steps</a> 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 instruction</0> 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"
}

View File

@@ -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);
});
});

View File

@@ -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);

View File

@@ -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: <a href={GETTING_STARTED_LINK} target="_blank"
rel="noopener noreferrer" />,
},
};
dispatch(addNoticeToast({ error: 'update_failed', options }));
dispatch(getUpdateFailure());
};

View File

@@ -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;
}

View File

@@ -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) => {

View File

@@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
<div key={key} title={t(key)}>
{icon && (
<Fragment>
<svg className="logs__whois-icon text-muted-dark icons">
<svg className="logs__whois-icon text-muted-dark icons icon--24">
<use xlinkHref={`#${icon}`} />
</svg>
&nbsp;

View File

@@ -63,6 +63,27 @@ const Examples = (props) => (
</Trans>
</span>
</li>
<li>
<code>quic://dns-unfiltered.adguard.com:784</code> &nbsp;
<span>
<Trans
components={[
<a
href="https://tools.ietf.org/html/draft-huitema-quic-dnsoquic-07"
target="_blank"
rel="noopener noreferrer"
key="0"
>
DNS-over-QUIC
</a>,
]}
>
example_upstream_doq
</Trans>
&nbsp;
<span className="text-lowercase">(<Trans>experimental</Trans>)</span>
</span>
</li>
<li>
<code>tcp://9.9.9.9</code> <Trans>example_upstream_tcp</Trans>
</li>

View File

@@ -32,7 +32,7 @@ const Upstream = () => {
dispatch(setDnsConfig(dnsConfig));
};
const upstreamDns = upstream_dns_file ? t('configured_in', { path: upstream_dns_file }) : upstream_dns;
const upstreamDns = upstream_dns_file ? t('upstream_dns_configured_in_file', { path: upstream_dns_file }) : upstream_dns;
return <Card
title={t('upstream_dns')}

View File

@@ -11,11 +11,15 @@ import {
renderRadioField,
toNumber,
} from '../../../helpers/form';
import { validateIsSafePort, validatePort, validatePortTLS } from '../../../helpers/validators';
import {
validateIsSafePort, validatePort, validatePortQuic, validatePortTLS,
} from '../../../helpers/validators';
import i18n from '../../../i18n';
import KeyStatus from './KeyStatus';
import CertificateStatus from './CertificateStatus';
import { DNS_OVER_TLS_PORT, FORM_NAME, STANDARD_HTTPS_PORT } from '../../../helpers/constants';
import {
DNS_OVER_QUIC_PORT, DNS_OVER_TLS_PORT, FORM_NAME, STANDARD_HTTPS_PORT,
} from '../../../helpers/constants';
const validate = (values) => {
const errors = {};
@@ -38,6 +42,7 @@ const clearFields = (change, setTlsConfig, t) => {
certificate_path: '',
port_https: STANDARD_HTTPS_PORT,
port_dns_over_tls: DNS_OVER_TLS_PORT,
port_dns_over_quic: DNS_OVER_QUIC_PORT,
server_name: '',
force_https: false,
enabled: false,
@@ -189,6 +194,30 @@ let Form = (props) => {
</div>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="port_dns_over_quic">
<Trans>encryption_doq</Trans>
&nbsp;
<span className="text-lowercase">(<Trans>experimental</Trans>)</span>
</label>
<Field
id="port_dns_over_quic"
name="port_dns_over_quic"
component={renderInputField}
type="number"
className="form-control"
placeholder={t('encryption_doq')}
validate={[validatePortQuic]}
normalize={toNumber}
onChange={handleChange}
disabled={!isEnabled}
/>
<div className="form__desc">
<Trans>encryption_doq_desc</Trans>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">

View File

@@ -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,

View File

@@ -54,7 +54,7 @@
}
.form__message--error {
color: var(--red);
color: #cd201f;
}
.form__message--left-pad {

View File

@@ -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 <div className={`toast toast--${type}`}
onMouseOver={clearRemoveToastTimeout}
onMouseOut={setRemoveToastTimeout}>
<p className="toast__content">{t(message)}</p>
<p className="toast__content">
<Trans
i18nKey={message}
{...options}
/>
</p>
<button className="toast__dismiss" onClick={removeCurrentToast}>
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -45,6 +50,7 @@ Toast.propTypes = {
id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
options: PropTypes.object,
};
export default Toast;

View File

@@ -54,6 +54,8 @@ export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
export const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse';
export const UPSTREAM_CONFIGURATION_WIKI_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams';
export const GETTING_STARTED_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update';
export const ADDRESS_IN_USE_TEXT = 'address already in use';
export const INSTALL_FIRST_STEP = 1;
@@ -70,6 +72,7 @@ export const STANDARD_DNS_PORT = 53;
export const STANDARD_WEB_PORT = 80;
export const STANDARD_HTTPS_PORT = 443;
export const DNS_OVER_TLS_PORT = 853;
export const DNS_OVER_QUIC_PORT = 784;
export const MAX_PORT = 65535;
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
@@ -77,8 +80,6 @@ export const EMPTY_DATE = '0001-01-01T00:00:00Z';
export const DEBOUNCE_TIMEOUT = 300;
export const DEBOUNCE_FILTER_TIMEOUT = 500;
export const CHECK_TIMEOUT = 1000;
export const SUCCESS_TOAST_TIMEOUT = 5000;
export const FAILURE_TOAST_TIMEOUT = 30000;
export const HIDE_TOOLTIP_DELAY = 300;
export const SHOW_TOOLTIP_DELAY = 200;
export const MODAL_OPEN_TIMEOUT = 150;
@@ -541,8 +542,17 @@ export const TOAST_TYPES = {
NOTICE: 'notice',
};
export const SUCCESS_TOAST_TIMEOUT = 5000;
export const FAILURE_TOAST_TIMEOUT = 30000;
export const TOAST_TIMEOUTS = {
[TOAST_TYPES.SUCCESS]: 5000,
[TOAST_TYPES.ERROR]: 30000,
[TOAST_TYPES.NOTICE]: 30000,
[TOAST_TYPES.SUCCESS]: SUCCESS_TOAST_TIMEOUT,
[TOAST_TYPES.ERROR]: FAILURE_TOAST_TIMEOUT,
[TOAST_TYPES.NOTICE]: FAILURE_TOAST_TIMEOUT,
};
export const ADDRESS_TYPES = {
IP: 'IP',
CIDR: 'CIDR',
UNKNOWN: 'UNKNOWN',
};

View File

@@ -14,6 +14,7 @@ import queryString from 'query-string';
import { getTrackerData } from './trackers/trackers';
import {
ADDRESS_TYPES,
CHECK_TIMEOUT,
CUSTOM_FILTERING_RULES_ID,
DEFAULT_DATE_FORMAT_OPTIONS,
@@ -509,6 +510,18 @@ const isIpMatchCidr = (parsedIp, parsedCidr) => {
}
};
export const isIpInCidr = (ip, cidr) => {
try {
const parsedIp = ipaddr.parse(ip);
const parsedCidr = ipaddr.parseCIDR(cidr);
return isIpMatchCidr(parsedIp, parsedCidr);
} catch (e) {
console.error(e);
return false;
}
};
/**
* The purpose of this method is to quickly check
* if this IP can possibly be in the specified CIDR range.
@@ -578,6 +591,29 @@ const isIpQuickMatchCIDR = (ip, listItem) => {
return false;
};
/**
*
* @param ipOrCidr
* @returns {'IP' | 'CIDR' | 'UNKNOWN'}
*
*/
export const findAddressType = (address) => {
try {
const cidrMaybe = address.includes('/');
if (!cidrMaybe && ipaddr.isValid(address)) {
return ADDRESS_TYPES.IP;
}
if (cidrMaybe && ipaddr.parseCIDR(address)) {
return ADDRESS_TYPES.CIDR;
}
return ADDRESS_TYPES.UNKNOWN;
} catch (e) {
return ADDRESS_TYPES.UNKNOWN;
}
};
/**
* @param ip {string}
* @param list {string}
@@ -622,6 +658,42 @@ export const getIpMatchListStatus = (ip, list) => {
}
};
/**
* @param ids {string[]}
* @returns {Object}
*/
export const separateIpsAndCidrs = (ids) => ids.reduce((acc, curr) => {
const addressType = findAddressType(curr);
if (addressType === ADDRESS_TYPES.IP) {
acc.ips.push(curr);
}
if (addressType === ADDRESS_TYPES.CIDR) {
acc.cidrs.push(curr);
}
return acc;
}, { ips: [], cidrs: [] });
export const countClientsStatistics = (ids, autoClients) => {
const { ips, cidrs } = separateIpsAndCidrs(ids);
const ipsCount = ips.reduce((acc, curr) => {
const count = autoClients[curr] || 0;
return acc + count;
}, 0);
const cidrsCount = Object.entries(autoClients)
.reduce((acc, curr) => {
const [id, count] = curr;
if (cidrs.some((cidr) => isIpInCidr(id, cidr))) {
// eslint-disable-next-line no-param-reassign
acc += count;
}
return acc;
}, 0);
return ipsCount + cidrsCount;
};
/**
* @param {string} elapsedMs

View File

@@ -9,7 +9,7 @@ const getFormattedWhois = (whois) => {
.map((key) => {
const icon = WHOIS_ICONS[key];
return (
<span className="logs__whois text-muted " key={key} title={whoisInfo[key]}>
<span className="logs__whois text-muted" key={key} title={whoisInfo[key]}>
{icon && (
<>
<svg className="logs__whois-icon icons icon--18">

View File

@@ -180,6 +180,12 @@ export const validatePortTLS = (value) => {
return undefined;
};
/**
* @param value {number}
* @returns {undefined|string}
*/
export const validatePortQuic = validatePortTLS;
/**
* @param value {number}
* @returns {undefined|string}

View File

@@ -15,6 +15,7 @@ const toasts = handleActions({
const errorToast = {
id: nanoid(),
message,
options: payload.options,
type: TOAST_TYPES.ERROR,
};
@@ -35,6 +36,7 @@ const toasts = handleActions({
const noticeToast = {
id: nanoid(),
message: payload.error.toString(),
options: payload.options,
type: TOAST_TYPES.NOTICE,
};