Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home

This commit is contained in:
Andrey Meshkov 2020-12-28 12:25:27 +03:00
commit e5780fa308
36 changed files with 699 additions and 186 deletions

View File

@ -1846,7 +1846,7 @@ Response:
} }
There are also deprecated properties `filter_id` and `rule` on the top level of There are also deprecated properties `filter_id` and `rule` on the top level of
the response object. Their usaga should be replaced with the response object. Their usage should be replaced with
`rules[*].filter_list_id` and `rules[*].text` correspondingly. See the `rules[*].filter_list_id` and `rules[*].text` correspondingly. See the
_OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file. _OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file.

View File

@ -56,6 +56,13 @@ and this project adheres to
[#2391]: https://github.com/AdguardTeam/AdGuardHome/issues/2391 [#2391]: https://github.com/AdguardTeam/AdGuardHome/issues/2391
[#2394]: https://github.com/AdguardTeam/AdGuardHome/issues/2394 [#2394]: https://github.com/AdguardTeam/AdGuardHome/issues/2394
### Deprecated
- _Go_ 1.14 support. v0.106.0 will require at least _Go_ 1.15 to build.
- The `darwin/386` port. It will be removed in v0.106.0.
- The `"rule"` and `"filter_id"` fields in `GET /filtering/check_host` and
`GET /querylog` responses. They will be removed in v0.106.0 ([#2102]).
### Fixed ### Fixed
- Inability to set DNS cache TTL limits ([#2459]). - Inability to set DNS cache TTL limits ([#2459]).

View File

@ -70,6 +70,14 @@ The rules are mostly sorted in the alphabetical order.
func TestType_Method_suffix(t *testing.T) { /* … */ } func TestType_Method_suffix(t *testing.T) { /* … */ }
``` ```
* Name parameters in interface definitions:
```go
type Frobulator interface {
Frobulate(f Foo, b Bar) (r Result, err error)
}
```
* Name the deferred errors (e.g. when closing something) `cerr`. * Name the deferred errors (e.g. when closing something) `cerr`.
* No shadowing, since it can often lead to subtle bugs, especially with * No shadowing, since it can often lead to subtle bugs, especially with
@ -172,10 +180,15 @@ The rules are mostly sorted in the alphabetical order.
* Put utility flags in the ASCII order and **don't** group them together. For * Put utility flags in the ASCII order and **don't** group them together. For
example, `ls -1 -A -q`. example, `ls -1 -A -q`.
* `snake_case`, not `camelCase`. * `snake_case`, not `camelCase` for variables. `kebab-case` for filenames.
* UPPERCASE names for external exported variables, lowercase for local,
unexported ones.
* Use `set -e -f -u` and also `set -x` in verbose mode. * Use `set -e -f -u` and also `set -x` in verbose mode.
* Use `readonly` liberally.
* Use the `"$var"` form instead of the `$var` form, unless word splitting is * Use the `"$var"` form instead of the `$var` form, unless word splitting is
required. required.

View File

@ -67,10 +67,11 @@ endif
# Version properties # Version properties
COMMIT=$(shell git rev-parse --short HEAD) COMMIT=$(shell git rev-parse --short HEAD)
TAG_NAME=$(shell git describe --abbrev=0)
PRERELEASE_VERSION=$(shell git describe --abbrev=0)
# TODO(a.garipov): The cut call is a temporary solution to trim # TODO(a.garipov): The cut call is a temporary solution to trim
# prerelease versions. See the comment in .goreleaser.yml. # prerelease versions. See the comment in .goreleaser.yml.
TAG_NAME=$(shell git describe --abbrev=0 | cut -c 1-8) RELEASE_VERSION=$(shell git describe --abbrev=0 | cut -c 1-8)
RELEASE_VERSION=$(TAG_NAME)
SNAPSHOT_VERSION=$(RELEASE_VERSION)-SNAPSHOT-$(COMMIT) SNAPSHOT_VERSION=$(RELEASE_VERSION)-SNAPSHOT-$(COMMIT)
# Set proper version # Set proper version
@ -78,6 +79,8 @@ VERSION=
ifeq ($(TAG_NAME),$(shell git describe --abbrev=4)) ifeq ($(TAG_NAME),$(shell git describe --abbrev=4))
ifeq ($(CHANNEL),edge) ifeq ($(CHANNEL),edge)
VERSION=$(SNAPSHOT_VERSION) VERSION=$(SNAPSHOT_VERSION)
else ifeq ($(CHANNEL),beta)
VERSION=$(PRERELEASE_VERSION)
else else
VERSION=$(RELEASE_VERSION) VERSION=$(RELEASE_VERSION)
endif endif

View File

@ -123,20 +123,22 @@ AdGuard Home provides a lot of features out-of-the-box with no need to install a
> Disclaimer: some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature. > Disclaimer: some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature.
| Feature | AdGuard Home | Pi-Hole | | Feature | AdGuard Home | Pi-Hole |
|-------------------------------------------------------------------------|--------------|--------------------------------------------------------| |-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------|
| Blocking ads and trackers | ✅ | ✅ | | Blocking ads and trackers | ✅ | ✅ |
| Customizing blocklists | ✅ | ✅ | | Customizing blocklists | ✅ | ✅ |
| Built-in DHCP server | ✅ | ✅ | | Built-in DHCP server | ✅ | ✅ |
| HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttpd | | HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttpd |
| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) | | Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) |
| Cross-platform | ✅ | ❌ (not natively, only via Docker) | | Cross-platform | ✅ | ❌ (not natively, only via Docker) |
| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) | | Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) |
| Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) | | Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) |
| Parental control (blocking adult domains) | ✅ | ❌ | | Parental control (blocking adult domains) | ✅ | ❌ |
| Force Safe search on search engines | ✅ | ❌ | | Force Safe search on search engines | ✅ | ❌ |
| Per-client (device) configuration | ✅ | ✅ | | Per-client (device) configuration | ✅ | ✅ |
| Access settings (choose who can use AGH DNS) | ✅ | ❌ | | Access settings (choose who can use AGH DNS) | ✅ | ❌ |
| Written in a memory-safe language | ✅ | ❌ |
| Running without root privileges | ✅ | ❌ |
<a id="comparison-adblock"></a> <a id="comparison-adblock"></a>
### How does AdGuard Home compare to traditional ad blockers ### How does AdGuard Home compare to traditional ad blockers

View File

@ -270,7 +270,7 @@
"source_label": "Source", "source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.", "found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category", "category_label": "Category",
"rule_label": "Rule", "rule_label": "Rule(s)",
"list_label": "List", "list_label": "List",
"unknown_filter": "Unknown filter {{filterId}}", "unknown_filter": "Unknown filter {{filterId}}",
"known_tracker": "Known tracker", "known_tracker": "Known tracker",
@ -530,7 +530,6 @@
"check_ip": "IP addresses: {{ip}}", "check_ip": "IP addresses: {{ip}}",
"check_cname": "CNAME: {{cname}}", "check_cname": "CNAME: {{cname}}",
"check_reason": "Reason: {{reason}}", "check_reason": "Reason: {{reason}}",
"check_rule": "Rule: {{rule}}",
"check_service": "Service name: {{service}}", "check_service": "Service name: {{service}}",
"service_name": "Service name", "service_name": "Service name",
"check_not_found": "Not found in your filter lists", "check_not_found": "Not found in your filter lists",

View File

@ -287,7 +287,7 @@ export const getDnsStatus = () => async (dispatch) => {
try { try {
checkStatus(handleRequestSuccess, handleRequestError); checkStatus(handleRequestSuccess, handleRequestError);
} catch (error) { } catch (error) {
handleRequestError(error); handleRequestError();
} }
}; };

View File

@ -1,9 +1,9 @@
:root { :root {
--yellow-pale: rgba(247, 181, 0, 0.1); --yellow-pale: rgba(247, 181, 0, 0.1);
--green79: #67B279; --green79: #67b279;
--gray-a5: #a5a5a5; --gray-a5: #a5a5a5;
--gray-d8: #d8d8d8; --gray-d8: #d8d8d8;
--gray-f3: #F3F3F3; --gray-f3: #f3f3f3;
--font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace; --font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
} }

View File

@ -34,7 +34,7 @@
align-items: center; align-items: center;
} }
.dashboard-title__button{ .dashboard-title__button {
margin: 0 0.5rem; margin: 0 0.5rem;
} }
@ -44,7 +44,7 @@
align-items: flex-start; align-items: flex-start;
} }
.dashboard-title__button{ .dashboard-title__button {
margin: 0.5rem 0; margin: 0.5rem 0;
display: block; display: block;
} }

View File

@ -44,6 +44,7 @@ const Dashboard = ({
const refreshButton = <button const refreshButton = <button
type="button" type="button"
className="btn btn-icon btn-outline-primary btn-sm" className="btn btn-icon btn-outline-primary btn-sm"
title={t('refresh_btn')}
onClick={() => getAllStats()} onClick={() => getAllStats()}
> >
<svg className="icons"> <svg className="icons">

View File

@ -12,7 +12,7 @@ import {
checkSafeSearch, checkSafeSearch,
checkSafeBrowsing, checkSafeBrowsing,
checkParental, checkParental,
getFilterName, getRulesToFilterList,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants'; import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants';
import { toggleBlocking } from '../../../actions'; import { toggleBlocking } from '../../../actions';
@ -41,32 +41,27 @@ const renderBlockingButton = (isFiltered, domain) => {
</button>; </button>;
}; };
const getTitle = (reason) => { const getTitle = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const filters = useSelector((state) => state.filtering.filters, shallowEqual); const filters = useSelector((state) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
const filter_id = useSelector((state) => state.filtering.check.filter_id); const rules = useSelector((state) => state.filtering.check.rules, shallowEqual);
const reason = useSelector((state) => state.filtering.check.reason);
const filterName = getFilterName(
filters,
whitelistFilters,
filter_id,
'filtered_custom_rules',
(filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''),
);
const getReasonFiltered = (reason) => { const getReasonFiltered = (reason) => {
const filterKey = reason.replace(FILTERED, ''); const filterKey = reason.replace(FILTERED, '');
return i18next.t('query_log_filtered', { filter: filterKey }); return i18next.t('query_log_filtered', { filter: filterKey });
}; };
const ruleAndFilterNames = getRulesToFilterList(rules, filters, whitelistFilters);
const REASON_TO_TITLE_MAP = { const REASON_TO_TITLE_MAP = {
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'), [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'),
[FILTERED_STATUS.REWRITE]: t('rewrite_applied'), [FILTERED_STATUS.REWRITE]: t('rewrite_applied'),
[FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'), [FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'),
[FILTERED_STATUS.FILTERED_BLACK_LIST]: filterName, [FILTERED_STATUS.FILTERED_BLACK_LIST]: ruleAndFilterNames,
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: filterName, [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: ruleAndFilterNames,
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason),
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason),
[FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason),
@ -78,7 +73,11 @@ const getTitle = (reason) => {
return <> return <>
<div>{t('check_reason', { reason })}</div> <div>{t('check_reason', { reason })}</div>
<div>{filterName}</div> <div>
{t('rule_label')}:
&nbsp;
{ruleAndFilterNames}
</div>
</>; </>;
}; };
@ -86,14 +85,13 @@ const Info = () => {
const { const {
hostname, hostname,
reason, reason,
rule,
service_name, service_name,
cname, cname,
ip_addrs, ip_addrs,
} = useSelector((state) => state.filtering.check, shallowEqual); } = useSelector((state) => state.filtering.check, shallowEqual);
const { t } = useTranslation(); const { t } = useTranslation();
const title = getTitle(reason); const title = getTitle();
const className = classNames('card mb-0 p-3', { const className = classNames('card mb-0 p-3', {
'logs__row--red': checkFiltered(reason), 'logs__row--red': checkFiltered(reason),
@ -112,7 +110,6 @@ const Info = () => {
<div>{title}</div> <div>{title}</div>
{!onlyFiltered {!onlyFiltered
&& <> && <>
{rule && <div>{t('check_rule', { rule })}</div>}
{service_name && <div>{t('check_service', { service: service_name })}</div>} {service_name && <div>{t('check_service', { service: service_name })}</div>}
{cname && <div>{t('check_cname', { cname })}</div>} {cname && <div>{t('check_cname', { cname })}</div>}
{ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>} {ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}

View File

@ -46,7 +46,7 @@ const Header = () => {
<div className="header__column"> <div className="header__column">
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Link to="/" className="nav-link pl-0 pr-1"> <Link to="/" className="nav-link pl-0 pr-1">
<img src={logo} alt="" className="header-brand-img" /> <img src={logo} alt="AdGuard Home logo" className="header-brand-img" />
</Link> </Link>
{!processing && isCoreRunning {!processing && isCoreRunning
&& <span className={badgeClass} && <span className={badgeClass}

View File

@ -70,7 +70,7 @@
} }
.grid .key-colon:nth-child(odd)::after { .grid .key-colon:nth-child(odd)::after {
content: ':'; content: ":";
} }
.grid__one-row { .grid__one-row {
@ -95,7 +95,7 @@
} }
.title--border:before { .title--border:before {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
border-top: 0.5px solid var(--gray-d8) !important; border-top: 0.5px solid var(--gray-d8) !important;

View File

@ -4,8 +4,9 @@ import classNames from 'classnames';
import React from 'react'; import React from 'react';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import { import {
getRulesToFilterList,
formatElapsedMs, formatElapsedMs,
getFilterName, getFilterNames,
getServiceName, getServiceName,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants'; import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
@ -18,8 +19,7 @@ const ResponseCell = ({
response, response,
status, status,
upstream, upstream,
rule, rules,
filterId,
service_name, service_name,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -36,7 +36,6 @@ const ResponseCell = ({
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason); const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>; const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
const filter = getFilterName(filters, whitelistFilters, filterId);
const renderResponses = (responseArr) => { const renderResponses = (responseArr) => {
if (!responseArr || responseArr.length === 0) { if (!responseArr || responseArr.length === 0) {
@ -52,18 +51,23 @@ const ResponseCell = ({
})}</div>; })}</div>;
}; };
const rulesList = getRulesToFilterList(rules, filters, whitelistFilters);
const COMMON_CONTENT = { const COMMON_CONTENT = {
encryption_status: boldStatusLabel, encryption_status: boldStatusLabel,
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
response_code: status, response_code: status,
...(service_name ? { service_name: getServiceName(service_name) } : { filter }), ...(service_name
rule_label: rule, ? { service_name: getServiceName(service_name) }
: { }
),
rule_label: rulesList,
response_table_header: renderResponses(response), response_table_header: renderResponses(response),
original_response: renderResponses(originalResponse), original_response: renderResponses(originalResponse),
}; };
const content = rule const content = rules.length > 0
? Object.entries(COMMON_CONTENT) ? Object.entries(COMMON_CONTENT)
: Object.entries({ : Object.entries({
...COMMON_CONTENT, ...COMMON_CONTENT,
@ -78,7 +82,8 @@ const ResponseCell = ({
} }
return getServiceName(service_name); return getServiceName(service_name);
case FILTERED_STATUS.FILTERED_BLACK_LIST: case FILTERED_STATUS.FILTERED_BLACK_LIST:
return filter; case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
return getFilterNames(rules, filters, whitelistFilters).join(', ');
default: default:
return formattedElapsedMs; return formattedElapsedMs;
} }
@ -113,8 +118,10 @@ ResponseCell.propTypes = {
response: propTypes.array.isRequired, response: propTypes.array.isRequired,
status: propTypes.string.isRequired, status: propTypes.string.isRequired,
upstream: propTypes.string.isRequired, upstream: propTypes.string.isRequired,
rule: propTypes.string, rules: propTypes.arrayOf(propTypes.shape({
filterId: propTypes.number, text: propTypes.string.isRequired,
filter_list_id: propTypes.number.isRequired,
})),
service_name: propTypes.string, service_name: propTypes.string,
}; };

View File

@ -6,11 +6,11 @@ import propTypes from 'prop-types';
import { import {
captitalizeWords, captitalizeWords,
checkFiltered, checkFiltered,
getRulesToFilterList,
formatDateTime, formatDateTime,
formatElapsedMs, formatElapsedMs,
formatTime, formatTime,
getBlockingClientName, getBlockingClientName,
getFilterName,
getServiceName, getServiceName,
processContent, processContent,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
@ -70,8 +70,7 @@ const Row = memo(({
upstream, upstream,
type, type,
client_proto, client_proto,
filterId, rules,
rule,
originalResponse, originalResponse,
status, status,
service_name, service_name,
@ -107,8 +106,6 @@ const Row = memo(({
const sourceData = getSourceData(tracker); const sourceData = getSourceData(tracker);
const filter = getFilterName(filters, whitelistFilters, filterId);
const { const {
confirmMessage, confirmMessage,
buttonKey: blockingClientKey, buttonKey: blockingClientKey,
@ -172,8 +169,8 @@ const Row = memo(({
response_details: 'title', response_details: 'title',
install_settings_dns: upstream, install_settings_dns: upstream,
elapsed: formattedElapsedMs, elapsed: formattedElapsedMs,
filter: rule ? filter : null, rule_label: rules.length > 0
rule_label: rule, && getRulesToFilterList(rules, filters, whitelistFilters),
response_table_header: response?.join('\n'), response_table_header: response?.join('\n'),
response_code: status, response_code: status,
client_details: 'title', client_details: 'title',
@ -235,8 +232,10 @@ Row.propTypes = {
upstream: propTypes.string.isRequired, upstream: propTypes.string.isRequired,
type: propTypes.string.isRequired, type: propTypes.string.isRequired,
client_proto: propTypes.string.isRequired, client_proto: propTypes.string.isRequired,
filterId: propTypes.number, rules: propTypes.arrayOf(propTypes.shape({
rule: propTypes.string, text: propTypes.string.isRequired,
filter_list_id: propTypes.number.isRequired,
})),
originalResponse: propTypes.array, originalResponse: propTypes.array,
status: propTypes.string.isRequired, status: propTypes.string.isRequired,
service_name: propTypes.string, service_name: propTypes.string,

View File

@ -9,21 +9,18 @@
--size-response: 150; --size-response: 150;
--size-client: 123; --size-client: 123;
--gray-216: rgba(216, 216, 216, 0.23); --gray-216: rgba(216, 216, 216, 0.23);
--gray-4d: #4D4D4D; --gray-4d: #4d4d4d;
--gray-f3: #F3F3F3; --gray-f3: #f3f3f3;
--gray-8: #888; --gray-8: #888;
--gray-3: #333; --gray-3: #333;
--danger: #DF3812; --danger: #df3812;
--white80: rgba(255, 255, 255, 0.8); --white80: rgba(255, 255, 255, 0.8);
--btn-block: #c23814;
--btn-block: #C23814; --btn-block-disabled: #e3b3a6;
--btn-block-disabled: #E3B3A6; --btn-block-active: #a62200;
--btn-block-active: #A62200;
--btn-unblock: #888888; --btn-unblock: #888888;
--btn-unblock-disabled: #D8D8D8; --btn-unblock-disabled: #d8d8d8;
--btn-unblock-active: #4D4D4D; --btn-unblock-active: #4d4d4d;
--option-border-radius: 4px; --option-border-radius: 4px;
} }
@ -87,7 +84,7 @@
} }
.custom-select__arrow--left { .custom-select__arrow--left {
background: var(--white) url('../ui/svg/chevron-down.svg') no-repeat; background: var(--white) url("../ui/svg/chevron-down.svg") no-repeat;
background-position: 5px 9px; background-position: 5px 9px;
background-size: 22px; background-size: 22px;
} }
@ -431,3 +428,13 @@
margin-right: 1px; margin-right: 1px;
opacity: 0.5; opacity: 0.5;
} }
.filteringRules__rule {
margin-bottom: 0;
}
.filteringRules__filter {
font-style: italic;
font-weight: normal;
margin-bottom: 1rem;
}

View File

@ -6,18 +6,21 @@
.icon--24 { .icon--24 {
--size: 1.5rem; --size: 1.5rem;
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
} }
.icon--20 { .icon--20 {
--size: 1.25rem; --size: 1.25rem;
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
} }
.icon--18 { .icon--18 {
--size: 1.125rem; --size: 1.125rem;
width: var(--size); width: var(--size);
height: var(--size); height: var(--size);
} }

View File

@ -7,6 +7,7 @@ import i18n from 'i18next';
import uniqBy from 'lodash/uniqBy'; import uniqBy from 'lodash/uniqBy';
import ipaddr from 'ipaddr.js'; import ipaddr from 'ipaddr.js';
import queryString from 'query-string'; import queryString from 'query-string';
import React from 'react';
import { getTrackerData } from './trackers/trackers'; import { getTrackerData } from './trackers/trackers';
import { import {
@ -68,6 +69,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
time, time,
filterId, filterId,
rule, rule,
rules,
service_name, service_name,
original_answer, original_answer,
upstream, upstream,
@ -80,6 +82,15 @@ export const normalizeLogs = (logs) => logs.map((log) => {
return `${type}: ${value} (ttl=${ttl})`; return `${type}: ${value} (ttl=${ttl})`;
}) : []); }) : []);
let newRules = rules;
/* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */
if (rule !== undefined && filterId !== undefined && rules !== undefined && rules.length === 0) {
newRules = {
filter_list_id: filterId,
text: rule,
};
}
return { return {
time, time,
domain, domain,
@ -88,8 +99,10 @@ export const normalizeLogs = (logs) => logs.map((log) => {
reason, reason,
client, client,
client_proto, client_proto,
/* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */
filterId, filterId,
rule, rule,
rules: newRules,
status, status,
service_name, service_name,
originalAnswer: original_answer, originalAnswer: original_answer,
@ -190,7 +203,12 @@ export const getIpList = (interfaces) => Object.values(interfaces)
.reduce((acc, curr) => acc.concat(curr.ip_addresses), []) .reduce((acc, curr) => acc.concat(curr.ip_addresses), [])
.sort(); .sort();
export const getDnsAddress = (ip, port = '') => { /**
* @param {string} ip
* @param {number} [port]
* @returns {string}
*/
export const getDnsAddress = (ip, port = 0) => {
const isStandardDnsPort = port === STANDARD_DNS_PORT; const isStandardDnsPort = port === STANDARD_DNS_PORT;
let address = ip; let address = ip;
@ -205,7 +223,12 @@ export const getDnsAddress = (ip, port = '') => {
return address; return address;
}; };
export const getWebAddress = (ip, port = '') => { /**
* @param {string} ip
* @param {number} [port]
* @returns {string}
*/
export const getWebAddress = (ip, port = 0) => {
const isStandardWebPort = port === STANDARD_WEB_PORT; const isStandardWebPort = port === STANDARD_WEB_PORT;
let address = `http://${ip}`; let address = `http://${ip}`;
@ -716,6 +739,75 @@ export const getFilterName = (
return resolveFilterName(filter); return resolveFilterName(filter);
}; };
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @returns {string[]}
*/
export const getFilterNames = (rules, filters, whitelistFilters) => rules.map(
({ filter_list_id }) => getFilterName(filters, whitelistFilters, filter_list_id),
);
/**
* @param {array} rules
* @returns {string[]}
*/
export const getRuleNames = (rules) => rules.map(({ text }) => text);
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @returns {object}
*/
export const getFilterNameToRulesMap = (rules, filters, whitelistFilters) => rules.reduce(
(acc, { text, filter_list_id }) => {
const filterName = getFilterName(filters, whitelistFilters, filter_list_id);
acc[filterName] = (acc[filterName] || []).concat(text);
return acc;
}, {},
);
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @param {object} classes
* @returns {JSXElement}
*/
export const getRulesToFilterList = (rules, filters, whitelistFilters, classes = {
list: 'filteringRules',
rule: 'filteringRules__rule font-monospace',
filter: 'filteringRules__filter',
}) => {
const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters);
return <dl className={classes.list}>
{Object.entries(filterNameToRulesMap).reduce(
(acc, [filterName, rulesArr]) => acc
.concat(rulesArr.map((rule, i) => <dd key={i} className={classes.rule}>{rule}</dd>))
.concat(<dt className={classes.filter} key={classes.filter}>{filterName}</dt>),
[],
)}
</dl>;
};
/**
* @param {array} rules
* @param {array} filters
* @param {array} whitelistFilters
* @returns {string}
*/
export const getRulesAndFilterNames = (rules, filters, whitelistFilters) => {
const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters);
return Object.entries(filterNameToRulesMap).map(
([filterName, filterRules]) => filterRules.concat(filterName).join('\n'),
).join('\n\n');
};
/** /**
* @param ip {string} * @param ip {string}
* @param gateway_ip {string} * @param gateway_ip {string}

View File

@ -31,7 +31,7 @@ const getFormattedWhois = (whois) => {
* @param {object} info.whois_info * @param {object} info.whois_info
* @param {boolean} [isDetailed] * @param {boolean} [isDetailed]
* @param {boolean} [isLogs] * @param {boolean} [isLogs]
* @returns {JSX.Element} * @returns {JSXElement}
*/ */
export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => { export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => {
let whoisContainer = null; let whoisContainer = null;

View File

@ -16,7 +16,7 @@ import { getLastIpv4Octet, isValidAbsolutePath } from './form';
// https://redux-form.com/8.3.0/examples/fieldlevelvalidation/ // https://redux-form.com/8.3.0/examples/fieldlevelvalidation/
// If the value is valid, the validation function should return undefined. // If the value is valid, the validation function should return undefined.
/** /**
* @param value {string} * @param value {string|number}
* @returns {undefined|string} * @returns {undefined|string}
*/ */
export const validateRequiredValue = (value) => { export const validateRequiredValue = (value) => {

View File

@ -41,16 +41,13 @@ const AddressList = ({
AddressList.propTypes = { AddressList.propTypes = {
interfaces: PropTypes.object.isRequired, interfaces: PropTypes.object.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
port: PropTypes.oneOfType([ port: PropTypes.number.isRequired,
PropTypes.string,
PropTypes.number,
]),
isDns: PropTypes.bool, isDns: PropTypes.bool,
}; };
renderItem.propTypes = { renderItem.propTypes = {
ip: PropTypes.string.isRequired, ip: PropTypes.string.isRequired,
port: PropTypes.string.isRequired, port: PropTypes.number.isRequired,
isDns: PropTypes.bool.isRequired, isDns: PropTypes.bool.isRequired,
}; };

View File

@ -24,13 +24,7 @@ const access = handleActions(
[actions.setAccessListRequest]: (state) => ({ ...state, processingSet: true }), [actions.setAccessListRequest]: (state) => ({ ...state, processingSet: true }),
[actions.setAccessListFailure]: (state) => ({ ...state, processingSet: false }), [actions.setAccessListFailure]: (state) => ({ ...state, processingSet: false }),
[actions.setAccessListSuccess]: (state) => { [actions.setAccessListSuccess]: (state) => ({ ...state, processingSet: false }),
const newState = {
...state,
processingSet: false,
};
return newState;
},
[actions.toggleClientBlockRequest]: (state) => ({ ...state, processingSet: true }), [actions.toggleClientBlockRequest]: (state) => ({ ...state, processingSet: true }),
[actions.toggleClientBlockFailure]: (state) => ({ ...state, processingSet: false }), [actions.toggleClientBlockFailure]: (state) => ({ ...state, processingSet: false }),

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.14
require ( require (
github.com/AdguardTeam/dnsproxy v0.33.7 github.com/AdguardTeam/dnsproxy v0.33.7
github.com/AdguardTeam/golibs v0.4.4 github.com/AdguardTeam/golibs v0.4.4
github.com/AdguardTeam/urlfilter v0.14.0 github.com/AdguardTeam/urlfilter v0.14.1
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.0.1 github.com/ameshkov/dnscrypt/v2 v2.0.1
github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify v1.4.9

4
go.sum
View File

@ -26,8 +26,8 @@ github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw= github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw=
github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU= github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.14.0 h1:+aAhOvZDVGzl5gTERB4pOJCL1zxMyw7vLecJJ6TQTCw= github.com/AdguardTeam/urlfilter v0.14.1 h1:imYls0fit9ojA6pP1hWFUEIjyoXbDF85ZM+G67bI48c=
github.com/AdguardTeam/urlfilter v0.14.0/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U= github.com/AdguardTeam/urlfilter v0.14.1/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=

View File

@ -7,8 +7,8 @@ import (
// DNSRewriteResult is the result of application of $dnsrewrite rules. // DNSRewriteResult is the result of application of $dnsrewrite rules.
type DNSRewriteResult struct { type DNSRewriteResult struct {
RCode rules.RCode `json:",omitempty"`
Response DNSRewriteResultResponse `json:",omitempty"` Response DNSRewriteResultResponse `json:",omitempty"`
RCode rules.RCode `json:",omitempty"`
} }
// DNSRewriteResultResponse is the collection of DNS response records // DNSRewriteResultResponse is the collection of DNS response records

View File

@ -379,7 +379,7 @@ func processFilteringAfterResponse(ctx *dnsContext) int {
if len(d.Res.Answer) != 0 { if len(d.Res.Answer) != 0 {
answer := []dns.RR{} answer := []dns.RR{}
answer = append(answer, s.genCNAMEAnswer(d.Req, res.CanonName)) answer = append(answer, s.genAnswerCNAME(d.Req, res.CanonName))
answer = append(answer, d.Res.Answer...) answer = append(answer, d.Res.Answer...)
d.Res.Answer = answer d.Res.Answer = answer
} }

View File

@ -13,27 +13,55 @@ import (
) )
// filterDNSRewriteResponse handles a single DNS rewrite response entry. // filterDNSRewriteResponse handles a single DNS rewrite response entry.
// It returns the constructed answer resource record. // It returns the properly constructed answer resource record.
func (s *Server) filterDNSRewriteResponse(req *dns.Msg, rr rules.RRType, v rules.RRValue) (ans dns.RR, err error) { func (s *Server) filterDNSRewriteResponse(req *dns.Msg, rr rules.RRType, v rules.RRValue) (ans dns.RR, err error) {
// TODO(a.garipov): As more types are added, we will probably want to
// use a handler-oriented approach here. So, think of a way to decouple
// the answer generation logic from the Server.
switch rr { switch rr {
case dns.TypeA, dns.TypeAAAA: case dns.TypeA, dns.TypeAAAA:
ip, ok := v.(net.IP) ip, ok := v.(net.IP)
if !ok { if !ok {
return nil, fmt.Errorf("value has type %T, not net.IP", v) return nil, fmt.Errorf("value for rr type %d has type %T, not net.IP", rr, v)
} }
if rr == dns.TypeA { if rr == dns.TypeA {
return s.genAAnswer(req, ip.To4()), nil return s.genAnswerA(req, ip.To4()), nil
} }
return s.genAAAAAnswer(req, ip), nil return s.genAnswerAAAA(req, ip), nil
case dns.TypeTXT: case dns.TypePTR,
dns.TypeTXT:
str, ok := v.(string) str, ok := v.(string)
if !ok { if !ok {
return nil, fmt.Errorf("value has type %T, not string", v) return nil, fmt.Errorf("value for rr type %d has type %T, not string", rr, v)
} }
return s.genTXTAnswer(req, []string{str}), nil if rr == dns.TypeTXT {
return s.genAnswerTXT(req, []string{str}), nil
}
return s.genAnswerPTR(req, str), nil
case dns.TypeMX:
mx, ok := v.(*rules.DNSMX)
if !ok {
return nil, fmt.Errorf("value for rr type %d has type %T, not *rules.DNSMX", rr, v)
}
return s.genAnswerMX(req, mx), nil
case dns.TypeHTTPS,
dns.TypeSVCB:
svcb, ok := v.(*rules.DNSSVCB)
if !ok {
return nil, fmt.Errorf("value for rr type %d has type %T, not *rules.DNSSVCB", rr, v)
}
if rr == dns.TypeHTTPS {
return s.genAnswerHTTPS(req, svcb), nil
}
return s.genAnswerSVCB(req, svcb), nil
default: default:
log.Debug("don't know how to handle dns rr type %d, skipping", rr) log.Debug("don't know how to handle dns rr type %d, skipping", rr)

View File

@ -87,17 +87,17 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
name := host name := host
if len(res.CanonName) != 0 { if len(res.CanonName) != 0 {
resp.Answer = append(resp.Answer, s.genCNAMEAnswer(req, res.CanonName)) resp.Answer = append(resp.Answer, s.genAnswerCNAME(req, res.CanonName))
name = res.CanonName name = res.CanonName
} }
for _, ip := range res.IPList { for _, ip := range res.IPList {
if req.Question[0].Qtype == dns.TypeA { if req.Question[0].Qtype == dns.TypeA {
a := s.genAAnswer(req, ip.To4()) a := s.genAnswerA(req, ip.To4())
a.Hdr.Name = dns.Fqdn(name) a.Hdr.Name = dns.Fqdn(name)
resp.Answer = append(resp.Answer, a) resp.Answer = append(resp.Answer, a)
} else if req.Question[0].Qtype == dns.TypeAAAA { } else if req.Question[0].Qtype == dns.TypeAAAA {
a := s.genAAAAAnswer(req, ip) a := s.genAnswerAAAA(req, ip)
a.Hdr.Name = dns.Fqdn(name) a.Hdr.Name = dns.Fqdn(name)
resp.Answer = append(resp.Answer, a) resp.Answer = append(resp.Answer, a)
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -92,48 +93,64 @@ func (s *Server) genServerFailure(request *dns.Msg) *dns.Msg {
func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg { func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := s.makeResponse(request) resp := s.makeResponse(request)
resp.Answer = append(resp.Answer, s.genAAnswer(request, ip)) resp.Answer = append(resp.Answer, s.genAnswerA(request, ip))
return resp return resp
} }
func (s *Server) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg { func (s *Server) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := s.makeResponse(request) resp := s.makeResponse(request)
resp.Answer = append(resp.Answer, s.genAAAAAnswer(request, ip)) resp.Answer = append(resp.Answer, s.genAnswerAAAA(request, ip))
return resp return resp
} }
func (s *Server) genAAnswer(req *dns.Msg, ip net.IP) *dns.A { func (s *Server) hdr(req *dns.Msg, rrType rules.RRType) (h dns.RR_Header) {
answer := new(dns.A) return dns.RR_Header{
answer.Hdr = dns.RR_Header{
Name: req.Question[0].Name, Name: req.Question[0].Name,
Rrtype: dns.TypeA, Rrtype: rrType,
Ttl: s.conf.BlockedResponseTTL, Ttl: s.conf.BlockedResponseTTL,
Class: dns.ClassINET, Class: dns.ClassINET,
} }
answer.A = ip
return answer
} }
func (s *Server) genAAAAAnswer(req *dns.Msg, ip net.IP) *dns.AAAA { func (s *Server) genAnswerA(req *dns.Msg, ip net.IP) (ans *dns.A) {
answer := new(dns.AAAA) return &dns.A{
answer.Hdr = dns.RR_Header{ Hdr: s.hdr(req, dns.TypeA),
Name: req.Question[0].Name, A: ip,
Rrtype: dns.TypeAAAA,
Ttl: s.conf.BlockedResponseTTL,
Class: dns.ClassINET,
} }
answer.AAAA = ip
return answer
} }
func (s *Server) genTXTAnswer(req *dns.Msg, strs []string) (answer *dns.TXT) { func (s *Server) genAnswerAAAA(req *dns.Msg, ip net.IP) (ans *dns.AAAA) {
return &dns.AAAA{
Hdr: s.hdr(req, dns.TypeAAAA),
AAAA: ip,
}
}
func (s *Server) genAnswerCNAME(req *dns.Msg, cname string) (ans *dns.CNAME) {
return &dns.CNAME{
Hdr: s.hdr(req, dns.TypeCNAME),
Target: dns.Fqdn(cname),
}
}
func (s *Server) genAnswerMX(req *dns.Msg, mx *rules.DNSMX) (ans *dns.MX) {
return &dns.MX{
Hdr: s.hdr(req, dns.TypePTR),
Preference: mx.Preference,
Mx: mx.Exchange,
}
}
func (s *Server) genAnswerPTR(req *dns.Msg, ptr string) (ans *dns.PTR) {
return &dns.PTR{
Hdr: s.hdr(req, dns.TypePTR),
Ptr: ptr,
}
}
func (s *Server) genAnswerTXT(req *dns.Msg, strs []string) (ans *dns.TXT) {
return &dns.TXT{ return &dns.TXT{
Hdr: dns.RR_Header{ Hdr: s.hdr(req, dns.TypeTXT),
Name: req.Question[0].Name,
Rrtype: dns.TypeTXT,
Ttl: s.conf.BlockedResponseTTL,
Class: dns.ClassINET,
},
Txt: strs, Txt: strs,
} }
} }
@ -198,19 +215,6 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
return resp return resp
} }
// Make a CNAME response
func (s *Server) genCNAMEAnswer(req *dns.Msg, cname string) *dns.CNAME {
answer := new(dns.CNAME)
answer.Hdr = dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: dns.TypeCNAME,
Ttl: s.conf.BlockedResponseTTL,
Class: dns.ClassINET,
}
answer.Target = dns.Fqdn(cname)
return answer
}
// Create REFUSED DNS response // Create REFUSED DNS response
func (s *Server) makeResponseREFUSED(request *dns.Msg) *dns.Msg { func (s *Server) makeResponseREFUSED(request *dns.Msg) *dns.Msg {
resp := dns.Msg{} resp := dns.Msg{}

View File

@ -0,0 +1,168 @@
package dnsforward
import (
"encoding/base64"
"net"
"strconv"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
)
// genAnswerHTTPS returns a properly initialized HTTPS resource record.
//
// See the comment on genAnswerSVCB for a list of current restrictions on
// parameter values.
func (s *Server) genAnswerHTTPS(req *dns.Msg, svcb *rules.DNSSVCB) (ans *dns.HTTPS) {
ans = &dns.HTTPS{
SVCB: *s.genAnswerSVCB(req, svcb),
}
ans.Hdr.Rrtype = dns.TypeHTTPS
return ans
}
// strToSVCBKey is the string-to-svcb-key mapping.
//
// See https://github.com/miekg/dns/blob/23c4faca9d32b0abbb6e179aa1aadc45ac53a916/svcb.go#L27.
//
// TODO(a.garipov): Propose exporting this API or something similar in the
// github.com/miekg/dns module.
var strToSVCBKey = map[string]dns.SVCBKey{
"alpn": dns.SVCB_ALPN,
"echconfig": dns.SVCB_ECHCONFIG,
"ipv4hint": dns.SVCB_IPV4HINT,
"ipv6hint": dns.SVCB_IPV6HINT,
"mandatory": dns.SVCB_MANDATORY,
"no-default-alpn": dns.SVCB_NO_DEFAULT_ALPN,
"port": dns.SVCB_PORT,
}
// svcbKeyHandler is a handler for one SVCB parameter key.
type svcbKeyHandler func(valStr string) (val dns.SVCBKeyValue)
// svcbKeyHandlers are the supported SVCB parameters handlers.
var svcbKeyHandlers = map[string]svcbKeyHandler{
"alpn": func(valStr string) (val dns.SVCBKeyValue) {
return &dns.SVCBAlpn{
Alpn: []string{valStr},
}
},
"echconfig": func(valStr string) (val dns.SVCBKeyValue) {
ech, err := base64.StdEncoding.DecodeString(valStr)
if err != nil {
log.Debug("can't parse svcb/https echconfig: %s; ignoring", err)
return nil
}
return &dns.SVCBECHConfig{
ECH: ech,
}
},
"ipv4hint": func(valStr string) (val dns.SVCBKeyValue) {
ip := net.ParseIP(valStr)
if ip4 := ip.To4(); ip == nil || ip4 == nil {
log.Debug("can't parse svcb/https ipv4 hint %q; ignoring", valStr)
return nil
}
return &dns.SVCBIPv4Hint{
Hint: []net.IP{ip},
}
},
"ipv6hint": func(valStr string) (val dns.SVCBKeyValue) {
ip := net.ParseIP(valStr)
if ip == nil {
log.Debug("can't parse svcb/https ipv6 hint %q; ignoring", valStr)
return nil
}
return &dns.SVCBIPv6Hint{
Hint: []net.IP{ip},
}
},
"mandatory": func(valStr string) (val dns.SVCBKeyValue) {
code, ok := strToSVCBKey[valStr]
if !ok {
log.Debug("unknown svcb/https mandatory key %q, ignoring", valStr)
return nil
}
return &dns.SVCBMandatory{
Code: []dns.SVCBKey{code},
}
},
"no-default-alpn": func(_ string) (val dns.SVCBKeyValue) {
return &dns.SVCBNoDefaultAlpn{}
},
"port": func(valStr string) (val dns.SVCBKeyValue) {
port64, err := strconv.ParseUint(valStr, 10, 16)
if err != nil {
log.Debug("can't parse svcb/https port: %s; ignoring", err)
return nil
}
return &dns.SVCBPort{
Port: uint16(port64),
}
},
}
// genAnswerSVCB returns a properly initialized SVCB resource record.
//
// Currently, there are several restrictions on how the parameters are parsed.
// Firstly, the parsing of non-contiguous values isn't supported. Secondly, the
// parsing of value-lists is not supported either.
//
// ipv4hint=127.0.0.1 // Supported.
// ipv4hint="127.0.0.1" // Unsupported.
// ipv4hint=127.0.0.1,127.0.0.2 // Unsupported.
// ipv4hint="127.0.0.1,127.0.0.2" // Unsupported.
//
// TODO(a.garipov): Support all of these.
func (s *Server) genAnswerSVCB(req *dns.Msg, svcb *rules.DNSSVCB) (ans *dns.SVCB) {
ans = &dns.SVCB{
Hdr: s.hdr(req, dns.TypeSVCB),
Priority: svcb.Priority,
Target: svcb.Target,
}
if len(svcb.Params) == 0 {
return ans
}
values := make([]dns.SVCBKeyValue, 0, len(svcb.Params))
for k, valStr := range svcb.Params {
handler, ok := svcbKeyHandlers[k]
if !ok {
log.Debug("unknown svcb/https key %q, ignoring", k)
continue
}
val := handler(valStr)
if val == nil {
continue
}
values = append(values, val)
}
if len(values) > 0 {
ans.Value = values
}
return ans
}

View File

@ -0,0 +1,154 @@
package dnsforward
import (
"net"
"testing"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
func TestGenAnswerHTTPS_andSVCB(t *testing.T) {
// Preconditions.
s := &Server{
conf: ServerConfig{
FilteringConfig: FilteringConfig{
BlockedResponseTTL: 3600,
},
},
}
req := &dns.Msg{
Question: []dns.Question{{
Name: "abcd",
}},
}
// Constants and helper values.
const host = "example.com"
const prio = 32
ip4 := net.IPv4(127, 0, 0, 1)
ip6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
// Helper functions.
dnssvcb := func(key, value string) (svcb *rules.DNSSVCB) {
svcb = &rules.DNSSVCB{
Target: host,
Priority: prio,
}
if key == "" {
return svcb
}
svcb.Params = map[string]string{
key: value,
}
return svcb
}
wantsvcb := func(kv dns.SVCBKeyValue) (want *dns.SVCB) {
want = &dns.SVCB{
Hdr: s.hdr(req, dns.TypeSVCB),
Priority: prio,
Target: host,
}
if kv == nil {
return want
}
want.Value = []dns.SVCBKeyValue{kv}
return want
}
// Tests.
testCases := []struct {
svcb *rules.DNSSVCB
want *dns.SVCB
name string
}{{
svcb: dnssvcb("", ""),
want: wantsvcb(nil),
name: "no_params",
}, {
svcb: dnssvcb("foo", "bar"),
want: wantsvcb(nil),
name: "invalid",
}, {
svcb: dnssvcb("alpn", "h3"),
want: wantsvcb(&dns.SVCBAlpn{Alpn: []string{"h3"}}),
name: "alpn",
}, {
svcb: dnssvcb("echconfig", "AAAA"),
want: wantsvcb(&dns.SVCBECHConfig{ECH: []byte{0, 0, 0}}),
name: "echconfig",
}, {
svcb: dnssvcb("echconfig", "%BAD%"),
want: wantsvcb(nil),
name: "echconfig_invalid",
}, {
svcb: dnssvcb("ipv4hint", "127.0.0.1"),
want: wantsvcb(&dns.SVCBIPv4Hint{Hint: []net.IP{ip4}}),
name: "ipv4hint",
}, {
svcb: dnssvcb("ipv4hint", "127.0.01"),
want: wantsvcb(nil),
name: "ipv4hint_invalid",
}, {
svcb: dnssvcb("ipv6hint", "::1"),
want: wantsvcb(&dns.SVCBIPv6Hint{Hint: []net.IP{ip6}}),
name: "ipv6hint",
}, {
svcb: dnssvcb("ipv6hint", ":::1"),
want: wantsvcb(nil),
name: "ipv6hint_invalid",
}, {
svcb: dnssvcb("mandatory", "alpn"),
want: wantsvcb(&dns.SVCBMandatory{Code: []dns.SVCBKey{dns.SVCB_ALPN}}),
name: "mandatory",
}, {
svcb: dnssvcb("mandatory", "alpnn"),
want: wantsvcb(nil),
name: "mandatory_invalid",
}, {
svcb: dnssvcb("no-default-alpn", ""),
want: wantsvcb(&dns.SVCBNoDefaultAlpn{}),
name: "no-default-alpn",
}, {
svcb: dnssvcb("port", "8080"),
want: wantsvcb(&dns.SVCBPort{Port: 8080}),
name: "port",
}, {
svcb: dnssvcb("port", "1005008080"),
want: wantsvcb(nil),
name: "port",
}}
for _, tc := range testCases {
t.Run("https", func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
want := &dns.HTTPS{SVCB: *tc.want}
want.Hdr.Rrtype = dns.TypeHTTPS
got := s.genAnswerHTTPS(req, tc.svcb)
assert.Equal(t, want, got)
})
})
t.Run("svcb", func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
got := s.genAnswerSVCB(req, tc.svcb)
assert.Equal(t, tc.want, got)
})
})
}
}

View File

@ -59,10 +59,10 @@ func (s *session) deserialize(data []byte) bool {
// Auth - global object // Auth - global object
type Auth struct { type Auth struct {
db *bbolt.DB db *bbolt.DB
sessions map[string]*session // session name -> session data sessions map[string]*session
lock sync.Mutex
users []User users []User
sessionTTL uint32 // in seconds lock sync.Mutex
sessionTTL uint32
} }
// User object // User object
@ -223,24 +223,35 @@ func (a *Auth) removeSession(sess []byte) {
log.Debug("Auth: removed session from DB") log.Debug("Auth: removed session from DB")
} }
// CheckSession - check if session is valid // checkSessionResult is the result of checking a session.
// Return 0 if OK; -1 if session doesn't exist; 1 if session has expired type checkSessionResult int
func (a *Auth) CheckSession(sess string) int {
// checkSessionResult constants.
const (
checkSessionOK checkSessionResult = 0
checkSessionNotFound checkSessionResult = -1
checkSessionExpired checkSessionResult = 1
)
// checkSession checks if the session is valid.
func (a *Auth) checkSession(sess string) (res checkSessionResult) {
now := uint32(time.Now().UTC().Unix()) now := uint32(time.Now().UTC().Unix())
update := false update := false
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock()
s, ok := a.sessions[sess] s, ok := a.sessions[sess]
if !ok { if !ok {
a.lock.Unlock() return checkSessionNotFound
return -1
} }
if s.expire <= now { if s.expire <= now {
delete(a.sessions, sess) delete(a.sessions, sess)
key, _ := hex.DecodeString(sess) key, _ := hex.DecodeString(sess)
a.removeSession(key) a.removeSession(key)
a.lock.Unlock()
return 1 return checkSessionExpired
} }
newExpire := now + a.sessionTTL newExpire := now + a.sessionTTL
@ -250,8 +261,6 @@ func (a *Auth) CheckSession(sess string) int {
s.expire = newExpire s.expire = newExpire
} }
a.lock.Unlock()
if update { if update {
key, _ := hex.DecodeString(sess) key, _ := hex.DecodeString(sess)
if a.storeSession(key, s) { if a.storeSession(key, s) {
@ -259,7 +268,7 @@ func (a *Auth) CheckSession(sess string) int {
} }
} }
return 0 return checkSessionOK
} }
// RemoveSession - remove session // RemoveSession - remove session
@ -392,8 +401,8 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (authFirst bool)
ok = true ok = true
} else if err == nil { } else if err == nil {
r := Context.auth.CheckSession(cookie.Value) r := Context.auth.checkSession(cookie.Value)
if r == 0 { if r == checkSessionOK {
ok = true ok = true
} else if r < 0 { } else if r < 0 {
log.Debug("Auth: invalid cookie value: %s", cookie) log.Debug("Auth: invalid cookie value: %s", cookie)
@ -434,12 +443,13 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
authRequired := Context.auth != nil && Context.auth.AuthRequired() authRequired := Context.auth != nil && Context.auth.AuthRequired()
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil { if authRequired && err == nil {
r := Context.auth.CheckSession(cookie.Value) r := Context.auth.checkSession(cookie.Value)
if r == 0 { if r == checkSessionOK {
w.Header().Set("Location", "/") w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
return return
} else if r < 0 { } else if r == checkSessionNotFound {
log.Debug("Auth: invalid cookie value: %s", cookie) log.Debug("Auth: invalid cookie value: %s", cookie)
} }
} }
@ -503,32 +513,34 @@ func (a *Auth) UserFind(login, password string) User {
return User{} return User{}
} }
// GetCurrentUser - get the current user // getCurrentUser returns the current user. It returns an empty User if the
func (a *Auth) GetCurrentUser(r *http.Request) User { // user is not found.
func (a *Auth) getCurrentUser(r *http.Request) User {
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if err != nil { if err != nil {
// there's no Cookie, check Basic authentication // There's no Cookie, check Basic authentication.
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if ok { if ok {
u := Context.auth.UserFind(user, pass) return Context.auth.UserFind(user, pass)
return u
} }
return User{} return User{}
} }
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock()
s, ok := a.sessions[cookie.Value] s, ok := a.sessions[cookie.Value]
if !ok { if !ok {
a.lock.Unlock()
return User{} return User{}
} }
for _, u := range a.users { for _, u := range a.users {
if u.Name == s.userName { if u.Name == s.userName {
a.lock.Unlock()
return u return u
} }
} }
a.lock.Unlock()
return User{} return User{}
} }

View File

@ -38,7 +38,7 @@ func TestAuth(t *testing.T) {
user := User{Name: "name"} user := User{Name: "name"}
a.UserAdd(&user, "password") a.UserAdd(&user, "password")
assert.True(t, a.CheckSession("notfound") == -1) assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
a.RemoveSession("notfound") a.RemoveSession("notfound")
sess, err := getSession(&users[0]) sess, err := getSession(&users[0])
@ -49,13 +49,13 @@ func TestAuth(t *testing.T) {
// check expiration // check expiration
s.expire = uint32(now) s.expire = uint32(now)
a.addSession(sess, &s) a.addSession(sess, &s)
assert.True(t, a.CheckSession(sessStr) == 1) assert.Equal(t, checkSessionExpired, a.checkSession(sessStr))
// add session with TTL = 2 sec // add session with TTL = 2 sec
s = session{} s = session{}
s.expire = uint32(time.Now().UTC().Unix() + 2) s.expire = uint32(time.Now().UTC().Unix() + 2)
a.addSession(sess, &s) a.addSession(sess, &s)
assert.True(t, a.CheckSession(sessStr) == 0) assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
a.Close() a.Close()
@ -63,8 +63,8 @@ func TestAuth(t *testing.T) {
a = InitAuth(fn, users, 60) a = InitAuth(fn, users, 60)
// the session is still alive // the session is still alive
assert.True(t, a.CheckSession(sessStr) == 0) assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
// reset our expiration time because CheckSession() has just updated it // reset our expiration time because checkSession() has just updated it
s.expire = uint32(time.Now().UTC().Unix() + 2) s.expire = uint32(time.Now().UTC().Unix() + 2)
a.storeSession(sess, &s) a.storeSession(sess, &s)
a.Close() a.Close()
@ -76,7 +76,7 @@ func TestAuth(t *testing.T) {
// load and remove expired sessions // load and remove expired sessions
a = InitAuth(fn, users, 60) a = InitAuth(fn, users, 60)
assert.True(t, a.CheckSession(sessStr) == -1) assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
a.Close() a.Close()
os.Remove(fn) os.Remove(fn)
@ -111,7 +111,7 @@ func TestAuthHTTP(t *testing.T) {
Context.auth = InitAuth(fn, users, 60) Context.auth = InitAuth(fn, users, 60)
handlerCalled := false handlerCalled := false
handler := func(w http.ResponseWriter, r *http.Request) { handler := func(_ http.ResponseWriter, _ *http.Request) {
handlerCalled = true handlerCalled = true
} }
handler2 := optionalAuth(handler) handler2 := optionalAuth(handler)

View File

@ -89,7 +89,7 @@ type profileJSON struct {
func handleGetProfile(w http.ResponseWriter, r *http.Request) { func handleGetProfile(w http.ResponseWriter, r *http.Request) {
pj := profileJSON{} pj := profileJSON{}
u := Context.auth.GetCurrentUser(r) u := Context.auth.getCurrentUser(r)
pj.Name = u.Name pj.Name = u.Name
data, err := json.Marshal(pj) data, err := json.Marshal(pj)

View File

@ -2,13 +2,14 @@ package home
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"syscall" "syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/AdGuardHome/internal/sysutil"
"github.com/AdguardTeam/AdGuardHome/internal/update" "github.com/AdguardTeam/AdGuardHome/internal/update"
@ -19,6 +20,13 @@ type getVersionJSONRequest struct {
RecheckNow bool `json:"recheck_now"` RecheckNow bool `json:"recheck_now"`
} }
// temporaryError is the interface for temporary errors from the Go standard
// library.
type temporaryError interface {
error
Temporary() (ok bool)
}
// Get the latest available version from the Internet // Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
if Context.disableUpdate { if Context.disableUpdate {
@ -41,14 +49,29 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
var info update.VersionInfo var info update.VersionInfo
for i := 0; i != 3; i++ { for i := 0; i != 3; i++ {
Context.controlLock.Lock() func() {
info, err = Context.updater.GetVersionResponse(req.RecheckNow) Context.controlLock.Lock()
Context.controlLock.Unlock() defer Context.controlLock.Unlock()
if err != nil && strings.HasSuffix(err.Error(), "i/o timeout") {
// This case may happen while we're restarting DNS server info, err = Context.updater.GetVersionResponse(req.RecheckNow)
// https://github.com/AdguardTeam/AdGuardHome/internal/issues/934 }()
continue
if err != nil {
var terr temporaryError
if errors.As(err, &terr) && terr.Temporary() {
// Temporary network error. This case may happen while
// we're restarting our DNS server. Log and sleep for
// some time.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/934.
d := time.Duration(i) * time.Second
log.Info("temp net error: %q; sleeping for %s and retrying", err, d)
time.Sleep(d)
continue
}
} }
break break
} }
if err != nil { if err != nil {

View File

@ -7,8 +7,11 @@ initialisms = [
, "DOQ" , "DOQ"
, "DOT" , "DOT"
, "EDNS" , "EDNS"
, "MX"
, "PTR"
, "QUIC" , "QUIC"
, "SDNS" , "SDNS"
, "SVCB"
] ]
dot_import_whitelist = [] dot_import_whitelist = []
http_status_code_whitelist = [] http_status_code_whitelist = []