Compare commits
22 Commits
v0.108.0-b
...
v0.108.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a1887a854 | ||
|
|
65b526b969 | ||
|
|
61ed743748 | ||
|
|
c02a14117d | ||
|
|
7b92d53b84 | ||
|
|
f7dd832517 | ||
|
|
c21f958eaf | ||
|
|
5a195b441c | ||
|
|
c46516475d | ||
|
|
97af062f7b | ||
|
|
8004b135b1 | ||
|
|
4b9264531b | ||
|
|
9a506d3755 | ||
|
|
e320eb29c2 | ||
|
|
282f11a7c2 | ||
|
|
91f3e29c08 | ||
|
|
1fd6cf1a2f | ||
|
|
efed23701a | ||
|
|
ad1bf5cf6e | ||
|
|
ee8eb1d8a6 | ||
|
|
39f5c50acd | ||
|
|
d4a4bda645 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
*.db
|
||||
*.log
|
||||
*.snap
|
||||
*.test
|
||||
/agh-backup/
|
||||
/bin/
|
||||
/build/*
|
||||
|
||||
119
CHANGELOG.md
119
CHANGELOG.md
@@ -14,15 +14,86 @@ and this project adheres to
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.33] - 2023-06-28 (APPROX.)
|
||||
## [v0.107.34] - 2023-07-26 (APPROX.)
|
||||
|
||||
See also the [v0.107.33 GitHub milestone][ms-v0.107.33].
|
||||
See also the [v0.107.34 GitHub milestone][ms-v0.107.34].
|
||||
|
||||
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
|
||||
[ms-v0.107.34]: https://github.com/AdguardTeam/AdGuardHome/milestone/69?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved CPU and RAM consumption during updates of filtering-rule lists.
|
||||
|
||||
#### Configuration Changes
|
||||
|
||||
In this release, the schema version has changed from 23 to 24.
|
||||
|
||||
- Properties starting with `log_`, and `verbose` property, which used to set up
|
||||
logging are now moved to the new object `log` containing new properties `file`,
|
||||
`max_backups`, `max_size`, `max_age`, `compress`, `local_time`, and `verbose`:
|
||||
|
||||
```yaml
|
||||
# BEFORE:
|
||||
'log_file': ""
|
||||
'log_max_backups': 0
|
||||
'log_max_size': 100
|
||||
'log_max_age': 3
|
||||
'log_compress': false
|
||||
'log_localtime': false
|
||||
'verbose': false
|
||||
|
||||
# AFTER:
|
||||
'log':
|
||||
'file': ""
|
||||
'max_backups': 0
|
||||
'max_size': 100
|
||||
'max_age': 3
|
||||
'compress': false
|
||||
'local_time': false
|
||||
'verbose': false
|
||||
```
|
||||
|
||||
To rollback this change, remove the new object `log`, set back `log_` and
|
||||
`verbose` properties and change the `schema_version` back to `23`.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Default exposure of the non-standard ports 784 and 8853 for DNS-over-QUIC in
|
||||
the `Dockerfile`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Two unspecified IPs when a host is blocked in two filter lists ([#5972]).
|
||||
- Incorrect setting of Parental Control cache size.
|
||||
- Excessive RAM and CPU consumption by Safe Browsing and Parental Control
|
||||
filters ([#5896]).
|
||||
|
||||
### Removed
|
||||
|
||||
- The `HEALTHCHECK` section and the use of `tini` in the `ENTRYPOINT` section in
|
||||
`Dockerfile` ([#5939]). They caused a lot of issues, especially with tools
|
||||
like `docker-compose` and `podman`.
|
||||
|
||||
**NOTE:** Some Docker tools may cache `ENTRYPOINT` sections, so some users may
|
||||
be required to backup their configuration, stop the container, purge the old
|
||||
image, and reload it from scratch.
|
||||
|
||||
[#5896]: https://github.com/AdguardTeam/AdGuardHome/issues/5896
|
||||
[#5972]: https://github.com/AdguardTeam/AdGuardHome/issues/5972
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [v0.107.33] - 2023-07-03
|
||||
|
||||
See also the [v0.107.33 GitHub milestone][ms-v0.107.33].
|
||||
|
||||
### Added
|
||||
|
||||
- The new command-line flag `--web-addr` is the address to serve the web UI on,
|
||||
@@ -37,8 +108,27 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
||||
|
||||
#### Configuration Changes
|
||||
|
||||
In this release, the schema version has changed from 20 to 22.
|
||||
In this release, the schema version has changed from 20 to 23.
|
||||
|
||||
- Properties `bind_host`, `bind_port`, and `web_session_ttl` which used to setup
|
||||
web UI binding configuration, are now moved to a new object `http` containing
|
||||
new properties `address` and `session_ttl`:
|
||||
|
||||
```yaml
|
||||
# BEFORE:
|
||||
'bind_host': '1.2.3.4'
|
||||
'bind_port': 8080
|
||||
'web_session_ttl': 720
|
||||
|
||||
# AFTER:
|
||||
'http':
|
||||
'address': '1.2.3.4:8080'
|
||||
'session_ttl': '720h'
|
||||
```
|
||||
|
||||
Note that the new `http.session_ttl` property is now a duration string. To
|
||||
rollback this change, remove the new object `http`, set back `bind_host`,
|
||||
`bind_port`, `web_session_ttl`, and change the `schema_version` back to `22`.
|
||||
- Property `clients.persistent.blocked_services`, which in schema versions 21
|
||||
and earlier used to be a list containing ids of blocked services, is now an
|
||||
object containing ids and schedule for blocked services:
|
||||
@@ -118,14 +208,20 @@ In this release, the schema version has changed from 20 to 22.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- The `HEALTHCHECK` section and the use of `tini` in the `ENTRYPOINT` section in
|
||||
`Dockerfile` ([#5939]). They cause a lot of issues, especially with tools
|
||||
like `docker-compose` and `podman`, and will be removed in a future release.
|
||||
- Flags `-h`, `--host`, `-p`, `--port` have been deprecated. The `-h` flag
|
||||
will work as an alias for `--help`, instead of the deprecated `--host` in the
|
||||
future releases.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ignoring of `/etc/hosts` file when resolving the hostnames of upstream DNS
|
||||
servers ([#5902]).
|
||||
- Excessive error logging when using DNS-over-QUIC ([#5285]).
|
||||
- Cannot set `bind_host` in AdGuardHome.yaml (docker version)([#4231], [#4235]).
|
||||
- Inability to set `bind_host` in `AdGuardHome.yaml` in Docker ([#4231],
|
||||
[#4235]).
|
||||
- The blocklists can now be deleted properly ([#5700]).
|
||||
- Queries with the question-section target `.`, for example `NS .`, are now
|
||||
counted in the statistics and correctly shown in the query log ([#5910]).
|
||||
@@ -138,12 +234,12 @@ In this release, the schema version has changed from 20 to 22.
|
||||
[#4235]: https://github.com/AdguardTeam/AdGuardHome/pull/4235
|
||||
[#5285]: https://github.com/AdguardTeam/AdGuardHome/issues/5285
|
||||
[#5700]: https://github.com/AdguardTeam/AdGuardHome/issues/5700
|
||||
[#5902]: https://github.com/AdguardTeam/AdGuardHome/issues/5902
|
||||
[#5910]: https://github.com/AdguardTeam/AdGuardHome/issues/5910
|
||||
[#5913]: https://github.com/AdguardTeam/AdGuardHome/issues/5913
|
||||
[#5939]: https://github.com/AdguardTeam/AdGuardHome/discussions/5939
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
|
||||
|
||||
|
||||
|
||||
@@ -2125,11 +2221,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...HEAD
|
||||
[v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...v0.107.33
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.34...HEAD
|
||||
[v0.107.34]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...v0.107.34
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...HEAD
|
||||
[v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...v0.107.33
|
||||
[v0.107.32]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.31...v0.107.32
|
||||
[v0.107.31]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.30...v0.107.31
|
||||
[v0.107.30]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...v0.107.30
|
||||
|
||||
4
Makefile
4
Makefile
@@ -78,7 +78,7 @@ build: deps quick-build
|
||||
|
||||
quick-build: js-build go-build
|
||||
|
||||
ci: deps test
|
||||
ci: deps test go-bench go-fuzz
|
||||
|
||||
deps: js-deps go-deps
|
||||
lint: js-lint go-lint
|
||||
@@ -104,8 +104,10 @@ js-deps:
|
||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
||||
|
||||
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
||||
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
|
||||
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
|
||||
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"client_settings": "Cài đặt máy khách",
|
||||
"client_settings": "Cài đặt thiết bị",
|
||||
"example_upstream_reserved": "ngược dòng <0>cho các miền cụ thể</0>;",
|
||||
"example_upstream_comment": "một lời bình luận.",
|
||||
"upstream_parallel": "Sử dụng truy vấn song song để tăng tốc độ giải quyết bằng cách truy vấn đồng thời tất cả các máy chủ ngược tuyến",
|
||||
|
||||
@@ -513,6 +513,7 @@
|
||||
"statistics_clear_confirm": "您確定要清除統計資料嗎?",
|
||||
"statistics_retention_confirm": "您確定要更改統計資料保存時間嗎?如果您縮短期限部分資料可能將會遺失",
|
||||
"statistics_cleared": "已清除統計資料",
|
||||
"statistics_enable": "啟用統計數據",
|
||||
"interval_hours": "{{count}} 小時",
|
||||
"interval_hours_plural": "{{count}} 小時",
|
||||
"filters_configuration": "過濾器設定",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { shallowEqual, useSelector } from 'react-redux';
|
||||
import Card from '../ui/Card';
|
||||
import { formatNumber } from '../../helpers/helpers';
|
||||
import LogsSearchLink from '../ui/LogsSearchLink';
|
||||
import { RESPONSE_FILTER } from '../../helpers/constants';
|
||||
import { RESPONSE_FILTER, DAY } from '../../helpers/constants';
|
||||
import Tooltip from '../ui/Tooltip';
|
||||
|
||||
const Row = ({
|
||||
@@ -54,12 +54,12 @@ const Counters = ({ refreshButton, subtitle }) => {
|
||||
avgProcessingTime,
|
||||
} = useSelector((state) => state.stats, shallowEqual);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const days = interval / DAY;
|
||||
const rows = [
|
||||
{
|
||||
label: 'dns_query',
|
||||
count: numDnsQueries,
|
||||
tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }),
|
||||
tooltipTitle: days === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: days }),
|
||||
response_status: RESPONSE_FILTER.ALL.QUERY,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"timeUpdated": "2023-06-26T13:46:24.414Z",
|
||||
"timeUpdated": "2023-07-01T00:11:37.465Z",
|
||||
"categories": {
|
||||
"0": "audio_video_player",
|
||||
"1": "comments",
|
||||
@@ -3348,6 +3348,13 @@
|
||||
"url": "https://www.microsoft.com/",
|
||||
"companyId": "microsoft"
|
||||
},
|
||||
"binge": {
|
||||
"name": "Binge",
|
||||
"categoryId": 0,
|
||||
"url": "https://binge.com.au/",
|
||||
"companyId": "foxtel",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"binlayer": {
|
||||
"name": "BinLayer",
|
||||
"categoryId": 4,
|
||||
@@ -7164,6 +7171,13 @@
|
||||
"url": "http://flagcounter.com/",
|
||||
"companyId": "flag_counter"
|
||||
},
|
||||
"flash": {
|
||||
"name": "Flash",
|
||||
"categoryId": 0,
|
||||
"url": "https://flashnews.com.au/",
|
||||
"companyId": "foxtel",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"flashtalking": {
|
||||
"name": "Flashtalking",
|
||||
"categoryId": 4,
|
||||
@@ -7369,6 +7383,13 @@
|
||||
"url": "https://publishers.foxaudiencenetwork.com/",
|
||||
"companyId": "fox_audience_network"
|
||||
},
|
||||
"fox_sports": {
|
||||
"name": "Fox Sports",
|
||||
"categoryId": 0,
|
||||
"url": "https://foxsports.com.au/",
|
||||
"companyId": "foxtel",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"foxnews_static": {
|
||||
"name": "Fox News CDN",
|
||||
"categoryId": 9,
|
||||
@@ -7381,6 +7402,13 @@
|
||||
"url": "https://www.foxpush.com/",
|
||||
"companyId": "foxpush"
|
||||
},
|
||||
"foxtel": {
|
||||
"name": "Foxtel",
|
||||
"categoryId": 0,
|
||||
"url": "https://foxtel.com.au/",
|
||||
"companyId": "foxtel",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"foxydeal_com": {
|
||||
"name": "foxydeal.com",
|
||||
"categoryId": 12,
|
||||
@@ -7983,12 +8011,40 @@
|
||||
"url": "http://www.google.com",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_auth": {
|
||||
"name": "Google Auth",
|
||||
"categoryId": 2,
|
||||
"url": "https://myaccount.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_beacons": {
|
||||
"name": "Google Beacons",
|
||||
"categoryId": 6,
|
||||
"url": "https://google.xyz",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_chat": {
|
||||
"name": "Google Chat",
|
||||
"categoryId": 7,
|
||||
"url": "https://mail.google.com/chat/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_cloud_platform": {
|
||||
"name": "Google Cloud Platform",
|
||||
"categoryId": 10,
|
||||
"url": "https://cloud.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_cloud_storage": {
|
||||
"name": "Google Cloud Storage",
|
||||
"categoryId": 10,
|
||||
"url": "https://cloud.google.com/storage/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_custom_search": {
|
||||
"name": "Google Custom Search Ads",
|
||||
"categoryId": 4,
|
||||
@@ -8001,6 +8057,27 @@
|
||||
"url": "https://programmablesearchengine.google.com/about/",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_dns": {
|
||||
"name": "Google DNS",
|
||||
"categoryId": 10,
|
||||
"url": "hhttps://dns.google/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_domains": {
|
||||
"name": "Google Domains",
|
||||
"categoryId": 10,
|
||||
"url": "https://domains.google/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_edge": {
|
||||
"name": "Google Edge CDN",
|
||||
"categoryId": 9,
|
||||
"url": "https://peering.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_email": {
|
||||
"name": "Google Email",
|
||||
"categoryId": 13,
|
||||
@@ -8013,12 +8090,47 @@
|
||||
"url": "https://fonts.google.com/",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_hosted": {
|
||||
"name": "Google Hosted",
|
||||
"categoryId": 10,
|
||||
"url": "https://workspace.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_ima": {
|
||||
"name": "Google IMA",
|
||||
"categoryId": 4,
|
||||
"url": "http://www.google.com",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_location": {
|
||||
"name": "Google Location",
|
||||
"categoryId": 8,
|
||||
"url": "https://patents.google.com/patent/WO2007025143A1/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_maps": {
|
||||
"name": "Google Maps",
|
||||
"categoryId": 2,
|
||||
"url": "https://www.google.com/maps/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_marketing": {
|
||||
"name": "Google Marketing",
|
||||
"categoryId": 6,
|
||||
"url": "https://marketingplatform.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_meet": {
|
||||
"name": "Google Meet",
|
||||
"categoryId": 2,
|
||||
"url": "https://meet.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_photos": {
|
||||
"name": "Google Photos",
|
||||
"categoryId": 9,
|
||||
@@ -8031,6 +8143,13 @@
|
||||
"url": "http://www.google.com",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_play": {
|
||||
"name": "Google Play",
|
||||
"categoryId": 8,
|
||||
"url": "https://play.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_plus": {
|
||||
"name": "Google+ Platform",
|
||||
"categoryId": 7,
|
||||
@@ -8110,6 +8229,13 @@
|
||||
"url": "http://www.google.com",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_voice": {
|
||||
"name": "Google Voice",
|
||||
"categoryId": 2,
|
||||
"url": "https://voice.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"google_website_optimizer": {
|
||||
"name": "Google Website Optimizer",
|
||||
"categoryId": 6,
|
||||
@@ -8122,6 +8248,13 @@
|
||||
"url": "http://www.google.com",
|
||||
"companyId": "google"
|
||||
},
|
||||
"google_workspace": {
|
||||
"name": "Google Workspace",
|
||||
"categoryId": 2,
|
||||
"url": "https://workspace.google.com/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"googleapis.com": {
|
||||
"name": "Google APIs",
|
||||
"categoryId": 9,
|
||||
@@ -9766,6 +9899,13 @@
|
||||
"url": "http://kavanga.ru/",
|
||||
"companyId": "kavanga"
|
||||
},
|
||||
"kayo_sports": {
|
||||
"name": "Kayo Sports",
|
||||
"categoryId": 0,
|
||||
"url": "https://kayosports.com.au/",
|
||||
"companyId": "foxtel",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"keen_io": {
|
||||
"name": "Keen IO",
|
||||
"categoryId": 6,
|
||||
@@ -13093,6 +13233,13 @@
|
||||
"url": "http://perfectmarket.com/",
|
||||
"companyId": "perfect_market"
|
||||
},
|
||||
"perfops": {
|
||||
"name": "PerfOps",
|
||||
"categoryId": 6,
|
||||
"url": "https://perfops.net/",
|
||||
"companyId": "perfops",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"perform_group": {
|
||||
"name": "Perform Group",
|
||||
"categoryId": 5,
|
||||
@@ -16484,6 +16631,13 @@
|
||||
"url": "http://www.streak.com/",
|
||||
"companyId": "streak"
|
||||
},
|
||||
"streamotion": {
|
||||
"name": "Streamotion",
|
||||
"categoryId": 0,
|
||||
"url": "https://streamotion.com.au/",
|
||||
"companyId": "foxtel",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"streamrail.com": {
|
||||
"name": "StreamRail",
|
||||
"categoryId": 4,
|
||||
@@ -20598,6 +20752,7 @@
|
||||
"bing.com": "bing_ads",
|
||||
"bing.net": "bing_ads",
|
||||
"virtualearth.net": "bing_maps",
|
||||
"binge.com.au": "binge",
|
||||
"view.binlayer.com": "binlayer",
|
||||
"widgets.binotel.com": "binotel",
|
||||
"esendra.fi": "bisnode",
|
||||
@@ -21212,6 +21367,7 @@
|
||||
"2mdn.net": "doubleclick",
|
||||
"doubleclick.net": "doubleclick",
|
||||
"invitemedia.com": "doubleclick",
|
||||
"doubleclick.com": "doubleclick",
|
||||
"doublepimp.com": "doublepimp",
|
||||
"doublepimpssl.com": "doublepimp",
|
||||
"redcourtside.com": "doublepimp",
|
||||
@@ -21435,12 +21591,28 @@
|
||||
"findizer.fr": "findizer.fr",
|
||||
"findologic.com": "findologic.com",
|
||||
"app-measurement.com": "firebase",
|
||||
"fcm.googleapis.com": "firebase",
|
||||
"firebaseappcheck.googleapis.com": "firebase",
|
||||
"firebaseapp.com": "firebase",
|
||||
"firebase.com": "firebase",
|
||||
"firebasedynamiclinks.googleapis.com": "firebase",
|
||||
"firebasedynamiclinks-ipv4.googleapis.com": "firebase",
|
||||
"firebasedynamiclinks-ipv6.googleapis.com": "firebase",
|
||||
"firebase.googleapis.com": "firebase",
|
||||
"firebase.google.com": "firebase",
|
||||
"firebaseinappmessaging.googleapis.com": "firebase",
|
||||
"firebaseinstallations.googleapis.com": "firebase",
|
||||
"firebaselogging.googleapis.com": "firebase",
|
||||
"firebaselogging-pa.googleapis.com": "firebase",
|
||||
"firebaseperusertopics-pa.googleapis.com": "firebase",
|
||||
"firebaseremoteconfig.googleapis.com": "firebase",
|
||||
"firebaseio.com": "firebaseio.com",
|
||||
"firstimpression.io": "first_impression",
|
||||
"fitanalytics.com": "fit_analytics",
|
||||
"fivetran.com": "fivetran",
|
||||
"flagads.net": "flag_ads",
|
||||
"flagcounter.com": "flag_counter",
|
||||
"flashnews.com.au": "flash",
|
||||
"flashtalking.com": "flashtalking",
|
||||
"flattr.com": "flattr_button",
|
||||
"flexlinks.com": "flexoffers",
|
||||
@@ -21486,9 +21658,11 @@
|
||||
"platform.foursquare.com": "foursquare_widget",
|
||||
"fout.jp": "fout.jp",
|
||||
"fimserve.com": "fox_audience_network",
|
||||
"foxsports.com.au": "fox_sports",
|
||||
"fncstatic.com": "foxnews_static",
|
||||
"cdn.foxpush.net": "foxpush",
|
||||
"foxpush.com": "foxpush",
|
||||
"foxtel.com.au": "foxtel",
|
||||
"foxydeal.com": "foxydeal_com",
|
||||
"yabidos.com": "fraudlogix",
|
||||
"besucherstatistiken.com": "free_counter",
|
||||
@@ -21649,15 +21823,287 @@
|
||||
"google.ru": "google",
|
||||
"google.se": "google",
|
||||
"google.tn": "google",
|
||||
"1e100.net": "google",
|
||||
"agnss.goog": "google",
|
||||
"channel.status.request.url": "google",
|
||||
"g.cn": "google",
|
||||
"g.co": "google",
|
||||
"google.ad": "google",
|
||||
"google.ae": "google",
|
||||
"google.al": "google",
|
||||
"google.am": "google",
|
||||
"googleapis.cn": "google",
|
||||
"google.as": "google",
|
||||
"google.az": "google",
|
||||
"google.ba": "google",
|
||||
"google.bf": "google",
|
||||
"google.bg": "google",
|
||||
"google.bi": "google",
|
||||
"google.bj": "google",
|
||||
"google.bs": "google",
|
||||
"google.bt": "google",
|
||||
"google.by": "google",
|
||||
"google.cat": "google",
|
||||
"google.cd": "google",
|
||||
"google.cf": "google",
|
||||
"google.cg": "google",
|
||||
"google.ci": "google",
|
||||
"google.cl": "google",
|
||||
"google.cm": "google",
|
||||
"google.cn": "google",
|
||||
"google.co.ao": "google",
|
||||
"google.co.bw": "google",
|
||||
"google.co.ck": "google",
|
||||
"google.co.cr": "google",
|
||||
"googlecode.com": "google",
|
||||
"google.co.il": "google",
|
||||
"google.co.ke": "google",
|
||||
"google.co.kr": "google",
|
||||
"google.co.ls": "google",
|
||||
"google.com.af": "google",
|
||||
"google.com.ag": "google",
|
||||
"google.com.ai": "google",
|
||||
"google.com.bd": "google",
|
||||
"google.com.bh": "google",
|
||||
"google.com.bn": "google",
|
||||
"google.com.bo": "google",
|
||||
"google.com.bz": "google",
|
||||
"google.com.co": "google",
|
||||
"google.com.cu": "google",
|
||||
"google.com.cy": "google",
|
||||
"google.com.ec": "google",
|
||||
"google.com.eg": "google",
|
||||
"google.com.et": "google",
|
||||
"google.com.fj": "google",
|
||||
"google.com.gh": "google",
|
||||
"google.com.gi": "google",
|
||||
"google.com.gt": "google",
|
||||
"google.com.hk": "google",
|
||||
"google.com.jm": "google",
|
||||
"google.com.kh": "google",
|
||||
"google.com.kw": "google",
|
||||
"google.com.lb": "google",
|
||||
"google.com.my": "google",
|
||||
"google.com.na": "google",
|
||||
"google.com.nf": "google",
|
||||
"google.com.ng": "google",
|
||||
"google.com.ni": "google",
|
||||
"google.com.np": "google",
|
||||
"google.com.om": "google",
|
||||
"google.com.pa": "google",
|
||||
"google.com.pe": "google",
|
||||
"google.com.pg": "google",
|
||||
"google.com.ph": "google",
|
||||
"google.com.pk": "google",
|
||||
"google.com.pr": "google",
|
||||
"google.com.py": "google",
|
||||
"google.com.qa": "google",
|
||||
"google.com.sa": "google",
|
||||
"google.com.sb": "google",
|
||||
"google.com.sg": "google",
|
||||
"google.com.sl": "google",
|
||||
"google.com.sv": "google",
|
||||
"google.com.tj": "google",
|
||||
"google.com.uy": "google",
|
||||
"google.com.vc": "google",
|
||||
"google.com.vn": "google",
|
||||
"google.co.mz": "google",
|
||||
"google.co.nz": "google",
|
||||
"google.co.tz": "google",
|
||||
"google.co.ug": "google",
|
||||
"google.co.uz": "google",
|
||||
"google.co.ve": "google",
|
||||
"google.co.vi": "google",
|
||||
"google.co.za": "google",
|
||||
"google.co.zm": "google",
|
||||
"google.co.zw": "google",
|
||||
"google.cv": "google",
|
||||
"google.dj": "google",
|
||||
"google.dm": "google",
|
||||
"googledownloads.cn": "google",
|
||||
"google.ee": "google",
|
||||
"google.fm": "google",
|
||||
"google.ga": "google",
|
||||
"google.ge": "google",
|
||||
"google.gg": "google",
|
||||
"google.gl": "google",
|
||||
"google.gm": "google",
|
||||
"google.gp": "google",
|
||||
"google.gy": "google",
|
||||
"google.hn": "google",
|
||||
"google.hr": "google",
|
||||
"google.ht": "google",
|
||||
"google.im": "google",
|
||||
"google.in": "google",
|
||||
"google.iq": "google",
|
||||
"google.is": "google",
|
||||
"google.je": "google",
|
||||
"google.jo": "google",
|
||||
"google.kg": "google",
|
||||
"google.ki": "google",
|
||||
"google.kz": "google",
|
||||
"google.la": "google",
|
||||
"google.li": "google",
|
||||
"google.lk": "google",
|
||||
"google.lt": "google",
|
||||
"google.lu": "google",
|
||||
"google.lv": "google",
|
||||
"google.md": "google",
|
||||
"google.me": "google",
|
||||
"google.mg": "google",
|
||||
"google.mk": "google",
|
||||
"google.ml": "google",
|
||||
"google.mn": "google",
|
||||
"google.ms": "google",
|
||||
"google.mu": "google",
|
||||
"google.mv": "google",
|
||||
"google.mw": "google",
|
||||
"google.ne": "google",
|
||||
"google.net": "google",
|
||||
"google.nr": "google",
|
||||
"google.nu": "google",
|
||||
"googleoptimize.com": "google",
|
||||
"google.org": "google",
|
||||
"google.pn": "google",
|
||||
"google.ps": "google",
|
||||
"google.rw": "google",
|
||||
"google.sc": "google",
|
||||
"google.sh": "google",
|
||||
"google.si": "google",
|
||||
"google.sk": "google",
|
||||
"google.sm": "google",
|
||||
"google.sn": "google",
|
||||
"google.so": "google",
|
||||
"google.sr": "google",
|
||||
"google.st": "google",
|
||||
"google.td": "google",
|
||||
"google.tg": "google",
|
||||
"google.tk": "google",
|
||||
"google.tl": "google",
|
||||
"google.tm": "google",
|
||||
"google.to": "google",
|
||||
"google.tt": "google",
|
||||
"google.us": "google",
|
||||
"google.vg": "google",
|
||||
"google.vu": "google",
|
||||
"googleweblight.in": "google",
|
||||
"google.ws": "google",
|
||||
"googlezip.net": "google",
|
||||
"gstatic.cn": "google",
|
||||
"news.google.com": "google",
|
||||
"oo.gl": "google",
|
||||
"withgoogle.com": "google",
|
||||
"googleadservices.com": "google_adservices",
|
||||
"google-analytics.com": "google_analytics",
|
||||
"ssl-google-analytics.l.google.com": "google_analytics",
|
||||
"www-googletagmanager.l.google.com": "google_analytics",
|
||||
"appspot.com": "google_appspot",
|
||||
"googlehosted.com": "google_appspot",
|
||||
"accounts.google.com": "google_auth",
|
||||
"myaccount.google.com": "google_auth",
|
||||
"oauth2.googleapis.com": "google_auth",
|
||||
"ogs.google.com": "google_auth",
|
||||
"securetoken.googleapis.com": "google_auth",
|
||||
"beacons-google.com": "google_beacons",
|
||||
"alt1-mtalk.google.com": "google_chat",
|
||||
"alt2-mtalk.google.com": "google_chat",
|
||||
"alt3-mtalk.google.com": "google_chat",
|
||||
"alt4-mtalk.google.com": "google_chat",
|
||||
"alt5-mtalk.google.com": "google_chat",
|
||||
"alt6-mtalk.google.com": "google_chat",
|
||||
"alt7-mtalk.google.com": "google_chat",
|
||||
"alt8-mtalk.google.com": "google_chat",
|
||||
"chat.google.com": "google_chat",
|
||||
"mobile-gtalk4.l.google.com": "google_chat",
|
||||
"mobile-gtalk.l.google.com": "google_chat",
|
||||
"mtalk4.google.com": "google_chat",
|
||||
"mtalk.google.com": "google_chat",
|
||||
"talk.google.com": "google_chat",
|
||||
"talk.l.google.com": "google_chat",
|
||||
"talkx.l.google.com": "google_chat",
|
||||
"cloud.google.com": "google_cloud_platform",
|
||||
"gcp.gvt2.com": "google_cloud_platform",
|
||||
"storage.googleapis.com": "google_cloud_storage",
|
||||
"adsensecustomsearchads.com": "google_custom_search",
|
||||
"dns.google": "google_dns",
|
||||
"dns.google.com": "google_dns",
|
||||
"google-public-dns-a.google.com": "google_dns",
|
||||
"google-public-dns-b.google.com": "google_dns",
|
||||
"domains.google": "google_domains",
|
||||
"googledomains.com": "google_domains",
|
||||
"nic.google": "google_domains",
|
||||
"registry.google": "google_domains",
|
||||
"edge.google.com": "google_edge",
|
||||
"mail-ads.google.com": "google_email",
|
||||
"fonts.googleapis.com": "google_fonts",
|
||||
"cloudfunctions.net": "google_hosted",
|
||||
"ghs46.googlehosted.com": "google_hosted",
|
||||
"ghs4.googlehosted.com": "google_hosted",
|
||||
"ghs6.googlehosted.com": "google_hosted",
|
||||
"ghs.googlehosted.com": "google_hosted",
|
||||
"googlehosted.l.googleusercontent.com": "google_hosted",
|
||||
"run.app": "google_hosted",
|
||||
"supl.google.com": "google_location",
|
||||
"earth.app.goo.gl": "google_maps",
|
||||
"geo0.ggpht.com": "google_maps",
|
||||
"geo1.ggpht.com": "google_maps",
|
||||
"geo2.ggpht.com": "google_maps",
|
||||
"geo3.ggpht.com": "google_maps",
|
||||
"kh.google.com": "google_maps",
|
||||
"maps.app.goo.gl": "google_maps",
|
||||
"maps.google.ca": "google_maps",
|
||||
"maps.google.ch": "google_maps",
|
||||
"maps.google.co.jp": "google_maps",
|
||||
"maps.google.com": "google_maps",
|
||||
"maps.google.com.mx": "google_maps",
|
||||
"maps.google.co.uk": "google_maps",
|
||||
"maps.google.es": "google_maps",
|
||||
"maps.google.se": "google_maps",
|
||||
"maps.gstatic.com": "google_maps",
|
||||
"adsense.google.com": "google_marketing",
|
||||
"adservice.google.ca": "google_marketing",
|
||||
"adservice.google.co.in": "google_marketing",
|
||||
"adservice.google.co.kr": "google_marketing",
|
||||
"adservice.google.com": "google_marketing",
|
||||
"adservice.google.com.ar": "google_marketing",
|
||||
"adservice.google.com.au": "google_marketing",
|
||||
"adservice.google.com.br": "google_marketing",
|
||||
"adservice.google.com.co": "google_marketing",
|
||||
"adservice.google.com.gt": "google_marketing",
|
||||
"adservice.google.com.mx": "google_marketing",
|
||||
"adservice.google.com.pe": "google_marketing",
|
||||
"adservice.google.com.ph": "google_marketing",
|
||||
"adservice.google.com.pk": "google_marketing",
|
||||
"adservice.google.com.tr": "google_marketing",
|
||||
"adservice.google.com.tw": "google_marketing",
|
||||
"adservice.google.com.vn": "google_marketing",
|
||||
"adservice.google.co.uk": "google_marketing",
|
||||
"adservice.google.co.za": "google_marketing",
|
||||
"adservice.google.de": "google_marketing",
|
||||
"adservice.google.dk": "google_marketing",
|
||||
"adservice.google.es": "google_marketing",
|
||||
"adservice.google.fr": "google_marketing",
|
||||
"adservice.google.nl": "google_marketing",
|
||||
"adservice.google.no": "google_marketing",
|
||||
"adservice.google.pl": "google_marketing",
|
||||
"adservice.google.ru": "google_marketing",
|
||||
"adservice.google.vg": "google_marketing",
|
||||
"dai.google.com": "google_marketing",
|
||||
"doubleclickbygoogle.com": "google_marketing",
|
||||
"googlesyndication-cn.com": "google_marketing",
|
||||
"duo.google.com": "google_meet",
|
||||
"hangouts.clients6.google.com": "google_meet",
|
||||
"hangouts.googleapis.com": "google_meet",
|
||||
"hangouts.google.com": "google_meet",
|
||||
"meet.google.com": "google_meet",
|
||||
"meetings.googleapis.com": "google_meet",
|
||||
"stun1.l.google.com": "google_meet",
|
||||
"stun.l.google.com": "google_meet",
|
||||
"ggpht.com": "google_photos",
|
||||
"play-fe.googleapis.com": "google_play",
|
||||
"play.googleapis.com": "google_play",
|
||||
"play.google.com": "google_play",
|
||||
"play-lh.googleusercontent.com": "google_play",
|
||||
"1e100cdn.net": "google_servers",
|
||||
"gvt1.com": "google_servers",
|
||||
"gvt2.com": "google_servers",
|
||||
@@ -21670,7 +22116,22 @@
|
||||
"pki.goog": "google_trust_services",
|
||||
"googlecommerce.com": "google_trusted_stores",
|
||||
"googleusercontent.com": "google_users",
|
||||
"telephony.goog": "google_voice",
|
||||
"voice.google.com": "google_voice",
|
||||
"gmodules.com": "google_widgets",
|
||||
"calendar.google.com": "google_workspace",
|
||||
"contacts.google.com": "google_workspace",
|
||||
"currents.google.com": "google_workspace",
|
||||
"docs.google.com": "google_workspace",
|
||||
"drive.google.com": "google_workspace",
|
||||
"forms.google.com": "google_workspace",
|
||||
"gsuite.google.com": "google_workspace",
|
||||
"jamboard.google.com": "google_workspace",
|
||||
"keep.google.com": "google_workspace",
|
||||
"plus.google.com": "google_workspace",
|
||||
"sheets.google.com": "google_workspace",
|
||||
"slides.google.com": "google_workspace",
|
||||
"spreadsheets.google.com": "google_workspace",
|
||||
"googleapis.com": "googleapis.com",
|
||||
"gooal.herokuapp.com": "goooal",
|
||||
"gooo.al": "goooal",
|
||||
@@ -22037,6 +22498,7 @@
|
||||
"cen.katchup.fr": "katchup",
|
||||
"kau.li": "kauli",
|
||||
"kavanga.ru": "kavanga",
|
||||
"kayosports.com.au": "kayo_sports",
|
||||
"dc8na2hxrj29i.cloudfront.net": "keen_io",
|
||||
"keen.io": "keen_io",
|
||||
"widget.kelkoo.com": "kelkoo",
|
||||
@@ -22847,6 +23309,7 @@
|
||||
"perfectaudience.com": "perfect_audience",
|
||||
"prfct.co": "perfect_audience",
|
||||
"perfectmarket.com": "perfect_market",
|
||||
"perfops.io": "perfops",
|
||||
"performgroup.com": "perform_group",
|
||||
"analytics.performable.com": "performable",
|
||||
"performancing.com": "performancing_metrics",
|
||||
@@ -23652,6 +24115,7 @@
|
||||
"bizsolutions.strands.com": "strands_recommender",
|
||||
"strava.com": "strava",
|
||||
"mailfoogae.appspot.com": "streak",
|
||||
"streamotion.com.au": "streamotion",
|
||||
"streamrail.com": "streamrail.com",
|
||||
"streamrail.net": "streamrail.com",
|
||||
"stridespark.com": "stride",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# A docker file for scripts/make/build-docker.sh.
|
||||
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.18
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
@@ -25,8 +25,6 @@ RUN apk --no-cache add ca-certificates libcap tzdata && \
|
||||
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
|
||||
chown -R nobody: /opt/adguardhome
|
||||
|
||||
RUN apk --no-cache add tini
|
||||
|
||||
ARG DIST_DIR
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
@@ -43,43 +41,24 @@ RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
|
||||
# 68 : UDP : DHCP (client)
|
||||
# 80 : TCP : HTTP (main)
|
||||
# 443 : TCP, UDP : HTTPS, DNS-over-HTTPS (incl. HTTP/3), DNSCrypt (main)
|
||||
# 784 : UDP : DNS-over-QUIC (experimental)
|
||||
# 784 : UDP : DNS-over-QUIC (deprecated; use 853)
|
||||
# 853 : TCP, UDP : DNS-over-TLS, DNS-over-QUIC
|
||||
# 3000 : TCP, UDP : HTTP(S) (alt, incl. HTTP/3)
|
||||
# 3001 : TCP, UDP : HTTP(S) (beta, incl. HTTP/3)
|
||||
# 5443 : TCP, UDP : DNSCrypt (alt)
|
||||
# 6060 : TCP : HTTP (pprof)
|
||||
# 8853 : UDP : DNS-over-QUIC (experimental)
|
||||
# 8853 : UDP : DNS-over-QUIC (deprecated; use 853)
|
||||
#
|
||||
# TODO(a.garipov): Remove the old, non-standard 784 and 8853 ports for
|
||||
# DNS-over-QUIC in a future release.
|
||||
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 443/udp 784/udp\
|
||||
853/tcp 853/udp 3000/tcp 3000/udp 5443/tcp\
|
||||
5443/udp 6060/tcp 8853/udp
|
||||
853/tcp 853/udp 3000/tcp 3000/udp 5443/tcp 5443/udp 6060/tcp\
|
||||
8853/udp
|
||||
|
||||
WORKDIR /opt/adguardhome/work
|
||||
|
||||
# Install helpers for healthcheck.
|
||||
COPY --chown=nobody:nogroup\
|
||||
./${DIST_DIR}/docker/scripts\
|
||||
/opt/adguardhome/scripts
|
||||
|
||||
HEALTHCHECK \
|
||||
--interval=30s \
|
||||
--timeout=10s \
|
||||
--retries=3 \
|
||||
CMD [ "/opt/adguardhome/scripts/healthcheck.sh" ]
|
||||
|
||||
# It seems that the healthckech script sometimes spawns zombie processes, so we
|
||||
# need a way to handle them, since AdGuard Home doesn't know how to keep track
|
||||
# of the processes delegated to it by the OS. Use tini as entry point because
|
||||
# it needs the PID=1 to be the default parent for orphaned processes.
|
||||
#
|
||||
# See https://github.com/adguardTeam/adGuardHome/issues/3290.
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
||||
|
||||
CMD [ \
|
||||
"/opt/adguardhome/AdGuardHome", \
|
||||
"--no-check-update", \
|
||||
"-c", "/opt/adguardhome/conf/AdGuardHome.yaml", \
|
||||
"-w", "/opt/adguardhome/work" \
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/^[^[:space:]]/ { is_dns = /^dns:/ }
|
||||
|
||||
/^[[:space:]]+bind_hosts:/ { if (is_dns) prev_line = FNR }
|
||||
|
||||
/^[[:space:]]+- .+/ {
|
||||
if (FNR - prev_line == 1) {
|
||||
addrs[$2] = true
|
||||
prev_line = FNR
|
||||
|
||||
if ($2 == "0.0.0.0" || $2 == "'::'") {
|
||||
# Drop all the other addresses.
|
||||
delete addrs
|
||||
addrs[""] = true
|
||||
prev_line = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/^[[:space:]]+port:/ { if (is_dns) port = $2 }
|
||||
|
||||
END {
|
||||
for (addr in addrs) {
|
||||
if (match(addr, ":")) {
|
||||
print "[" addr "]:" port
|
||||
} else {
|
||||
print addr ":" port
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# AdGuard Home Docker healthcheck script
|
||||
|
||||
# Exit the script if a pipeline fails (-e), prevent accidental filename
|
||||
# expansion (-f), and consider undefined variables as errors (-u).
|
||||
set -e -f -u
|
||||
|
||||
# Function error_exit is an echo wrapper that writes to stderr and stops the
|
||||
# script execution with code 1.
|
||||
error_exit() {
|
||||
echo "$1" 1>&2
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
agh_dir="/opt/adguardhome"
|
||||
readonly agh_dir
|
||||
|
||||
filename="${agh_dir}/conf/AdGuardHome.yaml"
|
||||
readonly filename
|
||||
|
||||
if ! [ -f "$filename" ]
|
||||
then
|
||||
wget "http://127.0.0.1:3000" -O /dev/null -q || exit 1
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
help_dir="${agh_dir}/scripts"
|
||||
readonly help_dir
|
||||
|
||||
# Parse web host
|
||||
|
||||
web_url="$( awk -f "${help_dir}/web-bind.awk" "$filename" )"
|
||||
readonly web_url
|
||||
|
||||
if [ "$web_url" = '' ]
|
||||
then
|
||||
error_exit "no web bindings could be retrieved from $filename"
|
||||
fi
|
||||
|
||||
# TODO(e.burkov): Deal with 0 port.
|
||||
case "$web_url"
|
||||
in
|
||||
(*':0')
|
||||
error_exit '0 in web port is not supported by healthcheck'
|
||||
;;
|
||||
(*)
|
||||
# Go on.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Parse DNS hosts
|
||||
|
||||
dns_hosts="$( awk -f "${help_dir}/dns-bind.awk" "$filename" )"
|
||||
readonly dns_hosts
|
||||
|
||||
if [ "$dns_hosts" = '' ]
|
||||
then
|
||||
error_exit "no DNS bindings could be retrieved from $filename"
|
||||
fi
|
||||
|
||||
first_dns="$( echo "$dns_hosts" | head -n 1 )"
|
||||
readonly first_dns
|
||||
|
||||
# TODO(e.burkov): Deal with 0 port.
|
||||
case "$first_dns"
|
||||
in
|
||||
(*':0')
|
||||
error_exit '0 in DNS port is not supported by healthcheck'
|
||||
;;
|
||||
(*)
|
||||
# Go on.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check
|
||||
|
||||
# Skip SSL certificate validation since there is no guarantee the container
|
||||
# trusts the one used. It should be safe to drop the SSL validation since the
|
||||
# current script intended to be used from inside the container and only checks
|
||||
# the endpoint availability, ignoring the content of the response.
|
||||
#
|
||||
# See https://github.com/AdguardTeam/AdGuardHome/issues/5642.
|
||||
wget --no-check-certificate "$web_url" -O /dev/null -q || exit 1
|
||||
|
||||
test_fqdn="healthcheck.adguardhome.test."
|
||||
readonly test_fqdn
|
||||
|
||||
# The awk script currently returns only port prefixed with colon in case of
|
||||
# unspecified address.
|
||||
case "$first_dns"
|
||||
in
|
||||
(':'*)
|
||||
nslookup -type=a "$test_fqdn" "127.0.0.1${first_dns}" > /dev/null ||\
|
||||
nslookup -type=a "$test_fqdn" "[::1]${first_dns}" > /dev/null ||\
|
||||
error_exit "nslookup failed for $host"
|
||||
;;
|
||||
(*)
|
||||
echo "$dns_hosts" | while read -r host
|
||||
do
|
||||
nslookup -type=a "$test_fqdn" "$host" > /dev/null ||\
|
||||
error_exit "nslookup failed for $host"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
@@ -1,13 +0,0 @@
|
||||
# Don't consider the HTTPS hostname since the enforced HTTPS redirection should
|
||||
# work if the SSL check skipped. See file docker/healthcheck.sh.
|
||||
/^bind_host:/ { host = $2 }
|
||||
|
||||
/^bind_port:/ { port = $2 }
|
||||
|
||||
END {
|
||||
if (match(host, ":")) {
|
||||
print "http://[" host "]:" port
|
||||
} else {
|
||||
print "http://" host ":" port
|
||||
}
|
||||
}
|
||||
@@ -56,15 +56,20 @@ func (rm *requestMatcher) MatchRequest(
|
||||
) (res *urlfilter.DNSResult, ok bool) {
|
||||
switch req.DNSType {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
|
||||
log.Debug("%s: handling the request for %s", hostsContainerPrefix, req.Hostname)
|
||||
log.Debug(
|
||||
"%s: handling %s request for %s",
|
||||
hostsContainerPrefix,
|
||||
dns.Type(req.DNSType),
|
||||
req.Hostname,
|
||||
)
|
||||
|
||||
rm.stateLock.RLock()
|
||||
defer rm.stateLock.RUnlock()
|
||||
|
||||
return rm.engine.MatchRequest(req)
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
rm.stateLock.RLock()
|
||||
defer rm.stateLock.RUnlock()
|
||||
|
||||
return rm.engine.MatchRequest(req)
|
||||
}
|
||||
|
||||
// Translate returns the source hosts-syntax rule for the generated dnsrewrite
|
||||
@@ -96,6 +101,8 @@ const hostsContainerPrefix = "hosts container"
|
||||
|
||||
// HostsContainer stores the relevant hosts database provided by the OS and
|
||||
// processes both A/AAAA and PTR DNS requests for those.
|
||||
//
|
||||
// TODO(e.burkov): Improve API and move to golibs.
|
||||
type HostsContainer struct {
|
||||
// requestMatcher matches the requests and translates the rules. It's
|
||||
// embedded to implement MatchRequest and Translate for *HostsContainer.
|
||||
|
||||
@@ -2,8 +2,8 @@ package aghtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
@@ -19,23 +19,23 @@ import (
|
||||
|
||||
// Package fs
|
||||
|
||||
// type check
|
||||
var _ fs.FS = &FS{}
|
||||
|
||||
// FS is a mock [fs.FS] implementation for tests.
|
||||
// FS is a fake [fs.FS] implementation for tests.
|
||||
type FS struct {
|
||||
OnOpen func(name string) (fs.File, error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ fs.FS = (*FS)(nil)
|
||||
|
||||
// Open implements the [fs.FS] interface for *FS.
|
||||
func (fsys *FS) Open(name string) (fs.File, error) {
|
||||
return fsys.OnOpen(name)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ fs.GlobFS = &GlobFS{}
|
||||
var _ fs.GlobFS = (*GlobFS)(nil)
|
||||
|
||||
// GlobFS is a mock [fs.GlobFS] implementation for tests.
|
||||
// GlobFS is a fake [fs.GlobFS] implementation for tests.
|
||||
type GlobFS struct {
|
||||
// FS is embedded here to avoid implementing all it's methods.
|
||||
FS
|
||||
@@ -48,9 +48,9 @@ func (fsys *GlobFS) Glob(pattern string) ([]string, error) {
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ fs.StatFS = &StatFS{}
|
||||
var _ fs.StatFS = (*StatFS)(nil)
|
||||
|
||||
// StatFS is a mock [fs.StatFS] implementation for tests.
|
||||
// StatFS is a fake [fs.StatFS] implementation for tests.
|
||||
type StatFS struct {
|
||||
// FS is embedded here to avoid implementing all it's methods.
|
||||
FS
|
||||
@@ -62,47 +62,34 @@ func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
|
||||
return fsys.OnStat(name)
|
||||
}
|
||||
|
||||
// Package net
|
||||
// Package io
|
||||
|
||||
// type check
|
||||
var _ net.Listener = (*Listener)(nil)
|
||||
|
||||
// Listener is a mock [net.Listener] implementation for tests.
|
||||
type Listener struct {
|
||||
OnAccept func() (conn net.Conn, err error)
|
||||
OnAddr func() (addr net.Addr)
|
||||
OnClose func() (err error)
|
||||
// Writer is a fake [io.Writer] implementation for tests.
|
||||
type Writer struct {
|
||||
OnWrite func(b []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// Accept implements the [net.Listener] interface for *Listener.
|
||||
func (l *Listener) Accept() (conn net.Conn, err error) {
|
||||
return l.OnAccept()
|
||||
}
|
||||
var _ io.Writer = (*Writer)(nil)
|
||||
|
||||
// Addr implements the [net.Listener] interface for *Listener.
|
||||
func (l *Listener) Addr() (addr net.Addr) {
|
||||
return l.OnAddr()
|
||||
}
|
||||
|
||||
// Close implements the [net.Listener] interface for *Listener.
|
||||
func (l *Listener) Close() (err error) {
|
||||
return l.OnClose()
|
||||
// Write implements the [io.Writer] interface for *Writer.
|
||||
func (w *Writer) Write(b []byte) (n int, err error) {
|
||||
return w.OnWrite(b)
|
||||
}
|
||||
|
||||
// Module adguard-home
|
||||
|
||||
// Package aghos
|
||||
|
||||
// type check
|
||||
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
||||
|
||||
// FSWatcher is a mock [aghos.FSWatcher] implementation for tests.
|
||||
// FSWatcher is a fake [aghos.FSWatcher] implementation for tests.
|
||||
type FSWatcher struct {
|
||||
OnEvents func() (e <-chan struct{})
|
||||
OnAdd func(name string) (err error)
|
||||
OnClose func() (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
||||
|
||||
// Events implements the [aghos.FSWatcher] interface for *FSWatcher.
|
||||
func (w *FSWatcher) Events() (e <-chan struct{}) {
|
||||
return w.OnEvents()
|
||||
@@ -120,16 +107,16 @@ func (w *FSWatcher) Close() (err error) {
|
||||
|
||||
// Package agh
|
||||
|
||||
// type check
|
||||
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||
|
||||
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
|
||||
// ServiceWithConfig is a fake [agh.ServiceWithConfig] implementation for tests.
|
||||
type ServiceWithConfig[ConfigType any] struct {
|
||||
OnStart func() (err error)
|
||||
OnShutdown func(ctx context.Context) (err error)
|
||||
OnConfig func() (c ConfigType)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||
|
||||
// Start implements the [agh.ServiceWithConfig] interface for
|
||||
// *ServiceWithConfig.
|
||||
func (s *ServiceWithConfig[_]) Start() (err error) {
|
||||
@@ -152,10 +139,7 @@ func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||
|
||||
// Package upstream
|
||||
|
||||
// type check
|
||||
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
||||
|
||||
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
|
||||
// UpstreamMock is a fake [upstream.Upstream] implementation for tests.
|
||||
//
|
||||
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
|
||||
// rename it to just Upstream.
|
||||
@@ -165,6 +149,9 @@ type UpstreamMock struct {
|
||||
OnClose func() (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
||||
|
||||
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||
func (u *UpstreamMock) Address() (addr string) {
|
||||
return u.OnAddress()
|
||||
|
||||
@@ -51,6 +51,9 @@ func migrateDB(conf *ServerConfig) (err error) {
|
||||
oldLeasesPath := filepath.Join(conf.WorkDir, dbFilename)
|
||||
dataDirPath := filepath.Join(conf.DataDir, dataFilename)
|
||||
|
||||
// #nosec G304 -- Trust this path, since it's taken from the old file name
|
||||
// relative to the working directory and should generally be considered
|
||||
// safe.
|
||||
file, err := os.Open(oldLeasesPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Nothing to migrate.
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
@@ -436,102 +435,6 @@ func (s *Server) initDefaultSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
|
||||
// depending on configuration.
|
||||
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
|
||||
if !http3 {
|
||||
return upstream.DefaultHTTPVersions
|
||||
}
|
||||
|
||||
return []upstream.HTTPVersion{
|
||||
upstream.HTTPVersion3,
|
||||
upstream.HTTPVersion2,
|
||||
upstream.HTTPVersion11,
|
||||
}
|
||||
}
|
||||
|
||||
// prepareUpstreamSettings - prepares upstream DNS server settings
|
||||
func (s *Server) prepareUpstreamSettings() error {
|
||||
// We're setting a customized set of RootCAs. The reason is that Go default
|
||||
// mechanism of loading TLS roots does not always work properly on some
|
||||
// routers so we're loading roots manually and pass it here.
|
||||
//
|
||||
// See [aghtls.SystemRootCAs].
|
||||
upstream.RootCAs = s.conf.TLSv12Roots
|
||||
upstream.CipherSuites = s.conf.TLSCiphers
|
||||
|
||||
// Load upstreams either from the file, or from the settings
|
||||
var upstreams []string
|
||||
if s.conf.UpstreamDNSFileName != "" {
|
||||
data, err := os.ReadFile(s.conf.UpstreamDNSFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading upstream from file: %w", err)
|
||||
}
|
||||
|
||||
upstreams = stringutil.SplitTrimmed(string(data), "\n")
|
||||
|
||||
log.Debug("dns: using %d upstream servers from file %s", len(upstreams), s.conf.UpstreamDNSFileName)
|
||||
} else {
|
||||
upstreams = s.conf.UpstreamDNS
|
||||
}
|
||||
|
||||
httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams)
|
||||
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
||||
upstreamConfig, err := proxy.ParseUpstreamsConfig(
|
||||
upstreams,
|
||||
&upstream.Options{
|
||||
Bootstrap: s.conf.BootstrapDNS,
|
||||
Timeout: s.conf.UpstreamTimeout,
|
||||
HTTPVersions: httpVersions,
|
||||
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing upstream config: %w", err)
|
||||
}
|
||||
|
||||
if len(upstreamConfig.Upstreams) == 0 {
|
||||
log.Info("warning: no default upstream servers specified, using %v", defaultDNS)
|
||||
var uc *proxy.UpstreamConfig
|
||||
uc, err = proxy.ParseUpstreamsConfig(
|
||||
defaultDNS,
|
||||
&upstream.Options{
|
||||
Bootstrap: s.conf.BootstrapDNS,
|
||||
Timeout: s.conf.UpstreamTimeout,
|
||||
HTTPVersions: httpVersions,
|
||||
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing default upstreams: %w", err)
|
||||
}
|
||||
|
||||
upstreamConfig.Upstreams = uc.Upstreams
|
||||
}
|
||||
|
||||
s.conf.UpstreamConfig = upstreamConfig
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setProxyUpstreamMode sets the upstream mode and related settings in conf
|
||||
// based on provided parameters.
|
||||
func setProxyUpstreamMode(
|
||||
conf *proxy.Config,
|
||||
allServers bool,
|
||||
fastestAddr bool,
|
||||
fastestTimeout time.Duration,
|
||||
) {
|
||||
if allServers {
|
||||
conf.UpstreamMode = proxy.UModeParallel
|
||||
} else if fastestAddr {
|
||||
conf.UpstreamMode = proxy.UModeFastestAddr
|
||||
conf.FastestPingTimeout = fastestTimeout
|
||||
} else {
|
||||
conf.UpstreamMode = proxy.UModeLoadBalance
|
||||
}
|
||||
}
|
||||
|
||||
// prepareIpsetListSettings reads and prepares the ipset configuration either
|
||||
// from a file or from the data in the configuration file.
|
||||
func (s *Server) prepareIpsetListSettings() (err error) {
|
||||
@@ -540,6 +443,7 @@ func (s *Server) prepareIpsetListSettings() (err error) {
|
||||
return s.ipset.init(s.conf.IpsetList)
|
||||
}
|
||||
|
||||
// #nosec G304 -- Trust the path explicitly given by the user.
|
||||
data, err := os.ReadFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
@@ -277,17 +278,6 @@ func (s *Server) Resolve(host string) ([]net.IPAddr, error) {
|
||||
return s.internalProxy.LookupIPAddr(host)
|
||||
}
|
||||
|
||||
// RDNSExchanger is a resolver for clients' addresses.
|
||||
type RDNSExchanger interface {
|
||||
// Exchange tries to resolve the ip in a suitable way, i.e. either as local
|
||||
// or as external.
|
||||
Exchange(ip net.IP) (host string, err error)
|
||||
|
||||
// ResolvesPrivatePTR returns true if the RDNSExchanger is able to
|
||||
// resolve PTR requests for locally-served addresses.
|
||||
ResolvesPrivatePTR() (ok bool)
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrRDNSNoData is returned by [RDNSExchanger.Exchange] when the answer
|
||||
// section of response is either NODATA or has no PTR records.
|
||||
@@ -299,10 +289,10 @@ const (
|
||||
)
|
||||
|
||||
// type check
|
||||
var _ RDNSExchanger = (*Server)(nil)
|
||||
var _ rdns.Exchanger = (*Server)(nil)
|
||||
|
||||
// Exchange implements the RDNSExchanger interface for *Server.
|
||||
func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
// Exchange implements the [rdns.Exchanger] interface for *Server.
|
||||
func (s *Server) Exchange(ip netip.Addr) (host string, err error) {
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
@@ -310,7 +300,7 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
arpa, err := netutil.IPToReversedAddr(ip)
|
||||
arpa, err := netutil.IPToReversedAddr(ip.AsSlice())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reversing ip: %w", err)
|
||||
}
|
||||
@@ -335,7 +325,7 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
}
|
||||
|
||||
var resolver *proxy.Proxy
|
||||
if s.privateNets.Contains(ip) {
|
||||
if s.isPrivateIP(ip) {
|
||||
if !s.conf.UsePrivateRDNS {
|
||||
return "", nil
|
||||
}
|
||||
@@ -350,8 +340,12 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hostFromPTR(ctx.Res)
|
||||
}
|
||||
|
||||
// hostFromPTR returns domain name from the PTR response or error.
|
||||
func hostFromPTR(resp *dns.Msg) (host string, err error) {
|
||||
// Distinguish between NODATA response and a failed request.
|
||||
resp := ctx.Res
|
||||
if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError {
|
||||
return "", fmt.Errorf(
|
||||
"received %s response: %w",
|
||||
@@ -370,12 +364,25 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
return "", ErrRDNSNoData
|
||||
}
|
||||
|
||||
// ResolvesPrivatePTR implements the RDNSExchanger interface for *Server.
|
||||
func (s *Server) ResolvesPrivatePTR() (ok bool) {
|
||||
// isPrivateIP returns true if the ip is private.
|
||||
func (s *Server) isPrivateIP(ip netip.Addr) (ok bool) {
|
||||
return s.privateNets.Contains(ip.AsSlice())
|
||||
}
|
||||
|
||||
// ShouldResolveClient returns false if ip is a loopback address, or ip is
|
||||
// private and resolving of private addresses is disabled.
|
||||
func (s *Server) ShouldResolveClient(ip netip.Addr) (ok bool) {
|
||||
if ip.IsLoopback() {
|
||||
return false
|
||||
}
|
||||
|
||||
isPrivate := s.isPrivateIP(ip)
|
||||
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
return s.conf.UsePrivateRDNS
|
||||
return s.conf.ResolveClients &&
|
||||
(s.conf.UsePrivateRDNS || !isPrivate)
|
||||
}
|
||||
|
||||
// Start starts the DNS server.
|
||||
@@ -466,19 +473,15 @@ func (s *Server) setupResolvers(localAddrs []string) (err error) {
|
||||
|
||||
log.Debug("dnsforward: upstreams to resolve ptr for local addresses: %v", localAddrs)
|
||||
|
||||
var upsConfig *proxy.UpstreamConfig
|
||||
upsConfig, err = proxy.ParseUpstreamsConfig(
|
||||
localAddrs,
|
||||
&upstream.Options{
|
||||
Bootstrap: bootstraps,
|
||||
Timeout: defaultLocalTimeout,
|
||||
// TODO(e.burkov): Should we verify server's certificates?
|
||||
upsConfig, err := s.prepareUpstreamConfig(localAddrs, nil, &upstream.Options{
|
||||
Bootstrap: bootstraps,
|
||||
Timeout: defaultLocalTimeout,
|
||||
// TODO(e.burkov): Should we verify server's certificates?
|
||||
|
||||
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||
},
|
||||
)
|
||||
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing upstreams: %w", err)
|
||||
return fmt.Errorf("parsing private upstreams: %w", err)
|
||||
}
|
||||
|
||||
s.localResolvers = &proxy.Proxy{
|
||||
@@ -510,7 +513,8 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
|
||||
|
||||
err = s.prepareUpstreamSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing upstream settings: %w", err)
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
var proxyConfig proxy.Config
|
||||
|
||||
@@ -1273,11 +1273,11 @@ func TestServer_Exchange(t *testing.T) {
|
||||
)
|
||||
|
||||
var (
|
||||
onesIP = net.IP{1, 1, 1, 1}
|
||||
localIP = net.IP{192, 168, 1, 1}
|
||||
onesIP = netip.MustParseAddr("1.1.1.1")
|
||||
localIP = netip.MustParseAddr("192.168.1.1")
|
||||
)
|
||||
|
||||
revExtIPv4, err := netutil.IPToReversedAddr(onesIP)
|
||||
revExtIPv4, err := netutil.IPToReversedAddr(onesIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
extUpstream := &aghtest.UpstreamMock{
|
||||
@@ -1290,7 +1290,7 @@ func TestServer_Exchange(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
revLocIPv4, err := netutil.IPToReversedAddr(localIP)
|
||||
revLocIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
locUpstream := &aghtest.UpstreamMock{
|
||||
@@ -1330,7 +1330,7 @@ func TestServer_Exchange(t *testing.T) {
|
||||
want string
|
||||
wantErr error
|
||||
locUpstream upstream.Upstream
|
||||
req net.IP
|
||||
req netip.Addr
|
||||
}{{
|
||||
name: "external_good",
|
||||
want: onesHost,
|
||||
@@ -1354,7 +1354,7 @@ func TestServer_Exchange(t *testing.T) {
|
||||
want: "",
|
||||
wantErr: ErrRDNSNoData,
|
||||
locUpstream: locUpstream,
|
||||
req: net.IP{192, 168, 1, 2},
|
||||
req: netip.MustParseAddr("192.168.1.2"),
|
||||
}, {
|
||||
name: "invalid_answer",
|
||||
want: "",
|
||||
@@ -1396,3 +1396,57 @@ func TestServer_Exchange(t *testing.T) {
|
||||
assert.Empty(t, host)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_ShouldResolveClient(t *testing.T) {
|
||||
srv := &Server{
|
||||
privateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
ip netip.Addr
|
||||
want require.BoolAssertionFunc
|
||||
name string
|
||||
resolve bool
|
||||
usePrivate bool
|
||||
}{{
|
||||
name: "default",
|
||||
ip: netip.MustParseAddr("1.1.1.1"),
|
||||
want: require.True,
|
||||
resolve: true,
|
||||
usePrivate: true,
|
||||
}, {
|
||||
name: "no_rdns",
|
||||
ip: netip.MustParseAddr("1.1.1.1"),
|
||||
want: require.False,
|
||||
resolve: false,
|
||||
usePrivate: true,
|
||||
}, {
|
||||
name: "loopback",
|
||||
ip: netip.MustParseAddr("127.0.0.1"),
|
||||
want: require.False,
|
||||
resolve: true,
|
||||
usePrivate: true,
|
||||
}, {
|
||||
name: "private_resolve",
|
||||
ip: netip.MustParseAddr("192.168.0.1"),
|
||||
want: require.True,
|
||||
resolve: true,
|
||||
usePrivate: true,
|
||||
}, {
|
||||
name: "private_no_resolve",
|
||||
ip: netip.MustParseAddr("192.168.0.1"),
|
||||
want: require.False,
|
||||
resolve: true,
|
||||
usePrivate: false,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv.conf.ResolveClients = tc.resolve
|
||||
srv.conf.UsePrivateRDNS = tc.usePrivate
|
||||
|
||||
ok := srv.ShouldResolveClient(tc.ip)
|
||||
tc.want(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
|
||||
||cname.specific^$dnstype=~CNAME
|
||||
||0.0.0.1^$dnstype=~A
|
||||
||::1^$dnstype=~AAAA
|
||||
0.0.0.0 duplicate.domain
|
||||
0.0.0.0 duplicate.domain
|
||||
`
|
||||
|
||||
forwardConf := ServerConfig{
|
||||
@@ -137,6 +139,17 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
|
||||
},
|
||||
A: netutil.IPv4Zero(),
|
||||
}},
|
||||
}, {
|
||||
req: createTestMessage("duplicate.domain."),
|
||||
name: "duplicate_domain",
|
||||
wantAns: []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "duplicate.domain.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: netutil.IPv4Zero(),
|
||||
}},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -633,61 +633,70 @@ func (err domainSpecificTestError) Error() (msg string) {
|
||||
return fmt.Sprintf("WARNING: %s", err.error)
|
||||
}
|
||||
|
||||
// checkDNS checks the upstream server defined by upstreamConfigStr using
|
||||
// healthCheck for actually exchange messages. It uses bootstrap to resolve the
|
||||
// upstream's address.
|
||||
func checkDNS(
|
||||
upstreamConfigStr string,
|
||||
bootstrap []string,
|
||||
bootstrapPrefIPv6 bool,
|
||||
timeout time.Duration,
|
||||
healthCheck healthCheckFunc,
|
||||
) (err error) {
|
||||
if IsCommentOrEmpty(upstreamConfigStr) {
|
||||
return nil
|
||||
// parseUpstreamLine parses line and creates the [upstream.Upstream] using opts
|
||||
// and information from [s.dnsFilter.EtcHosts]. It returns an error if the line
|
||||
// is not a valid upstream line, see [upstream.AddressToUpstream]. It's a
|
||||
// caller's responsibility to close u.
|
||||
func (s *Server) parseUpstreamLine(
|
||||
line string,
|
||||
opts *upstream.Options,
|
||||
) (u upstream.Upstream, specific bool, err error) {
|
||||
// Separate upstream from domains list.
|
||||
upstreamAddr, domains, err := separateUpstream(line)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("wrong upstream format: %w", err)
|
||||
}
|
||||
|
||||
// Separate upstream from domains list.
|
||||
upstreamAddr, domains, err := separateUpstream(upstreamConfigStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wrong upstream format: %w", err)
|
||||
}
|
||||
specific = len(domains) > 0
|
||||
|
||||
useDefault, err := validateUpstream(upstreamAddr, domains)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wrong upstream format: %w", err)
|
||||
return nil, specific, fmt.Errorf("wrong upstream format: %w", err)
|
||||
} else if useDefault {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(bootstrap) == 0 {
|
||||
bootstrap = defaultBootstrap
|
||||
return nil, specific, nil
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: checking if upstream %q works", upstreamAddr)
|
||||
|
||||
u, err := upstream.AddressToUpstream(upstreamAddr, &upstream.Options{
|
||||
Bootstrap: bootstrap,
|
||||
Timeout: timeout,
|
||||
PreferIPv6: bootstrapPrefIPv6,
|
||||
})
|
||||
opts = &upstream.Options{
|
||||
Bootstrap: opts.Bootstrap,
|
||||
Timeout: opts.Timeout,
|
||||
PreferIPv6: opts.PreferIPv6,
|
||||
}
|
||||
|
||||
if s.dnsFilter != nil && s.dnsFilter.EtcHosts != nil {
|
||||
resolved := s.resolveUpstreamHost(extractUpstreamHost(upstreamAddr))
|
||||
sortNetIPAddrs(resolved, opts.PreferIPv6)
|
||||
opts.ServerIPAddrs = resolved
|
||||
}
|
||||
u, err = upstream.AddressToUpstream(upstreamAddr, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to choose upstream for %q: %w", upstreamAddr, err)
|
||||
return nil, specific, fmt.Errorf("creating upstream for %q: %w", upstreamAddr, err)
|
||||
}
|
||||
|
||||
return u, specific, nil
|
||||
}
|
||||
|
||||
func (s *Server) checkDNS(line string, opts *upstream.Options, check healthCheckFunc) (err error) {
|
||||
if IsCommentOrEmpty(line) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var u upstream.Upstream
|
||||
var specific bool
|
||||
defer func() {
|
||||
if err != nil && specific {
|
||||
err = domainSpecificTestError{error: err}
|
||||
}
|
||||
}()
|
||||
|
||||
u, specific, err = s.parseUpstreamLine(line, opts)
|
||||
if err != nil || u == nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, u.Close()) }()
|
||||
|
||||
if err = healthCheck(u); err != nil {
|
||||
err = fmt.Errorf("upstream %q fails to exchange: %w", upstreamAddr, err)
|
||||
if domains != nil {
|
||||
return domainSpecificTestError{error: err}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: upstream %q is ok", upstreamAddr)
|
||||
|
||||
return nil
|
||||
return check(u)
|
||||
}
|
||||
|
||||
func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -699,47 +708,54 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]string{}
|
||||
bootstraps := req.BootstrapDNS
|
||||
bootstrapPrefIPv6 := s.conf.BootstrapPreferIPv6
|
||||
timeout := s.conf.UpstreamTimeout
|
||||
opts := &upstream.Options{
|
||||
Bootstrap: req.BootstrapDNS,
|
||||
Timeout: s.conf.UpstreamTimeout,
|
||||
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||
}
|
||||
if len(opts.Bootstrap) == 0 {
|
||||
opts.Bootstrap = defaultBootstrap
|
||||
}
|
||||
|
||||
type upsCheckResult = struct {
|
||||
res string
|
||||
err error
|
||||
host string
|
||||
}
|
||||
|
||||
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
|
||||
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
|
||||
|
||||
upsNum := len(req.Upstreams) + len(req.PrivateUpstreams)
|
||||
result := make(map[string]string, upsNum)
|
||||
resCh := make(chan upsCheckResult, upsNum)
|
||||
|
||||
checkUps := func(ups string, healthCheck healthCheckFunc) {
|
||||
res := upsCheckResult{
|
||||
host: ups,
|
||||
}
|
||||
defer func() { resCh <- res }()
|
||||
|
||||
checkErr := checkDNS(ups, bootstraps, bootstrapPrefIPv6, timeout, healthCheck)
|
||||
if checkErr != nil {
|
||||
res.res = checkErr.Error()
|
||||
} else {
|
||||
res.res = "OK"
|
||||
}
|
||||
}
|
||||
|
||||
for _, ups := range req.Upstreams {
|
||||
go checkUps(ups, checkDNSUpstreamExc)
|
||||
go func(ups string) {
|
||||
resCh <- upsCheckResult{
|
||||
host: ups,
|
||||
err: s.checkDNS(ups, opts, checkDNSUpstreamExc),
|
||||
}
|
||||
}(ups)
|
||||
}
|
||||
for _, ups := range req.PrivateUpstreams {
|
||||
go checkUps(ups, checkPrivateUpstreamExc)
|
||||
go func(ups string) {
|
||||
resCh <- upsCheckResult{
|
||||
host: ups,
|
||||
err: s.checkDNS(ups, opts, checkPrivateUpstreamExc),
|
||||
}
|
||||
}(ups)
|
||||
}
|
||||
|
||||
for i := 0; i < upsNum; i++ {
|
||||
pair := <-resCh
|
||||
// TODO(e.burkov): The upstreams used for both common and private
|
||||
// resolving should be reported separately.
|
||||
result[pair.host] = pair.res
|
||||
pair := <-resCh
|
||||
if pair.err != nil {
|
||||
result[pair.host] = pair.err.Error()
|
||||
} else {
|
||||
result[pair.host] = "OK"
|
||||
}
|
||||
}
|
||||
close(resCh)
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, result)
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
@@ -280,6 +282,10 @@ func TestIsCommentOrEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateUpstreams(t *testing.T) {
|
||||
const sdnsStamp = `sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_J` +
|
||||
`S3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczE` +
|
||||
`uYWRndWFyZC5jb20`
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErr string
|
||||
@@ -300,7 +306,7 @@ func TestValidateUpstreams(t *testing.T) {
|
||||
"[//]tls://1.1.1.1",
|
||||
"[/www.host.com/]#",
|
||||
"[/host.com/google.com/]8.8.8.8",
|
||||
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
"[/host/]" + sdnsStamp,
|
||||
},
|
||||
}, {
|
||||
name: "with_default",
|
||||
@@ -310,7 +316,7 @@ func TestValidateUpstreams(t *testing.T) {
|
||||
"[//]tls://1.1.1.1",
|
||||
"[/www.host.com/]#",
|
||||
"[/host.com/google.com/]8.8.8.8",
|
||||
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
"[/host/]" + sdnsStamp,
|
||||
"8.8.8.8",
|
||||
},
|
||||
}, {
|
||||
@@ -326,9 +332,10 @@ func TestValidateUpstreams(t *testing.T) {
|
||||
wantErr: `validating upstream "123.3.7m": not an ip:port`,
|
||||
set: []string{"123.3.7m"},
|
||||
}, {
|
||||
name: "invalid",
|
||||
wantErr: `bad upstream for domain "[/host.com]tls://dns.adguard.com": missing separator`,
|
||||
set: []string{"[/host.com]tls://dns.adguard.com"},
|
||||
name: "invalid",
|
||||
wantErr: `bad upstream for domain "[/host.com]tls://dns.adguard.com": ` +
|
||||
`missing separator`,
|
||||
set: []string{"[/host.com]tls://dns.adguard.com"},
|
||||
}, {
|
||||
name: "invalid",
|
||||
wantErr: `validating upstream "[host.ru]#": not an ip:port`,
|
||||
@@ -340,14 +347,14 @@ func TestValidateUpstreams(t *testing.T) {
|
||||
"1.1.1.1",
|
||||
"tls://1.1.1.1",
|
||||
"https://dns.adguard.com/dns-query",
|
||||
"sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
sdnsStamp,
|
||||
"udp://dns.google",
|
||||
"udp://8.8.8.8",
|
||||
"[/host.com/]1.1.1.1",
|
||||
"[//]tls://1.1.1.1",
|
||||
"[/www.host.com/]#",
|
||||
"[/host.com/google.com/]8.8.8.8",
|
||||
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
"[/host/]" + sdnsStamp,
|
||||
"[/пример.рф/]8.8.8.8",
|
||||
},
|
||||
}, {
|
||||
@@ -418,27 +425,28 @@ func TestValidateUpstreamsPrivate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newLocalUpstreamListener(t *testing.T, port int, handler dns.Handler) (real net.Addr) {
|
||||
func newLocalUpstreamListener(t *testing.T, port uint16, handler dns.Handler) (real netip.AddrPort) {
|
||||
t.Helper()
|
||||
|
||||
startCh := make(chan struct{})
|
||||
upsSrv := &dns.Server{
|
||||
Addr: netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(port)).String(),
|
||||
Addr: netip.AddrPortFrom(netutil.IPv4Localhost(), port).String(),
|
||||
Net: "tcp",
|
||||
Handler: handler,
|
||||
NotifyStartedFunc: func() { close(startCh) },
|
||||
}
|
||||
go func() {
|
||||
t := testutil.PanicT{}
|
||||
|
||||
err := upsSrv.ListenAndServe()
|
||||
require.NoError(t, err)
|
||||
require.NoError(testutil.PanicT{}, err)
|
||||
}()
|
||||
|
||||
<-startCh
|
||||
testutil.CleanupAndRequireSuccess(t, upsSrv.Shutdown)
|
||||
|
||||
return upsSrv.Listener.Addr()
|
||||
return testutil.RequireTypeAssert[*net.TCPAddr](t, upsSrv.Listener.Addr()).AddrPort()
|
||||
}
|
||||
|
||||
func TestServer_handleTestUpstreaDNS(t *testing.T) {
|
||||
func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
||||
goodHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
||||
err := w.WriteMsg(new(dns.Msg).SetReply(m))
|
||||
require.NoError(testutil.PanicT{}, err)
|
||||
@@ -457,9 +465,38 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
|
||||
Host: newLocalUpstreamListener(t, 0, badHandler).String(),
|
||||
}).String()
|
||||
|
||||
const upsTimeout = 100 * time.Millisecond
|
||||
const (
|
||||
upsTimeout = 100 * time.Millisecond
|
||||
|
||||
srv := createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
hostsFileName = "hosts"
|
||||
upstreamHost = "custom.localhost"
|
||||
)
|
||||
|
||||
hostsListener := newLocalUpstreamListener(t, 0, goodHandler)
|
||||
hostsUps := (&url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: netutil.JoinHostPort(upstreamHost, int(hostsListener.Port())),
|
||||
}).String()
|
||||
|
||||
hc, err := aghnet.NewHostsContainer(
|
||||
filtering.SysHostsListID,
|
||||
fstest.MapFS{
|
||||
hostsFileName: &fstest.MapFile{
|
||||
Data: []byte(hostsListener.Addr().String() + " " + upstreamHost),
|
||||
},
|
||||
},
|
||||
&aghtest.FSWatcher{
|
||||
OnEvents: func() (e <-chan struct{}) { return nil },
|
||||
OnAdd: func(_ string) (err error) { return nil },
|
||||
OnClose: func() (err error) { return nil },
|
||||
},
|
||||
hostsFileName,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
srv := createTestServer(t, &filtering.Config{
|
||||
EtcHosts: hc,
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
UpstreamTimeout: upsTimeout,
|
||||
@@ -486,8 +523,7 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
|
||||
"upstream_dns": []string{badUps},
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
badUps: `upstream "` + badUps + `" fails to exchange: ` +
|
||||
`couldn't communicate with upstream: exchanging with ` +
|
||||
badUps: `couldn't communicate with upstream: exchanging with ` +
|
||||
badUps + ` over tcp: dns: id mismatch`,
|
||||
},
|
||||
name: "broken",
|
||||
@@ -497,20 +533,40 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
goodUps: "OK",
|
||||
badUps: `upstream "` + badUps + `" fails to exchange: ` +
|
||||
`couldn't communicate with upstream: exchanging with ` +
|
||||
badUps: `couldn't communicate with upstream: exchanging with ` +
|
||||
badUps + ` over tcp: dns: id mismatch`,
|
||||
},
|
||||
name: "both",
|
||||
}, {
|
||||
body: map[string]any{
|
||||
"upstream_dns": []string{"[/domain.example/]" + badUps},
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
"[/domain.example/]" + badUps: `WARNING: couldn't communicate ` +
|
||||
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
|
||||
`dns: id mismatch`,
|
||||
},
|
||||
name: "domain_specific_error",
|
||||
}, {
|
||||
body: map[string]any{
|
||||
"upstream_dns": []string{hostsUps},
|
||||
},
|
||||
wantResp: map[string]any{
|
||||
hostsUps: "OK",
|
||||
},
|
||||
name: "etc_hosts",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reqBody, err := json.Marshal(tc.body)
|
||||
var reqBody []byte
|
||||
reqBody, err = json.Marshal(tc.body)
|
||||
require.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
|
||||
|
||||
var r *http.Request
|
||||
r, err = http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
|
||||
require.NoError(t, err)
|
||||
|
||||
srv.handleTestUpstreamDNS(w, r)
|
||||
@@ -538,11 +594,15 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
|
||||
req := map[string]any{
|
||||
"upstream_dns": []string{sleepyUps},
|
||||
}
|
||||
reqBody, err := json.Marshal(req)
|
||||
|
||||
var reqBody []byte
|
||||
reqBody, err = json.Marshal(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
|
||||
|
||||
var r *http.Request
|
||||
r, err = http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
|
||||
require.NoError(t, err)
|
||||
|
||||
srv.handleTestUpstreamDNS(w, r)
|
||||
|
||||
@@ -26,11 +26,25 @@ func (s *Server) makeResponse(req *dns.Msg) (resp *dns.Msg) {
|
||||
return resp
|
||||
}
|
||||
|
||||
// ipsFromRules extracts non-IP addresses from the filtering result rules.
|
||||
// containsIP returns true if the IP is already in the list.
|
||||
func containsIP(ips []net.IP, ip net.IP) bool {
|
||||
for _, a := range ips {
|
||||
if a.Equal(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ipsFromRules extracts unique non-IP addresses from the filtering result
|
||||
// rules.
|
||||
func ipsFromRules(resRules []*filtering.ResultRule) (ips []net.IP) {
|
||||
for _, r := range resRules {
|
||||
if r.IP != nil {
|
||||
ips = append(ips, r.IP)
|
||||
// len(resRules) and len(ips) are actually small enough for O(n^2) to do
|
||||
// not raise performance questions.
|
||||
if ip := r.IP; ip != nil && !containsIP(ips, ip) {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
311
internal/dnsforward/upstreams.go
Normal file
311
internal/dnsforward/upstreams.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// loadUpstreams parses upstream DNS servers from the configured file or from
|
||||
// the configuration itself.
|
||||
func (s *Server) loadUpstreams() (upstreams []string, err error) {
|
||||
if s.conf.UpstreamDNSFileName == "" {
|
||||
return stringutil.FilterOut(s.conf.UpstreamDNS, IsCommentOrEmpty), nil
|
||||
}
|
||||
|
||||
var data []byte
|
||||
data, err = os.ReadFile(s.conf.UpstreamDNSFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading upstream from file: %w", err)
|
||||
}
|
||||
|
||||
upstreams = stringutil.SplitTrimmed(string(data), "\n")
|
||||
|
||||
log.Debug("dnsforward: got %d upstreams in %q", len(upstreams), s.conf.UpstreamDNSFileName)
|
||||
|
||||
return stringutil.FilterOut(upstreams, IsCommentOrEmpty), nil
|
||||
}
|
||||
|
||||
// prepareUpstreamSettings sets upstream DNS server settings.
|
||||
func (s *Server) prepareUpstreamSettings() (err error) {
|
||||
// We're setting a customized set of RootCAs. The reason is that Go default
|
||||
// mechanism of loading TLS roots does not always work properly on some
|
||||
// routers so we're loading roots manually and pass it here.
|
||||
//
|
||||
// See [aghtls.SystemRootCAs].
|
||||
upstream.RootCAs = s.conf.TLSv12Roots
|
||||
upstream.CipherSuites = s.conf.TLSCiphers
|
||||
|
||||
// Load upstreams either from the file, or from the settings
|
||||
var upstreams []string
|
||||
upstreams, err = s.loadUpstreams()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading upstreams: %w", err)
|
||||
}
|
||||
|
||||
s.conf.UpstreamConfig, err = s.prepareUpstreamConfig(upstreams, defaultDNS, &upstream.Options{
|
||||
Bootstrap: s.conf.BootstrapDNS,
|
||||
Timeout: s.conf.UpstreamTimeout,
|
||||
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
|
||||
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing upstream config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareUpstreamConfig sets upstream configuration based on upstreams and
|
||||
// configuration of s.
|
||||
func (s *Server) prepareUpstreamConfig(
|
||||
upstreams []string,
|
||||
defaultUpstreams []string,
|
||||
opts *upstream.Options,
|
||||
) (uc *proxy.UpstreamConfig, err error) {
|
||||
uc, err = proxy.ParseUpstreamsConfig(upstreams, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing upstream config: %w", err)
|
||||
}
|
||||
|
||||
if len(uc.Upstreams) == 0 && defaultUpstreams != nil {
|
||||
log.Info("dnsforward: warning: no default upstreams specified, using %v", defaultUpstreams)
|
||||
var defaultUpstreamConfig *proxy.UpstreamConfig
|
||||
defaultUpstreamConfig, err = proxy.ParseUpstreamsConfig(defaultUpstreams, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing default upstreams: %w", err)
|
||||
}
|
||||
|
||||
uc.Upstreams = defaultUpstreamConfig.Upstreams
|
||||
}
|
||||
|
||||
if s.dnsFilter != nil && s.dnsFilter.EtcHosts != nil {
|
||||
err = s.replaceUpstreamsWithHosts(uc, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving upstreams with hosts: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
// replaceUpstreamsWithHosts replaces unique upstreams with their resolved
|
||||
// versions based on the system hosts file.
|
||||
//
|
||||
// TODO(e.burkov): This should be performed inside dnsproxy, which should
|
||||
// actually consider /etc/hosts. See TODO on [aghnet.HostsContainer].
|
||||
func (s *Server) replaceUpstreamsWithHosts(
|
||||
upsConf *proxy.UpstreamConfig,
|
||||
opts *upstream.Options,
|
||||
) (err error) {
|
||||
resolved := map[string]*upstream.Options{}
|
||||
|
||||
err = s.resolveUpstreamsWithHosts(resolved, upsConf.Upstreams, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving upstreams: %w", err)
|
||||
}
|
||||
|
||||
hosts := maps.Keys(upsConf.DomainReservedUpstreams)
|
||||
// TODO(e.burkov): Think of extracting sorted range into an util function.
|
||||
slices.Sort(hosts)
|
||||
for _, host := range hosts {
|
||||
err = s.resolveUpstreamsWithHosts(resolved, upsConf.DomainReservedUpstreams[host], opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving upstreams reserved for %s: %w", host, err)
|
||||
}
|
||||
}
|
||||
|
||||
hosts = maps.Keys(upsConf.SpecifiedDomainUpstreams)
|
||||
slices.Sort(hosts)
|
||||
for _, host := range hosts {
|
||||
err = s.resolveUpstreamsWithHosts(resolved, upsConf.SpecifiedDomainUpstreams[host], opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving upstreams specific for %s: %w", host, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveUpstreamsWithHosts resolves the IP addresses of each of the upstreams
|
||||
// and replaces those both in upstreams and resolved. Upstreams that failed to
|
||||
// resolve are placed to resolved as-is. This function only returns error of
|
||||
// upstreams closing.
|
||||
func (s *Server) resolveUpstreamsWithHosts(
|
||||
resolved map[string]*upstream.Options,
|
||||
upstreams []upstream.Upstream,
|
||||
opts *upstream.Options,
|
||||
) (err error) {
|
||||
for i := range upstreams {
|
||||
u := upstreams[i]
|
||||
addr := u.Address()
|
||||
host := extractUpstreamHost(addr)
|
||||
|
||||
withIPs, ok := resolved[host]
|
||||
if !ok {
|
||||
ips := s.resolveUpstreamHost(host)
|
||||
if len(ips) == 0 {
|
||||
resolved[host] = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
sortNetIPAddrs(ips, opts.PreferIPv6)
|
||||
|
||||
withIPs = opts.Clone()
|
||||
withIPs.ServerIPAddrs = ips
|
||||
resolved[host] = withIPs
|
||||
} else if withIPs == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = u.Close(); err != nil {
|
||||
return fmt.Errorf("closing upstream %s: %w", addr, err)
|
||||
}
|
||||
|
||||
upstreams[i], err = upstream.AddressToUpstream(addr, withIPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("replacing upstream %s with resolved %s: %w", addr, host, err)
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: using %s for %s", withIPs.ServerIPAddrs, upstreams[i].Address())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractUpstreamHost returns the hostname of addr without port with an
|
||||
// assumption that any address passed here has already been successfully parsed
|
||||
// by [upstream.AddressToUpstream]. This function eesentially mirrors the logic
|
||||
// of [upstream.AddressToUpstream], see TODO on [replaceUpstreamsWithHosts].
|
||||
func extractUpstreamHost(addr string) (host string) {
|
||||
var err error
|
||||
if strings.Contains(addr, "://") {
|
||||
var u *url.URL
|
||||
u, err = url.Parse(addr)
|
||||
if err != nil {
|
||||
log.Debug("dnsforward: parsing upstream %s: %s", addr, err)
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
return u.Hostname()
|
||||
}
|
||||
|
||||
// Probably, plain UDP upstream defined by address or address:port.
|
||||
host, err = netutil.SplitHost(addr)
|
||||
if err != nil {
|
||||
return addr
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// resolveUpstreamHost returns the version of ups with IP addresses from the
|
||||
// system hosts file placed into its options.
|
||||
func (s *Server) resolveUpstreamHost(host string) (addrs []net.IP) {
|
||||
req := &urlfilter.DNSRequest{
|
||||
Hostname: host,
|
||||
DNSType: dns.TypeA,
|
||||
}
|
||||
aRes, _ := s.dnsFilter.EtcHosts.MatchRequest(req)
|
||||
|
||||
req.DNSType = dns.TypeAAAA
|
||||
aaaaRes, _ := s.dnsFilter.EtcHosts.MatchRequest(req)
|
||||
|
||||
var ips []net.IP
|
||||
for _, rw := range append(aRes.DNSRewrites(), aaaaRes.DNSRewrites()...) {
|
||||
dr := rw.DNSRewrite
|
||||
if dr == nil || dr.Value == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if ip, ok := dr.Value.(net.IP); ok {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
// sortNetIPAddrs sorts addrs in accordance with the protocol preferences.
|
||||
// Invalid addresses are sorted near the end.
|
||||
//
|
||||
// TODO(e.burkov): This function taken from dnsproxy, which also already
|
||||
// contains a few similar functions. Think of moving to golibs.
|
||||
func sortNetIPAddrs(addrs []net.IP, preferIPv6 bool) {
|
||||
l := len(addrs)
|
||||
if l <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
slices.SortStableFunc(addrs, func(addrA, addrB net.IP) (sortsBefore bool) {
|
||||
switch len(addrA) {
|
||||
case net.IPv4len, net.IPv6len:
|
||||
switch len(addrB) {
|
||||
case net.IPv4len, net.IPv6len:
|
||||
// Go on.
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if aIs4, bIs4 := addrA.To4() != nil, addrB.To4() != nil; aIs4 != bIs4 {
|
||||
if aIs4 {
|
||||
return !preferIPv6
|
||||
}
|
||||
|
||||
return preferIPv6
|
||||
}
|
||||
|
||||
return bytes.Compare(addrA, addrB) < 0
|
||||
})
|
||||
}
|
||||
|
||||
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
|
||||
// depending on configuration.
|
||||
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
|
||||
if !http3 {
|
||||
return upstream.DefaultHTTPVersions
|
||||
}
|
||||
|
||||
return []upstream.HTTPVersion{
|
||||
upstream.HTTPVersion3,
|
||||
upstream.HTTPVersion2,
|
||||
upstream.HTTPVersion11,
|
||||
}
|
||||
}
|
||||
|
||||
// setProxyUpstreamMode sets the upstream mode and related settings in conf
|
||||
// based on provided parameters.
|
||||
func setProxyUpstreamMode(
|
||||
conf *proxy.Config,
|
||||
allServers bool,
|
||||
fastestAddr bool,
|
||||
fastestTimeout time.Duration,
|
||||
) {
|
||||
if allServers {
|
||||
conf.UpstreamMode = proxy.UModeParallel
|
||||
} else if fastestAddr {
|
||||
conf.UpstreamMode = proxy.UModeFastestAddr
|
||||
conf.FastestPingTimeout = fastestTimeout
|
||||
} else {
|
||||
conf.UpstreamMode = proxy.UModeLoadBalance
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package filtering
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -14,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
@@ -29,9 +27,9 @@ const filterDir = "filters"
|
||||
// TODO(e.burkov): Use more deterministic approach.
|
||||
var nextFilterID = time.Now().Unix()
|
||||
|
||||
// FilterYAML respresents a filter list in the configuration file.
|
||||
// FilterYAML represents a filter list in the configuration file.
|
||||
//
|
||||
// TODO(e.burkov): Investigate if the field oredering is important.
|
||||
// TODO(e.burkov): Investigate if the field ordering is important.
|
||||
type FilterYAML struct {
|
||||
Enabled bool
|
||||
URL string // URL or a file path
|
||||
@@ -213,7 +211,7 @@ func (d *DNSFilter) loadFilters(array []FilterYAML) {
|
||||
|
||||
err := d.load(filter)
|
||||
if err != nil {
|
||||
log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
|
||||
log.Error("filtering: loading filter %d: %s", filter.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,7 +336,8 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
|
||||
updateFlags = append(updateFlags, updated)
|
||||
if err != nil {
|
||||
nfail++
|
||||
log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
|
||||
log.Info("filtering: updating filter from url %q: %s\n", uf.URL, err)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -367,7 +366,13 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("Updated filter #%d. Rules: %d -> %d", f.ID, f.RulesCount, uf.RulesCount)
|
||||
log.Info(
|
||||
"filtering: updated filter %d; rule count: %d (was %d)",
|
||||
f.ID,
|
||||
uf.RulesCount,
|
||||
f.RulesCount,
|
||||
)
|
||||
|
||||
f.Name = uf.Name
|
||||
f.RulesCount = uf.RulesCount
|
||||
f.checksum = uf.checksum
|
||||
@@ -397,9 +402,10 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
|
||||
//
|
||||
// TODO(a.garipov, e.burkov): What the hell?
|
||||
func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
|
||||
log.Debug("filtering: updating...")
|
||||
|
||||
updNum := 0
|
||||
log.Debug("filtering: starting updating")
|
||||
defer func() { log.Debug("filtering: finished updating, %d updated", updNum) }()
|
||||
|
||||
var lists []FilterYAML
|
||||
var toUpd []bool
|
||||
isNetErr := false
|
||||
@@ -437,131 +443,9 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("filtering: update finished: %d lists updated", updNum)
|
||||
|
||||
return updNum, false
|
||||
}
|
||||
|
||||
// isPrintableText returns true if data is printable UTF-8 text with CR, LF, TAB
|
||||
// characters.
|
||||
//
|
||||
// TODO(e.burkov): Investigate the purpose of this and improve the
|
||||
// implementation. Perhaps, use something from the unicode package.
|
||||
func isPrintableText(data string) (ok bool) {
|
||||
for _, c := range []byte(data) {
|
||||
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
|
||||
continue
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// scanLinesWithBreak is essentially a [bufio.ScanLines] which keeps trailing
|
||||
// line breaks.
|
||||
func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
return i + 1, data[0 : i+1], nil
|
||||
}
|
||||
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
|
||||
// Request more data.
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// parseFilter copies filter's content from src to dst and returns the number of
|
||||
// rules, number of bytes written, checksum, and title of the parsed list. dst
|
||||
// must not be nil.
|
||||
func (d *DNSFilter) parseFilter(
|
||||
src io.Reader,
|
||||
dst io.Writer,
|
||||
) (rulesNum, written int, checksum uint32, title string, err error) {
|
||||
scanner := bufio.NewScanner(src)
|
||||
scanner.Split(scanLinesWithBreak)
|
||||
|
||||
titleFound := false
|
||||
for n := 0; scanner.Scan(); written += n {
|
||||
line := scanner.Text()
|
||||
var isRule bool
|
||||
var likelyTitle string
|
||||
isRule, likelyTitle, err = d.parseFilterLine(line, !titleFound, written == 0)
|
||||
if err != nil {
|
||||
return 0, written, 0, "", err
|
||||
}
|
||||
|
||||
if isRule {
|
||||
rulesNum++
|
||||
} else if likelyTitle != "" {
|
||||
title, titleFound = likelyTitle, true
|
||||
}
|
||||
|
||||
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
|
||||
|
||||
n, err = dst.Write([]byte(line))
|
||||
if err != nil {
|
||||
return 0, written, 0, "", fmt.Errorf("writing filter line: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return 0, written, 0, "", fmt.Errorf("scanning filter contents: %w", err)
|
||||
}
|
||||
|
||||
return rulesNum, written, checksum, title, nil
|
||||
}
|
||||
|
||||
// parseFilterLine returns true if the passed line is a rule. line is
|
||||
// considered a rule if it's not a comment and contains no title.
|
||||
func (d *DNSFilter) parseFilterLine(
|
||||
line string,
|
||||
lookForTitle bool,
|
||||
testHTML bool,
|
||||
) (isRule bool, title string, err error) {
|
||||
if !isPrintableText(line) {
|
||||
return false, "", errors.Error("filter contains non-printable characters")
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
if testHTML && isHTML(line) {
|
||||
return false, "", errors.Error("data is HTML, not plain text")
|
||||
}
|
||||
|
||||
if line[0] == '!' && lookForTitle {
|
||||
match := d.filterTitleRegexp.FindStringSubmatch(line)
|
||||
if len(match) > 1 {
|
||||
title = match[1]
|
||||
}
|
||||
|
||||
return false, title, nil
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
// isHTML returns true if the line contains HTML tags instead of plain text.
|
||||
// line shouldn have no leading space symbols.
|
||||
//
|
||||
// TODO(ameshkov): It actually gives too much false-positives. Perhaps, just
|
||||
// check if trimmed string begins with angle bracket.
|
||||
func isHTML(line string) (ok bool) {
|
||||
line = strings.ToLower(line)
|
||||
|
||||
return strings.HasPrefix(line, "<html") || strings.HasPrefix(line, "<!doctype")
|
||||
}
|
||||
|
||||
// update refreshes filter's content and a/mtimes of it's file.
|
||||
func (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {
|
||||
b, err = d.updateIntl(filter)
|
||||
@@ -573,7 +457,7 @@ func (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {
|
||||
filter.LastUpdated,
|
||||
)
|
||||
if chErr != nil {
|
||||
log.Error("os.Chtimes(): %v", chErr)
|
||||
log.Error("filtering: os.Chtimes(): %s", chErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,14 +466,12 @@ func (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {
|
||||
|
||||
// finalizeUpdate closes and gets rid of temporary file f with filter's content
|
||||
// according to updated. It also saves new values of flt's name, rules number
|
||||
// and checksum if sucсeeded.
|
||||
// and checksum if succeeded.
|
||||
func (d *DNSFilter) finalizeUpdate(
|
||||
file *os.File,
|
||||
flt *FilterYAML,
|
||||
updated bool,
|
||||
name string,
|
||||
rnum int,
|
||||
cs uint32,
|
||||
res *rulelist.ParseResult,
|
||||
) (err error) {
|
||||
tmpFileName := file.Name()
|
||||
|
||||
@@ -602,23 +484,24 @@ func (d *DNSFilter) finalizeUpdate(
|
||||
}
|
||||
|
||||
if !updated {
|
||||
log.Tracef("filter #%d from %s has no changes, skip", flt.ID, flt.URL)
|
||||
log.Debug("filtering: filter %d from url %q has no changes, skipping", flt.ID, flt.URL)
|
||||
|
||||
return os.Remove(tmpFileName)
|
||||
}
|
||||
|
||||
fltPath := flt.Path(d.DataDir)
|
||||
|
||||
log.Printf("saving contents of filter #%d into %s", flt.ID, fltPath)
|
||||
log.Info("filtering: saving contents of filter %d into %q", flt.ID, fltPath)
|
||||
|
||||
// Don't use renamio or maybe packages, since those will require loading the
|
||||
// whole filter content to the memory on Windows.
|
||||
// Don't use renameio or maybe packages, since those will require loading
|
||||
// the whole filter content to the memory on Windows.
|
||||
err = os.Rename(tmpFileName, fltPath)
|
||||
if err != nil {
|
||||
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
||||
}
|
||||
|
||||
flt.Name, flt.checksum, flt.RulesCount = aghalg.Coalesce(flt.Name, name), cs, rnum
|
||||
flt.Name = aghalg.Coalesce(flt.Name, res.Title)
|
||||
flt.checksum, flt.RulesCount = res.Checksum, res.RulesCount
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -626,11 +509,9 @@ func (d *DNSFilter) finalizeUpdate(
|
||||
// updateIntl updates the flt rewriting it's actual file. It returns true if
|
||||
// the actual update has been performed.
|
||||
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
log.Tracef("downloading update for filter %d from %s", flt.ID, flt.URL)
|
||||
log.Debug("filtering: downloading update for filter %d from %q", flt.ID, flt.URL)
|
||||
|
||||
var name string
|
||||
var rnum, n int
|
||||
var cs uint32
|
||||
var res *rulelist.ParseResult
|
||||
|
||||
var tmpFile *os.File
|
||||
tmpFile, err = os.CreateTemp(filepath.Join(d.DataDir, filterDir), "")
|
||||
@@ -638,9 +519,14 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
finErr := d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs)
|
||||
finErr := d.finalizeUpdate(tmpFile, flt, ok, res)
|
||||
if ok && finErr == nil {
|
||||
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
||||
log.Info(
|
||||
"filtering: updated filter %d: %d bytes, %d rules",
|
||||
flt.ID,
|
||||
res.BytesWritten,
|
||||
res.RulesCount,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -661,14 +547,14 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
var resp *http.Response
|
||||
resp, err = d.HTTPClient.Get(flt.URL)
|
||||
if err != nil {
|
||||
log.Printf("requesting filter from %s, skip: %s", flt.URL, err)
|
||||
log.Info("filtering: requesting filter from %q: %s, skipping", flt.URL, err)
|
||||
|
||||
return false, err
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
|
||||
log.Info("filtering got status code %d from %q, skipping", resp.StatusCode, flt.URL)
|
||||
|
||||
return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
@@ -685,16 +571,20 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
r = f
|
||||
}
|
||||
|
||||
rnum, n, cs, name, err = d.parseFilter(r, tmpFile)
|
||||
bufPtr := d.bufPool.Get().(*[]byte)
|
||||
defer d.bufPool.Put(bufPtr)
|
||||
|
||||
return cs != flt.checksum && err == nil, err
|
||||
p := rulelist.NewParser()
|
||||
res, err = p.Parse(tmpFile, r, *bufPtr)
|
||||
|
||||
return res.Checksum != flt.checksum && err == nil, err
|
||||
}
|
||||
|
||||
// loads filter contents from the file in dataDir
|
||||
func (d *DNSFilter) load(flt *FilterYAML) (err error) {
|
||||
fileName := flt.Path(d.DataDir)
|
||||
|
||||
log.Debug("filtering: loading filter %d from %s", flt.ID, fileName)
|
||||
log.Debug("filtering: loading filter %d from %q", flt.ID, fileName)
|
||||
|
||||
file, err := os.Open(fileName)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -710,14 +600,18 @@ func (d *DNSFilter) load(flt *FilterYAML) (err error) {
|
||||
return fmt.Errorf("getting filter file stat: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("filtering: file %s, id %d, length %d", fileName, flt.ID, st.Size())
|
||||
log.Debug("filtering: file %q, id %d, length %d", fileName, flt.ID, st.Size())
|
||||
|
||||
rulesCount, _, checksum, _, err := d.parseFilter(file, io.Discard)
|
||||
bufPtr := d.bufPool.Get().(*[]byte)
|
||||
defer d.bufPool.Put(bufPtr)
|
||||
|
||||
p := rulelist.NewParser()
|
||||
res, err := p.Parse(io.Discard, file, *bufPtr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing filter file: %w", err)
|
||||
}
|
||||
|
||||
flt.RulesCount, flt.checksum, flt.LastUpdated = rulesCount, checksum, st.ModTime()
|
||||
flt.RulesCount, flt.checksum, flt.LastUpdated = res.RulesCount, res.Checksum, st.ModTime()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -759,8 +653,9 @@ func (d *DNSFilter) enableFiltersLocked(async bool) {
|
||||
})
|
||||
}
|
||||
|
||||
if err := d.SetFilters(filters, allowFilters, async); err != nil {
|
||||
log.Debug("enabling filters: %s", err)
|
||||
err := d.setFilters(filters, allowFilters, async)
|
||||
if err != nil {
|
||||
log.Error("filtering: enabling filters: %s", err)
|
||||
}
|
||||
|
||||
d.SetEnabled(d.FilteringEnabled)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/mathutil"
|
||||
@@ -170,6 +170,15 @@ type Checker interface {
|
||||
|
||||
// DNSFilter matches hostnames and DNS requests against filtering rules.
|
||||
type DNSFilter struct {
|
||||
// bufPool is a pool of buffers used for filtering-rule list parsing.
|
||||
bufPool *sync.Pool
|
||||
|
||||
rulesStorage *filterlist.RuleStorage
|
||||
filteringEngine *urlfilter.DNSEngine
|
||||
|
||||
rulesStorageAllow *filterlist.RuleStorage
|
||||
filteringEngineAllow *urlfilter.DNSEngine
|
||||
|
||||
safeSearch SafeSearch
|
||||
|
||||
// safeBrowsingChecker is the safe browsing hash-prefix checker.
|
||||
@@ -178,12 +187,6 @@ type DNSFilter struct {
|
||||
// parentalControl is the parental control hash-prefix checker.
|
||||
parentalControlChecker Checker
|
||||
|
||||
rulesStorage *filterlist.RuleStorage
|
||||
filteringEngine *urlfilter.DNSEngine
|
||||
|
||||
rulesStorageAllow *filterlist.RuleStorage
|
||||
filteringEngineAllow *urlfilter.DNSEngine
|
||||
|
||||
engineLock sync.RWMutex
|
||||
|
||||
Config // for direct access by library users, even a = assignment
|
||||
@@ -196,12 +199,6 @@ type DNSFilter struct {
|
||||
|
||||
refreshLock *sync.Mutex
|
||||
|
||||
// filterTitleRegexp is the regular expression to retrieve a name of a
|
||||
// filter list.
|
||||
//
|
||||
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
|
||||
filterTitleRegexp *regexp.Regexp
|
||||
|
||||
hostCheckers []hostChecker
|
||||
}
|
||||
|
||||
@@ -339,12 +336,12 @@ func cloneRewrites(entries []*LegacyRewrite) (clone []*LegacyRewrite) {
|
||||
return clone
|
||||
}
|
||||
|
||||
// SetFilters sets new filters, synchronously or asynchronously. When filters
|
||||
// setFilters sets new filters, synchronously or asynchronously. When filters
|
||||
// are set asynchronously, the old filters continue working until the new
|
||||
// filters are ready.
|
||||
//
|
||||
// In this case the caller must ensure that the old filter files are intact.
|
||||
func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool) error {
|
||||
func (d *DNSFilter) setFilters(blockFilters, allowFilters []Filter, async bool) error {
|
||||
if async {
|
||||
params := filtersInitializerParams{
|
||||
allowFilters: allowFilters,
|
||||
@@ -370,14 +367,7 @@ func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := d.initFiltering(allowFilters, blockFilters)
|
||||
if err != nil {
|
||||
log.Error("filtering: can't initialize filtering subsystem: %s", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return d.initFiltering(allowFilters, blockFilters)
|
||||
}
|
||||
|
||||
// Starts initializing new filters by signal from channel
|
||||
@@ -386,7 +376,8 @@ func (d *DNSFilter) filtersInitializer() {
|
||||
params := <-d.filtersInitializerChan
|
||||
err := d.initFiltering(params.allowFilters, params.blockFilters)
|
||||
if err != nil {
|
||||
log.Error("Can't initialize filtering subsystem: %s", err)
|
||||
log.Error("filtering: initializing: %s", err)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -519,7 +510,7 @@ func (d *DNSFilter) matchSysHosts(
|
||||
dnsres, _ := d.EtcHosts.MatchRequest(&urlfilter.DNSRequest{
|
||||
Hostname: host,
|
||||
SortedClientTags: setts.ClientTags,
|
||||
// TODO(e.burkov): Wait for urlfilter update to pass net.IP.
|
||||
// TODO(e.burkov): Wait for urlfilter update to pass netip.Addr.
|
||||
ClientIP: setts.ClientIP.String(),
|
||||
ClientName: setts.ClientName,
|
||||
DNSType: qtype,
|
||||
@@ -718,7 +709,7 @@ func newRuleStorage(filters []Filter) (rs *filterlist.RuleStorage, err error) {
|
||||
}
|
||||
|
||||
// Initialize urlfilter objects.
|
||||
func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error {
|
||||
func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) (err error) {
|
||||
rulesStorage, err := newRuleStorage(blockFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -745,7 +736,8 @@ func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error {
|
||||
|
||||
// Make sure that the OS reclaims memory as soon as possible.
|
||||
debug.FreeOSMemory()
|
||||
log.Debug("initialized filtering engine")
|
||||
|
||||
log.Debug("filtering: initialized filtering engine")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -949,8 +941,14 @@ func InitModule() {
|
||||
// be non-nil.
|
||||
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
||||
d = &DNSFilter{
|
||||
bufPool: &sync.Pool{
|
||||
New: func() (buf any) {
|
||||
bufVal := make([]byte, rulelist.MaxRuleLen)
|
||||
|
||||
return &bufVal
|
||||
},
|
||||
},
|
||||
refreshLock: &sync.Mutex{},
|
||||
filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`),
|
||||
safeBrowsingChecker: c.SafeBrowsingChecker,
|
||||
parentalControlChecker: c.ParentalControlChecker,
|
||||
}
|
||||
@@ -1047,7 +1045,7 @@ func (d *DNSFilter) checkSafeBrowsing(
|
||||
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("safebrowsing lookup for %q", host)
|
||||
defer timer.LogElapsed("filtering: safebrowsing lookup for %q", host)
|
||||
}
|
||||
|
||||
res = Result{
|
||||
@@ -1079,7 +1077,7 @@ func (d *DNSFilter) checkParental(
|
||||
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("parental lookup for %q", host)
|
||||
defer timer.LogElapsed("filtering: parental lookup for %q", host)
|
||||
}
|
||||
|
||||
res = Result{
|
||||
|
||||
@@ -547,7 +547,7 @@ func TestWhitelist(t *testing.T) {
|
||||
}}
|
||||
d, setts := newForTest(t, nil, filters)
|
||||
|
||||
err := d.SetFilters(filters, whiteFilters, false)
|
||||
err := d.setFilters(filters, whiteFilters, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(d.Close)
|
||||
|
||||
@@ -25,7 +25,7 @@ func toCacheItem(data []byte) *cacheItem {
|
||||
t := time.Unix(int64(binary.BigEndian.Uint64(data)), 0)
|
||||
|
||||
data = data[expirySize:]
|
||||
hashes := make([]hostnameHash, len(data)/hashSize)
|
||||
hashes := make([]hostnameHash, 0, len(data)/hashSize)
|
||||
|
||||
for i := 0; i < len(data); i += hashSize {
|
||||
var hash hostnameHash
|
||||
@@ -41,12 +41,13 @@ func toCacheItem(data []byte) *cacheItem {
|
||||
|
||||
// fromCacheItem encodes cacheItem into data.
|
||||
func fromCacheItem(item *cacheItem) (data []byte) {
|
||||
data = make([]byte, len(item.hashes)*hashSize+expirySize)
|
||||
data = make([]byte, 0, len(item.hashes)*hashSize+expirySize)
|
||||
|
||||
expiry := item.expiry.Unix()
|
||||
binary.BigEndian.PutUint64(data[:expirySize], uint64(expiry))
|
||||
data = binary.BigEndian.AppendUint64(data, uint64(expiry))
|
||||
|
||||
for _, v := range item.hashes {
|
||||
// nolint:looppointer // The subsilce is used for a copy.
|
||||
// nolint:looppointer // The subslice of v is used for a copy.
|
||||
data = append(data, v[:]...)
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ func (c *Checker) findInCache(
|
||||
|
||||
i := 0
|
||||
for _, hash := range hashes {
|
||||
// nolint:looppointer // The subsilce is used for a safe cache lookup.
|
||||
// nolint:looppointer // The has subslice is used for a cache lookup.
|
||||
data := c.cache.Get(hash[:prefixLen])
|
||||
if data == nil {
|
||||
hashes[i] = hash
|
||||
@@ -97,34 +98,36 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
|
||||
|
||||
for _, hash := range respHashes {
|
||||
var pref prefix
|
||||
// nolint:looppointer // The subsilce is used for a copy.
|
||||
// nolint:looppointer // The hash subslice is used for a copy.
|
||||
copy(pref[:], hash[:])
|
||||
|
||||
hashToStore[pref] = append(hashToStore[pref], hash)
|
||||
}
|
||||
|
||||
for pref, hash := range hashToStore {
|
||||
// nolint:looppointer // The subsilce is used for a safe cache lookup.
|
||||
c.setCache(pref[:], hash)
|
||||
c.setCache(pref, hash)
|
||||
}
|
||||
|
||||
for _, hash := range hashesToRequest {
|
||||
// nolint:looppointer // The subsilce is used for a safe cache lookup.
|
||||
pref := hash[:prefixLen]
|
||||
val := c.cache.Get(pref)
|
||||
// nolint:looppointer // The hash subslice is used for a cache lookup.
|
||||
val := c.cache.Get(hash[:prefixLen])
|
||||
if val == nil {
|
||||
var pref prefix
|
||||
// nolint:looppointer // The hash subslice is used for a copy.
|
||||
copy(pref[:], hash[:])
|
||||
|
||||
c.setCache(pref, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setCache stores hash in cache.
|
||||
func (c *Checker) setCache(pref []byte, hashes []hostnameHash) {
|
||||
func (c *Checker) setCache(pref prefix, hashes []hostnameHash) {
|
||||
item := &cacheItem{
|
||||
expiry: time.Now().Add(c.cacheTime),
|
||||
hashes: hashes,
|
||||
}
|
||||
|
||||
c.cache.Set(pref, fromCacheItem(item))
|
||||
c.cache.Set(pref[:], fromCacheItem(item))
|
||||
log.Debug("%s: stored in cache: %v", c.svc, pref)
|
||||
}
|
||||
|
||||
44
internal/filtering/hashprefix/cache_internal_test.go
Normal file
44
internal/filtering/hashprefix/cache_internal_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package hashprefix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCacheItem(t *testing.T) {
|
||||
item := &cacheItem{
|
||||
expiry: time.Unix(0x01_23_45_67_89_AB_CD_EF, 0),
|
||||
hashes: []hostnameHash{{
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
}, {
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
}},
|
||||
}
|
||||
|
||||
wantData := []byte{
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
0x01, 0x03, 0x05, 0x07, 0x02, 0x04, 0x06, 0x08,
|
||||
}
|
||||
|
||||
gotData := fromCacheItem(item)
|
||||
assert.Equal(t, wantData, gotData)
|
||||
|
||||
newItem := toCacheItem(gotData)
|
||||
gotData = fromCacheItem(newItem)
|
||||
assert.Equal(t, wantData, gotData)
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func (c *Checker) getQuestion(hashes []hostnameHash) (q string) {
|
||||
b := &strings.Builder{}
|
||||
|
||||
for _, hash := range hashes {
|
||||
// nolint:looppointer // The subsilce is used for safe hex encoding.
|
||||
// nolint:looppointer // The hash subslice is used for hex encoding.
|
||||
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
r,
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
"Couldn't fetch filter from url %s: %s",
|
||||
"Couldn't fetch filter from URL %q: %s",
|
||||
filt.URL,
|
||||
err,
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@ func matchDomainWildcard(host, wildcard string) (ok bool) {
|
||||
return isWildcard(wildcard) && strings.HasSuffix(host, wildcard[1:])
|
||||
}
|
||||
|
||||
// legacyRewriteSortsBefore sorts rewirtes according to the following priority:
|
||||
// legacyRewriteSortsBefore sorts rewrites according to the following priority:
|
||||
//
|
||||
// 1. A and AAAA > CNAME;
|
||||
// 2. wildcard > exact;
|
||||
|
||||
9
internal/filtering/rulelist/error.go
Normal file
9
internal/filtering/rulelist/error.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package rulelist
|
||||
|
||||
import "github.com/AdguardTeam/golibs/errors"
|
||||
|
||||
// ErrHTML is returned by [Parser.Parse] if the data is likely to be HTML.
|
||||
//
|
||||
// TODO(a.garipov): This error is currently returned to the UI. Stop that and
|
||||
// make it all-lowercase.
|
||||
const ErrHTML errors.Error = "data is HTML, not plain text"
|
||||
184
internal/filtering/rulelist/parser.go
Normal file
184
internal/filtering/rulelist/parser.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package rulelist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"unicode"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// Parser is a filtering-rule parser that collects data, such as the checksum
|
||||
// and the title, as well as counts rules and removes comments.
|
||||
type Parser struct {
|
||||
title string
|
||||
rulesCount int
|
||||
written int
|
||||
checksum uint32
|
||||
titleFound bool
|
||||
}
|
||||
|
||||
// NewParser returns a new filtering-rule parser.
|
||||
func NewParser() (p *Parser) {
|
||||
return &Parser{}
|
||||
}
|
||||
|
||||
// ParseResult contains information about the results of parsing a
|
||||
// filtering-rule list by [Parser.Parse].
|
||||
type ParseResult struct {
|
||||
// Title is the title contained within the filtering-rule list, if any.
|
||||
Title string
|
||||
|
||||
// RulesCount is the number of rules in the list. It excludes empty lines
|
||||
// and comments.
|
||||
RulesCount int
|
||||
|
||||
// BytesWritten is the number of bytes written to dst.
|
||||
BytesWritten int
|
||||
|
||||
// Checksum is the CRC-32 checksum of the rules content. That is, excluding
|
||||
// empty lines and comments.
|
||||
Checksum uint32
|
||||
}
|
||||
|
||||
// Parse parses data from src into dst using buf during parsing. r is never
|
||||
// nil.
|
||||
func (p *Parser) Parse(dst io.Writer, src io.Reader, buf []byte) (r *ParseResult, err error) {
|
||||
s := bufio.NewScanner(src)
|
||||
s.Buffer(buf, MaxRuleLen)
|
||||
|
||||
lineIdx := 0
|
||||
for s.Scan() {
|
||||
var n int
|
||||
n, err = p.processLine(dst, s.Bytes(), lineIdx)
|
||||
p.written += n
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return p.result(), err
|
||||
}
|
||||
|
||||
lineIdx++
|
||||
}
|
||||
|
||||
r = p.result()
|
||||
err = s.Err()
|
||||
|
||||
return r, errors.Annotate(err, "scanning filter contents: %w")
|
||||
}
|
||||
|
||||
// result returns the current parsing result.
|
||||
func (p *Parser) result() (r *ParseResult) {
|
||||
return &ParseResult{
|
||||
Title: p.title,
|
||||
RulesCount: p.rulesCount,
|
||||
BytesWritten: p.written,
|
||||
Checksum: p.checksum,
|
||||
}
|
||||
}
|
||||
|
||||
// processLine processes a single line. It may write to dst, and if it does, n
|
||||
// is the number of bytes written.
|
||||
func (p *Parser) processLine(dst io.Writer, line []byte, lineIdx int) (n int, err error) {
|
||||
trimmed := bytes.TrimSpace(line)
|
||||
if p.written == 0 && isHTMLLine(trimmed) {
|
||||
return 0, ErrHTML
|
||||
}
|
||||
|
||||
badIdx, isRule := 0, false
|
||||
if p.titleFound {
|
||||
badIdx, isRule = parseLine(trimmed)
|
||||
} else {
|
||||
badIdx, isRule = p.parseLineTitle(trimmed)
|
||||
}
|
||||
if badIdx != -1 {
|
||||
return 0, fmt.Errorf(
|
||||
"line at index %d: character at index %d: non-printable character",
|
||||
lineIdx,
|
||||
badIdx+bytes.Index(line, trimmed),
|
||||
)
|
||||
}
|
||||
|
||||
if !isRule {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
p.rulesCount++
|
||||
p.checksum = crc32.Update(p.checksum, crc32.IEEETable, trimmed)
|
||||
|
||||
// Assume that there is generally enough space in the buffer to add a
|
||||
// newline.
|
||||
n, err = dst.Write(append(trimmed, '\n'))
|
||||
|
||||
return n, errors.Annotate(err, "writing rule line: %w")
|
||||
}
|
||||
|
||||
// isHTMLLine returns true if line is likely an HTML line. line is assumed to
|
||||
// be trimmed of whitespace characters.
|
||||
func isHTMLLine(line []byte) (isHTML bool) {
|
||||
return hasPrefixFold(line, []byte("<html")) || hasPrefixFold(line, []byte("<!doctype"))
|
||||
}
|
||||
|
||||
// hasPrefixFold is a simple, best-effort prefix matcher. It may return
|
||||
// incorrect results for some non-ASCII characters.
|
||||
func hasPrefixFold(b, prefix []byte) (ok bool) {
|
||||
l := len(prefix)
|
||||
|
||||
return len(b) >= l && bytes.EqualFold(b[:l], prefix)
|
||||
}
|
||||
|
||||
// parseLine returns true if the parsed line is a filtering rule. line is
|
||||
// assumed to be trimmed of whitespace characters. nonPrintIdx is the index of
|
||||
// the first non-printable character, if any; if there are none, nonPrintIdx is
|
||||
// -1.
|
||||
//
|
||||
// A line is considered a rule if it's not empty, not a comment, and contains
|
||||
// only printable characters.
|
||||
func parseLine(line []byte) (nonPrintIdx int, isRule bool) {
|
||||
if len(line) == 0 || line[0] == '#' || line[0] == '!' {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
nonPrintIdx = bytes.IndexFunc(line, isNotPrintable)
|
||||
|
||||
return nonPrintIdx, nonPrintIdx == -1
|
||||
}
|
||||
|
||||
// isNotPrintable returns true if r is not a printable character that can be
|
||||
// contained in a filtering rule.
|
||||
func isNotPrintable(r rune) (ok bool) {
|
||||
// Tab isn't included into Unicode's graphic symbols, so include it here
|
||||
// explicitly.
|
||||
return r != '\t' && !unicode.IsGraphic(r)
|
||||
}
|
||||
|
||||
// parseLineTitle is like [parseLine] but additionally looks for a title. line
|
||||
// is assumed to be trimmed of whitespace characters.
|
||||
func (p *Parser) parseLineTitle(line []byte) (nonPrintIdx int, isRule bool) {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
if line[0] != '!' {
|
||||
nonPrintIdx = bytes.IndexFunc(line, isNotPrintable)
|
||||
|
||||
return nonPrintIdx, nonPrintIdx == -1
|
||||
}
|
||||
|
||||
const titlePattern = "! Title: "
|
||||
if !bytes.HasPrefix(line, []byte(titlePattern)) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
title := bytes.TrimSpace(line[len(titlePattern):])
|
||||
if title != nil {
|
||||
// Note that title can be a non-nil empty slice. Consider that normal
|
||||
// and just stop looking for other titles.
|
||||
p.title = string(title)
|
||||
p.titleFound = true
|
||||
}
|
||||
|
||||
return -1, false
|
||||
}
|
||||
247
internal/filtering/rulelist/parser_test.go
Normal file
247
internal/filtering/rulelist/parser_test.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package rulelist_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParser_Parse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in string
|
||||
wantDst string
|
||||
wantErrMsg string
|
||||
wantTitle string
|
||||
wantRulesNum int
|
||||
wantWritten int
|
||||
}{{
|
||||
name: "empty",
|
||||
in: "",
|
||||
wantDst: "",
|
||||
wantErrMsg: "",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 0,
|
||||
wantWritten: 0,
|
||||
}, {
|
||||
name: "html",
|
||||
in: testRuleTextHTML,
|
||||
wantErrMsg: rulelist.ErrHTML.Error(),
|
||||
wantTitle: "",
|
||||
wantRulesNum: 0,
|
||||
wantWritten: 0,
|
||||
}, {
|
||||
name: "comments",
|
||||
in: "# Comment 1\n" +
|
||||
"! Comment 2\n",
|
||||
wantErrMsg: "",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 0,
|
||||
wantWritten: 0,
|
||||
}, {}, {
|
||||
name: "rule",
|
||||
in: testRuleTextBlocked,
|
||||
wantDst: testRuleTextBlocked,
|
||||
wantErrMsg: "",
|
||||
wantRulesNum: 1,
|
||||
wantTitle: "",
|
||||
wantWritten: len(testRuleTextBlocked),
|
||||
}, {
|
||||
name: "html_in_rule",
|
||||
in: testRuleTextBlocked + testRuleTextHTML,
|
||||
wantDst: testRuleTextBlocked + testRuleTextHTML,
|
||||
wantErrMsg: "",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 2,
|
||||
wantWritten: len(testRuleTextBlocked) + len(testRuleTextHTML),
|
||||
}, {
|
||||
name: "title",
|
||||
in: "! Title: Test Title \n" +
|
||||
"! Title: Bad, Ignored Title\n" +
|
||||
testRuleTextBlocked,
|
||||
wantDst: testRuleTextBlocked,
|
||||
wantErrMsg: "",
|
||||
wantTitle: "Test Title",
|
||||
wantRulesNum: 1,
|
||||
wantWritten: len(testRuleTextBlocked),
|
||||
}, {
|
||||
name: "bad_char",
|
||||
in: "! Title: Test Title \n" +
|
||||
testRuleTextBlocked +
|
||||
">>>\x7F<<<",
|
||||
wantDst: testRuleTextBlocked,
|
||||
wantErrMsg: "line at index 2: " +
|
||||
"character at index 3: " +
|
||||
"non-printable character",
|
||||
wantTitle: "Test Title",
|
||||
wantRulesNum: 1,
|
||||
wantWritten: len(testRuleTextBlocked),
|
||||
}, {
|
||||
name: "too_long",
|
||||
in: strings.Repeat("a", rulelist.MaxRuleLen+1),
|
||||
wantDst: "",
|
||||
wantErrMsg: "scanning filter contents: " + bufio.ErrTooLong.Error(),
|
||||
wantTitle: "",
|
||||
wantRulesNum: 0,
|
||||
wantWritten: 0,
|
||||
}, {
|
||||
name: "bad_tab_and_comment",
|
||||
in: testRuleTextBadTab,
|
||||
wantDst: testRuleTextBadTab,
|
||||
wantErrMsg: "",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 1,
|
||||
wantWritten: len(testRuleTextBadTab),
|
||||
}, {
|
||||
name: "etc_hosts_tab_and_comment",
|
||||
in: testRuleTextEtcHostsTab,
|
||||
wantDst: testRuleTextEtcHostsTab,
|
||||
wantErrMsg: "",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 1,
|
||||
wantWritten: len(testRuleTextEtcHostsTab),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dst := &bytes.Buffer{}
|
||||
buf := make([]byte, rulelist.MaxRuleLen)
|
||||
|
||||
p := rulelist.NewParser()
|
||||
r, err := p.Parse(dst, strings.NewReader(tc.in), buf)
|
||||
require.NotNil(t, r)
|
||||
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
assert.Equal(t, tc.wantDst, dst.String())
|
||||
assert.Equal(t, tc.wantTitle, r.Title)
|
||||
assert.Equal(t, tc.wantRulesNum, r.RulesCount)
|
||||
assert.Equal(t, tc.wantWritten, r.BytesWritten)
|
||||
|
||||
if tc.wantWritten > 0 {
|
||||
assert.NotZero(t, r.Checksum)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_Parse_writeError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dst := &aghtest.Writer{
|
||||
OnWrite: func(b []byte) (n int, err error) {
|
||||
return 1, errors.Error("test error")
|
||||
},
|
||||
}
|
||||
buf := make([]byte, rulelist.MaxRuleLen)
|
||||
|
||||
p := rulelist.NewParser()
|
||||
r, err := p.Parse(dst, strings.NewReader(testRuleTextBlocked), buf)
|
||||
require.NotNil(t, r)
|
||||
|
||||
testutil.AssertErrorMsg(t, "writing rule line: test error", err)
|
||||
assert.Equal(t, 1, r.BytesWritten)
|
||||
}
|
||||
|
||||
func TestParser_Parse_checksums(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
withoutComments = testRuleTextBlocked
|
||||
withComments = "! Some comment.\n" +
|
||||
" " + testRuleTextBlocked +
|
||||
"# Another comment.\n"
|
||||
)
|
||||
|
||||
buf := make([]byte, rulelist.MaxRuleLen)
|
||||
|
||||
p := rulelist.NewParser()
|
||||
r, err := p.Parse(&bytes.Buffer{}, strings.NewReader(withoutComments), buf)
|
||||
require.NotNil(t, r)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotWithoutComments := r.Checksum
|
||||
|
||||
p = rulelist.NewParser()
|
||||
|
||||
r, err = p.Parse(&bytes.Buffer{}, strings.NewReader(withComments), buf)
|
||||
require.NotNil(t, r)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotWithComments := r.Checksum
|
||||
assert.Equal(t, gotWithoutComments, gotWithComments)
|
||||
}
|
||||
|
||||
var (
|
||||
resSink *rulelist.ParseResult
|
||||
errSink error
|
||||
)
|
||||
|
||||
func BenchmarkParser_Parse(b *testing.B) {
|
||||
dst := &bytes.Buffer{}
|
||||
src := strings.NewReader(strings.Repeat(testRuleTextBlocked, 1000))
|
||||
buf := make([]byte, rulelist.MaxRuleLen)
|
||||
p := rulelist.NewParser()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resSink, errSink = p.Parse(dst, src, buf)
|
||||
dst.Reset()
|
||||
}
|
||||
|
||||
require.NoError(b, errSink)
|
||||
require.NotNil(b, resSink)
|
||||
}
|
||||
|
||||
func FuzzParser_Parse(f *testing.F) {
|
||||
const n = 64
|
||||
|
||||
testCases := []string{
|
||||
"",
|
||||
"# Comment",
|
||||
"! Comment",
|
||||
"! Title ",
|
||||
"! Title XXX",
|
||||
testRuleTextEtcHostsTab,
|
||||
testRuleTextHTML,
|
||||
testRuleTextBlocked,
|
||||
testRuleTextBadTab,
|
||||
"1.2.3.4",
|
||||
"1.2.3.4 etc-hosts.example",
|
||||
">>>\x00<<<",
|
||||
">>>\x7F<<<",
|
||||
strings.Repeat("a", n+1),
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
f.Add(tc)
|
||||
}
|
||||
|
||||
buf := make([]byte, n)
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
require.Eventually(t, func() (ok bool) {
|
||||
dst := &bytes.Buffer{}
|
||||
src := strings.NewReader(input)
|
||||
|
||||
p := rulelist.NewParser()
|
||||
r, _ := p.Parse(dst, src, buf)
|
||||
require.NotNil(t, r)
|
||||
|
||||
return true
|
||||
}, testTimeout, testTimeout/100)
|
||||
})
|
||||
}
|
||||
11
internal/filtering/rulelist/rulelist.go
Normal file
11
internal/filtering/rulelist/rulelist.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Package rulelist contains the implementation of the standard rule-list
|
||||
// filter that wraps an urlfilter filtering-engine.
|
||||
//
|
||||
// TODO(a.garipov): Expand.
|
||||
package rulelist
|
||||
|
||||
// MaxRuleLen is the maximum length of a line with a filtering rule, in bytes.
|
||||
//
|
||||
// TODO(a.garipov): Consider changing this to a rune length, like AdGuardDNS
|
||||
// does.
|
||||
const MaxRuleLen = 1024
|
||||
14
internal/filtering/rulelist/rulelist_test.go
Normal file
14
internal/filtering/rulelist/rulelist_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package rulelist_test
|
||||
|
||||
import "time"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// Common texts for tests.
|
||||
const (
|
||||
testRuleTextHTML = "<!DOCTYPE html>\n"
|
||||
testRuleTextBlocked = "||blocked.example^\n"
|
||||
testRuleTextBadTab = "||bad-tab-and-comment.example^\t# A comment.\n"
|
||||
testRuleTextEtcHostsTab = "0.0.0.0 tab..example^\t# A comment.\n"
|
||||
)
|
||||
@@ -27,6 +27,25 @@ var blockedServices = []blockedService{{
|
||||
"||9cache.com^",
|
||||
"||9gag.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "activision_blizzard",
|
||||
Name: "Activision Blizzard",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-237 0 1572 1572\"><path d=\"m549.1.2 548.4 1571.4H798l-74.2-200H374.5l-74.3 200H.7zM626 1085.1l-83-274.3-82.9 274.3z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||activision.com^",
|
||||
"||activisionblizzard.com^",
|
||||
"||demonware.net^",
|
||||
},
|
||||
}, {
|
||||
ID: "aliexpress",
|
||||
Name: "AliExpress",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M9 4C6.25 4 4 6.25 4 9v32c0 2.75 2.25 5 5 5h32c2.75 0 5-2.25 5-5V9c0-2.75-2.25-5-5-5H9zm0 2h32c1.668 0 3 1.332 3 3v3.38A3.973 3.973 0 0 0 41 11H9a3.973 3.973 0 0 0-3 1.38V9c0-1.668 1.332-3 3-3zm6 11a1 1 0 0 1 1 1c0 4.962 4.037 9 9 9s9-4.038 9-9a1 1 0 1 1 2 0c0 6.065-4.935 11-11 11s-11-4.935-11-11a1 1 0 0 1 1-1z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||ae-rus.net^",
|
||||
"||ae-rus.ru^",
|
||||
"||aliexpress.com^",
|
||||
"||aliexpress.ru^",
|
||||
},
|
||||
}, {
|
||||
ID: "amazon",
|
||||
Name: "Amazon",
|
||||
@@ -293,6 +312,21 @@ var blockedServices = []blockedService{{
|
||||
"||mincdn.com^",
|
||||
"||yo9.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "blizzard_entertainment",
|
||||
Name: "Blizzard Entertainment",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 -32 128 128\"><path fill-rule=\"evenodd\" d=\"M105 2h3v1h2l2 1 1 1h3l1 1h4l1 1 2 2v1l1 3v4l1 2v6l-1 2v2l-1 3v2l-1 2v14l-1 2v1l-1 3-1 1h-3l-1 1h-6a5 5 0 0 0 1-6l2-1h-1l-1-3v-3a350 350 0 0 1 0-8l-1-3v-1l-1-1V9h1V6l-1-1-4-3Zm9 13v10h1v25a8 8 0 0 0 2-4l1-1 1-3V30l1-1v-2l1-1v-5l-1-2-2-3-1-1h-3Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M101 24v1l2 1h1v2h1l1 2v5l1 2s0-1 0 0l1 7 1 2v7l-1 5h-2l-2-2-4-1 1-3 1-2a22 22 0 0 0-1-10l-1-4h-1l1-4-1-1-2-3v2l-1 1v3l1 6v4l1 1-1 3v4l1 1-1 2v4l-1-1a13 13 0 0 0-4-5l-2-2 2-5V27l-1-1v-4l-1-1v-5h-1a33 33 0 0 1 0-4l4-4h-2l-4-4h-1V3h10l2 1 2 1h1c2 0 2 1 3 2l2 3 1 3v1l-1 2v1a11 11 0 0 1-1 4l-4 3ZM96 9v13l1 1a3 3 0 0 0 1-1c1 0 2-1 2-3v-1l1-1v-3l-2-3-2-2h-1ZM26 3l1 1h1l2 3v5l1 1v2l-1 1v9l1 1 1 1-1 7v9l-1 1 1 1-1 1v8h3l1-1h7v-1h16v6l1 2h-6l-1-1h-2l-1-1H31a4 4 0 0 0-3-1l-1 1h-1l-1 1h-5l1-1a10 10 0 0 0 3-2v-9l1-1-1-1V35l1-1V21l-1-1v-4l1-1v-3l1-2-1-3h-1l-2-2-1-1 1-1h4Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M84 60v-3l-1-2v-4l-3-2v-1l1-2a11 11 0 0 0 2-6l-1-1-3-2h-2v3l1 1h1l-1 2h-4l-2 1-2 1-1-2v-1l1-1 1-1 1-2v-5l1-1v-6l1-1v-3l1-1v-3l1-2 1-1-1-1 1-1 1-3 1-1V7l1-1c1-1 0-4 2-3l1 3 1 1 1 2v1l1 5 1 3v2l1 1v2l1 1v8l1 3v9l-1 1-2 5v3l-1 2v4l-1 1h-1Zm-4-36-1 1v2l-1 2v4l4 1h2v-7l-1-3-2-1-1-1v2Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M77 4v1l-2 3v2l-1 2v1l-1 1-1 4v7h-1v2a5 5 0 0 1-1 2v2l-2 2v7l-1 2v2l-2 4v3-1h3v-1l3-1 1-1 3-2h3l1 1-1 1a3 3 0 0 0 0 1l1 1v5l-1 1h-7v-1h-2l-2 1h-4l-2 1-1-2v-2l1-1v-1l1-1-1-1 1-1v-2l1-2-1-2v-8l1-2 2-5-1-1 1-2v-1l1-1v-4l1-1 2-4v-2l1-1h-3V8h-1l-1 1-2 3-1 4h-1l-1-1v-2l1-1V4h16ZM32 4h9l1 2-3 2 1 2-1 1v13l1 2-1 2v6l-1 1v2l1 1v5l-1 1 1 2 1 1 2 1v2h-7l-2 1-1-1 3-2v-8a4 4 0 0 1 0-2l1-1v-3l-1-14v-2l1-1h-1V7l-2-1h-1l-1-1 1-1Zm12 0h14v15c-2 1-2 4-3 6v2c-1 0-3 1-2 4h-1l-2 3-1 2v3l-1 2-2 5h2l1-1h2l1-1c1-1 1-3 3-3l1-2 2-2h1l1 3h-1v1l-1 1v7h-8l-1 1-2-1h-3l-1-1 1-1v-3l-1-2 1-1-1-1 1-3v-2l1-2 1-3a7 7 0 0 1 2-4l1-4 2-2 2-3v-3h1l2-3V8l-3-1h-2l-1 1a3 3 0 0 0-2 3l-1 1v4l-1 1-1 1v-1l-1-1V4ZM17 22l1 1h1v3s0-1 0 0l2 1v5l1 2-1 8v3a6 6 0 0 1 0 2l-1 2-1 2-1 3-3 2-2 2-3 1-1-1-1 1H1l-1-1 2-1 1-4V26l1-1-1-3V11l1-1-1-1H2V8L1 7 0 6V5l1-1h15l1 1c2 0 3 1 3 2l1 3v6l-4 6Zm-6-11v9h1l1-2 2-1v-6h-1l-1-1h-2v1Zm0 19-1 1 1 2-1 3v9a2 2 0 0 0 0 1v6l-1 1 3-1 1-2h1v-4l1-4v-5l-1-1 1-3-1-1v-2s0 1 0 0v-1l-1-1a20 20 0 0 1-2-2v4Z\" clip-rule=\"evenodd\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||battle.net^",
|
||||
"||battlenet.com.cn^",
|
||||
"||blizzard.cn^",
|
||||
"||blizzardgames.cn^",
|
||||
"||blz-contentstack.com^",
|
||||
"||blzstatic.cn^",
|
||||
"||bnet.163.com^",
|
||||
"||bnet.cn^",
|
||||
"||lizzard.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "cloudflare",
|
||||
Name: "CloudFlare",
|
||||
@@ -329,6 +363,14 @@ var blockedServices = []blockedService{{
|
||||
"||warp.plus^",
|
||||
"||workers.dev^",
|
||||
},
|
||||
}, {
|
||||
ID: "clubhouse",
|
||||
Name: "Clubhouse",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M29.8 4a1 1 0 0 0-.92.7 1 1 0 0 0 .36 1.1 31.2 31.2 0 0 1 6 6.02 1 1 0 1 0 1.6-1.2 33.2 33.2 0 0 0-6.4-6.4A1 1 0 0 0 29.8 4Zm-7.16 1.06c-.46 0-.87.3-.99.74a1 1 0 0 0 .5 1.15 31.13 31.13 0 0 1 11.13 10.6 1 1 0 1 0 1.7-1.07A33.12 33.12 0 0 0 23.11 5.2a.96.96 0 0 0-.48-.14ZM14.5 7.01a3.42 3.42 0 0 0-3.27 2.28l-.26-.27A3.49 3.49 0 0 0 8.5 8.01c-.9 0-1.8.34-2.48 1.01a3.51 3.51 0 0 0-.57 4.17c-.52.15-1.01.42-1.43.84a3.52 3.52 0 0 0 0 4.94l.27.27c-.46.16-.9.41-1.27.79a3.52 3.52 0 0 0 0 4.94l.88.88 16.47 16.47a9.01 9.01 0 0 0 12.72 0l4.23-4.22a9.94 9.94 0 0 0 2.3-3.59l2.63-7.08a8.03 8.03 0 0 1 1.84-2.87l1.74-1.73 1-1a4.02 4.02 0 0 0 0-5.66 4.02 4.02 0 0 0-5.66 0l-1 1-.7.71-4.2 4.2a2.98 2.98 0 0 1-4.24 0L17.9 8.96l-.94-.94a3.49 3.49 0 0 0-2.47-1.01Zm0 1.98c.38 0 .76.15 1.06.45l.94.94 13.1 13.1a5.02 5.02 0 0 0 7.08 0l4.2-4.18.7-.71 1-1c.8-.8 2.05-.8 2.83 0 .8.79.8 2.04 0 2.83l-2.73 2.73a10.03 10.03 0 0 0-2.3 3.58l-2.63 7.08a8.02 8.02 0 0 1-1.84 2.87l-4.23 4.23a6.99 6.99 0 0 1-9.9 0L4.44 23.56a1.5 1.5 0 0 1 0-2.12c.59-.59 1.45-.55 2.08.08l.1.09 8.2 8.37a1 1 0 0 0 .97.29 1 1 0 0 0 .46-1.68l-9.52-9.73-.01-.01-1.28-1.29a1.5 1.5 0 0 1 0-2.12c.6-.6 1.47-.58 2.08.03l9.18 9.17a1 1 0 0 0 1.69-.43 1 1 0 0 0-.28-.98L9 14.13l-.06-.07-1.5-1.5c-.6-.6-.6-1.53 0-2.12a1.5 1.5 0 0 1 2.12 0L20.8 21.67a1 1 0 0 0 1.68-.44 1 1 0 0 0-.27-.97l-8.7-8.7-.06-.06a1.4 1.4 0 0 1-.01-2.06c.3-.3.68-.45 1.06-.45ZM4.23 32a1 1 0 0 0-.82 1.51c3 5.18 7.36 9.46 12.59 12.37a1 1 0 0 0 1.51-.89 1 1 0 0 0-.54-.86A31.16 31.16 0 0 1 5.15 32.5a1.01 1.01 0 0 0-.92-.51Z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||clubhouse.com^",
|
||||
"||clubhouseapi.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "crunchyroll",
|
||||
Name: "Crunchyroll",
|
||||
@@ -736,6 +778,18 @@ var blockedServices = []blockedService{{
|
||||
"||xxbay.com^",
|
||||
"||yibei.org^",
|
||||
},
|
||||
}, {
|
||||
ID: "electronic_arts",
|
||||
Name: "Electronic Arts",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 1000 1000\"><path d=\"M500 1000C224.3 1000 0 775.7 0 500S224.3 0 500 0s500 224.3 500 500-224.3 500-500 500zm84.63-693.4H302.05l-42.87 68.9h282.25zm57.75.66L469.63 582.33H278.02l44.2-68.96h114.85l43.87-68.93h-265.5l-43.86 68.93h62.9L147.2 651.05h364.2L645.9 438.9l49.05 74.46h-44.23l-41.88 68.96H739.8l45.48 68.72h83.54z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||ea.com^",
|
||||
"||eamobile.com^",
|
||||
"||easports.com^",
|
||||
"||nearpolar.com^",
|
||||
"||swtor.com^",
|
||||
"||tnt-ea.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "epic_games",
|
||||
Name: "Epic Games",
|
||||
@@ -1412,6 +1466,28 @@ var blockedServices = []blockedService{{
|
||||
"||lineshoppingseller.com^",
|
||||
"||linetv.tw^",
|
||||
},
|
||||
}, {
|
||||
ID: "linkedin",
|
||||
Name: "LinkedIn",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M41,4H9C6.24,4,4,6.24,4,9v32c0,2.76,2.24,5,5,5h32c2.76,0,5-2.24,5-5V9C46,6.24,43.76,4,41,4z M17,20v19h-6V20H17z M11,14.47c0-1.4,1.2-2.47,3-2.47s2.93,1.07,3,2.47c0,1.4-1.12,2.53-3,2.53C12.2,17,11,15.87,11,14.47z M39,39h-6c0,0,0-9.26,0-10 c0-2-1-4-3.5-4.04h-0.08C27,24.96,26,27.02,26,29c0,0.91,0,10,0,10h-6V20h6v2.56c0,0,1.93-2.56,5.81-2.56 c3.97,0,7.19,2.73,7.19,8.26V39z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||bizographics.com^",
|
||||
"||cs1404.wpc.epsiloncdn.net^",
|
||||
"||cs767.wpc.epsiloncdn.net^",
|
||||
"||l-0005.dc-msedge.net^",
|
||||
"||l-0005.l-dc-msedge.net^",
|
||||
"||l-0005.l-msedge.net^",
|
||||
"||l-0015.l-msedge.net^",
|
||||
"||licdn.cn^",
|
||||
"||licdn.com^",
|
||||
"||linkedin.at^",
|
||||
"||linkedin.be^",
|
||||
"||linkedin.cn^",
|
||||
"||linkedin.com^",
|
||||
"||linkedin.nl^",
|
||||
"||linkedin.qtlcdn.com^",
|
||||
"||lnkd.in^",
|
||||
},
|
||||
}, {
|
||||
ID: "mail_ru",
|
||||
Name: "Mail.ru",
|
||||
@@ -1429,6 +1505,7 @@ var blockedServices = []blockedService{{
|
||||
"||aus.social^",
|
||||
"||awscommunity.social^",
|
||||
"||climatejustice.social^",
|
||||
"||cupoftea.social^",
|
||||
"||cyberplace.social^",
|
||||
"||defcon.social^",
|
||||
"||det.social^",
|
||||
@@ -1470,12 +1547,12 @@ var blockedServices = []blockedService{{
|
||||
"||mastodon.social^",
|
||||
"||mastodon.uno^",
|
||||
"||mastodon.world^",
|
||||
"||mastodon.zaclys.com^",
|
||||
"||mastodonapp.uk^",
|
||||
"||mastodonners.nl^",
|
||||
"||mastodont.cat^",
|
||||
"||mastodontech.de^",
|
||||
"||mastodontti.fi^",
|
||||
"||mastouille.fr^",
|
||||
"||mathstodon.xyz^",
|
||||
"||metalhead.club^",
|
||||
"||mindly.social^",
|
||||
@@ -1485,13 +1562,13 @@ var blockedServices = []blockedService{{
|
||||
"||mstdn.plus^",
|
||||
"||mstdn.social^",
|
||||
"||muenchen.social^",
|
||||
"||muenster.im^",
|
||||
"||nerdculture.de^",
|
||||
"||noc.social^",
|
||||
"||norden.social^",
|
||||
"||nrw.social^",
|
||||
"||o3o.ca^",
|
||||
"||ohai.social^",
|
||||
"||pewtix.com^",
|
||||
"||piaille.fr^",
|
||||
"||pol.social^",
|
||||
"||ravenation.club^",
|
||||
@@ -1506,7 +1583,6 @@ var blockedServices = []blockedService{{
|
||||
"||social.linux.pizza^",
|
||||
"||social.politicaconciencia.org^",
|
||||
"||social.vivaldi.net^",
|
||||
"||sself.co^",
|
||||
"||stranger.social^",
|
||||
"||sueden.social^",
|
||||
"||tech.lgbt^",
|
||||
@@ -1566,6 +1642,44 @@ var blockedServices = []blockedService{{
|
||||
"||nflxso.net^",
|
||||
"||nflxvideo.net^",
|
||||
},
|
||||
}, {
|
||||
ID: "nintendo",
|
||||
Name: "Nintendo",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M6 7v36h12.6V21.75l13 20.78.27.47H44V7H31.4v1l.04 20.22L18.5 7.47 18.22 7Zm2 2h9.1l14.5 23.22 1.84 3v-3.5L33.4 9H42v32h-9L18.44 17.75l-1.85-2.94V41H8Z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||nintendo-europe.com^",
|
||||
"||nintendo.be^",
|
||||
"||nintendo.co.jp^",
|
||||
"||nintendo.co.uk^",
|
||||
"||nintendo.com.au^",
|
||||
"||nintendo.com^",
|
||||
"||nintendo.de^",
|
||||
"||nintendo.es^",
|
||||
"||nintendo.eu^",
|
||||
"||nintendo.fr^",
|
||||
"||nintendo.it^",
|
||||
"||nintendo.jp^",
|
||||
"||nintendo.net^",
|
||||
"||nintendo.nl^",
|
||||
"||nintendoswitch.cn^",
|
||||
"||nintendowifi.net^",
|
||||
},
|
||||
}, {
|
||||
ID: "nvidia",
|
||||
Name: "Nvidia",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 48 48\"><path d=\"M20 8a2 2 0 0 0-2 2v2.55l.84-.05c10.76-.37 17.78 8.82 17.78 8.82s-8.05 9.8-16.44 9.8c-.73 0-1.47-.07-2.18-.19v-2.2c.73.23 1.52.35 2.3.35 5.88 0 11.35-7.6 11.35-7.6s-5.07-6.91-12.81-6.66l-.82.03v-2.3c-9.49.77-17.68 8.8-17.68 8.8S4.97 34.76 18 35.98v-2.44c.59.07 1.22.12 1.81.12 7.82 0 13.47-3.99 18.94-8.7.91.73 4.62 2.49 5.4 3.26-5.2 4.36-17.33 7.86-24.2 7.86-.66 0-1.32-.03-1.95-.1V38c0 1.1.9 2 2 2h25a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2H20zm-2 6.86v2.82a11.8 11.8 0 0 1 1.57-.07c4.95 0 7.9 3.85 7.9 3.85l-4.03 3.39c-1.8-3.02-2.43-4.35-5.44-4.7v8.57c-4.06-1.38-5.4-6.14-5.4-6.14s2.37-2.83 5.38-2.46H18v-2.44a15.66 15.66 0 0 0-9.22 4.46s2 7.52 9.22 8.8v2.6c-9.56-1.17-12.82-11.7-12.82-11.7s4.27-6.3 12.82-6.97z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||geforce.com^",
|
||||
"||geforcenow.com^",
|
||||
"||nvidia.cn^",
|
||||
"||nvidia.com.global.ogslb.com^",
|
||||
"||nvidia.com^",
|
||||
"||nvidia.eu^",
|
||||
"||nvidia.partners^",
|
||||
"||nvidiagrid.net^",
|
||||
"||nvidianews.com^",
|
||||
"||tegrazone.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "ok",
|
||||
Name: "OK.ru",
|
||||
@@ -1964,6 +2078,16 @@ var blockedServices = []blockedService{{
|
||||
"||twvid.com^",
|
||||
"||vine.co^",
|
||||
},
|
||||
}, {
|
||||
ID: "ubisoft",
|
||||
Name: "Ubisoft",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 32 32\"><path d=\"M15.22 3C7.14 3 3.66 10.18 3.66 10.18l1.03.74s-1.3 2.45-1.26 5.6A12.5 12.5 0 0 0 16.08 29a12.5 12.5 0 0 0 12.49-12.46c0-9-6.98-13.54-13.35-13.54zm.07 2.2c6.3 0 11.2 5.07 11.2 10.98 0 6.27-4.71 10.62-10.2 10.62-4.04 0-7.69-3.08-7.69-7.3a5.8 5.8 0 0 1 2.75-5.03l.21.23a6.37 6.37 0 0 0-1.53 3.91c0 3.32 2.6 5.62 5.88 5.62 4.18 0 6.97-3.56 6.97-7.7 0-4.81-4.25-8.9-9.36-8.9a11.1 11.1 0 0 0-6.61 2.3l-.21-.2a10.07 10.07 0 0 1 8.59-4.54zM13.4 9.8c3.26 0 6.44 2.15 7.24 5.22l-.3.1a8.35 8.35 0 0 0-6.52-3.44c-5.08 0-7.75 4.62-7.36 8.47l-.3.12s-.56-1.24-.56-2.71a7.8 7.8 0 0 1 7.8-7.76zm2.15 5.33a2.77 2.77 0 0 1 2.78 2.74c0 1.23-.79 1.96-.79 1.96l.94.65s-.93 1.46-2.82 1.46a3.4 3.4 0 0 1-.1-6.8z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||ubi.com^",
|
||||
"||ubisoft.com^",
|
||||
"||ubisoft.org^",
|
||||
"||ubisoftconnect.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "valorant",
|
||||
Name: "Valorant",
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
)
|
||||
|
||||
@@ -118,13 +119,18 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C
|
||||
}
|
||||
}
|
||||
|
||||
weekly := schedule.EmptyWeekly()
|
||||
if prev != nil {
|
||||
weekly = prev.BlockedServices.Schedule.Clone()
|
||||
}
|
||||
|
||||
c = &Client{
|
||||
safeSearchConf: safeSearchConf,
|
||||
|
||||
Name: cj.Name,
|
||||
|
||||
BlockedServices: &filtering.BlockedServices{
|
||||
Schedule: prev.BlockedServices.Schedule.Clone(),
|
||||
Schedule: weekly,
|
||||
IDs: cj.BlockedServices,
|
||||
},
|
||||
|
||||
|
||||
@@ -30,32 +30,30 @@ import (
|
||||
const dataDir = "data"
|
||||
|
||||
// logSettings are the logging settings part of the configuration file.
|
||||
//
|
||||
// TODO(a.garipov): Put them into a separate object.
|
||||
type logSettings struct {
|
||||
// File is the path to the log file. If empty, logs are written to stdout.
|
||||
// If "syslog", logs are written to syslog.
|
||||
File string `yaml:"log_file"`
|
||||
File string `yaml:"file"`
|
||||
|
||||
// MaxBackups is the maximum number of old log files to retain.
|
||||
//
|
||||
// NOTE: MaxAge may still cause them to get deleted.
|
||||
MaxBackups int `yaml:"log_max_backups"`
|
||||
MaxBackups int `yaml:"max_backups"`
|
||||
|
||||
// MaxSize is the maximum size of the log file before it gets rotated, in
|
||||
// megabytes. The default value is 100 MB.
|
||||
MaxSize int `yaml:"log_max_size"`
|
||||
MaxSize int `yaml:"max_size"`
|
||||
|
||||
// MaxAge is the maximum duration for retaining old log files, in days.
|
||||
MaxAge int `yaml:"log_max_age"`
|
||||
MaxAge int `yaml:"max_age"`
|
||||
|
||||
// Compress determines, if the rotated log files should be compressed using
|
||||
// gzip.
|
||||
Compress bool `yaml:"log_compress"`
|
||||
Compress bool `yaml:"compress"`
|
||||
|
||||
// LocalTime determines, if the time used for formatting the timestamps in
|
||||
// is the computer's local time.
|
||||
LocalTime bool `yaml:"log_localtime"`
|
||||
LocalTime bool `yaml:"local_time"`
|
||||
|
||||
// Verbose determines, if verbose (aka debug) logging is enabled.
|
||||
Verbose bool `yaml:"verbose"`
|
||||
@@ -91,18 +89,17 @@ type clientSourcesConfig struct {
|
||||
HostsFile bool `yaml:"hosts"`
|
||||
}
|
||||
|
||||
// configuration is loaded from YAML
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
// configuration is loaded from YAML.
|
||||
//
|
||||
// Field ordering is important, YAML fields better not to be reordered, if it's
|
||||
// not absolutely necessary.
|
||||
type configuration struct {
|
||||
// Raw file data to avoid re-reading of configuration file
|
||||
// It's reset after config is parsed
|
||||
fileData []byte
|
||||
|
||||
// BindHost is the address for the web interface server to listen on.
|
||||
BindHost netip.Addr `yaml:"bind_host"`
|
||||
// BindPort is the port for the web interface server to listen on.
|
||||
BindPort int `yaml:"bind_port"`
|
||||
|
||||
// HTTPConfig is the block with http conf.
|
||||
HTTPConfig httpConfig `yaml:"http"`
|
||||
// Users are the clients capable for accessing the web interface.
|
||||
Users []webUser `yaml:"users"`
|
||||
// AuthAttempts is the maximum number of failed login attempts a user
|
||||
@@ -120,10 +117,6 @@ type configuration struct {
|
||||
// DebugPProf defines if the profiling HTTP handler will listen on :6060.
|
||||
DebugPProf bool `yaml:"debug_pprof"`
|
||||
|
||||
// TTL for a web session (in hours)
|
||||
// An active session is automatically refreshed once a day.
|
||||
WebSessionTTLHours uint32 `yaml:"web_session_ttl"`
|
||||
|
||||
DNS dnsConfig `yaml:"dns"`
|
||||
TLS tlsConfigSettings `yaml:"tls"`
|
||||
QueryLog queryLogConfig `yaml:"querylog"`
|
||||
@@ -147,7 +140,8 @@ type configuration struct {
|
||||
// Keep this field sorted to ensure consistent ordering.
|
||||
Clients *clientsConfig `yaml:"clients"`
|
||||
|
||||
logSettings `yaml:",inline"`
|
||||
// Log is a block with log configuration settings.
|
||||
Log logSettings `yaml:"log"`
|
||||
|
||||
OSConfig *osConfig `yaml:"os"`
|
||||
|
||||
@@ -156,7 +150,23 @@ type configuration struct {
|
||||
SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
// httpConfig is a block with HTTP configuration params.
|
||||
//
|
||||
// Field ordering is important, YAML fields better not to be reordered, if it's
|
||||
// not absolutely necessary.
|
||||
type httpConfig struct {
|
||||
// Address is the address to serve the web UI on.
|
||||
Address netip.AddrPort
|
||||
|
||||
// SessionTTL for a web session.
|
||||
// An active session is automatically refreshed once a day.
|
||||
SessionTTL timeutil.Duration `yaml:"session_ttl"`
|
||||
}
|
||||
|
||||
// dnsConfig is a block with DNS configuration params.
|
||||
//
|
||||
// Field ordering is important, YAML fields better not to be reordered, if it's
|
||||
// not absolutely necessary.
|
||||
type dnsConfig struct {
|
||||
BindHosts []netip.Addr `yaml:"bind_hosts"`
|
||||
Port int `yaml:"port"`
|
||||
@@ -261,11 +271,12 @@ type statsConfig struct {
|
||||
//
|
||||
// TODO(a.garipov, e.burkov): This global is awful and must be removed.
|
||||
var config = &configuration{
|
||||
BindPort: 3000,
|
||||
BindHost: netip.IPv4Unspecified(),
|
||||
AuthAttempts: 5,
|
||||
AuthBlockMin: 15,
|
||||
WebSessionTTLHours: 30 * 24,
|
||||
AuthAttempts: 5,
|
||||
AuthBlockMin: 15,
|
||||
HTTPConfig: httpConfig{
|
||||
Address: netip.AddrPortFrom(netip.IPv4Unspecified(), 3000),
|
||||
SessionTTL: timeutil.Duration{Duration: 30 * timeutil.Day},
|
||||
},
|
||||
DNS: dnsConfig{
|
||||
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
|
||||
Port: defaultPortDNS,
|
||||
@@ -378,7 +389,7 @@ var config = &configuration{
|
||||
HostsFile: true,
|
||||
},
|
||||
},
|
||||
logSettings: logSettings{
|
||||
Log: logSettings{
|
||||
Compress: false,
|
||||
LocalTime: false,
|
||||
MaxBackups: 0,
|
||||
@@ -409,26 +420,26 @@ func (c *configuration) getConfigFilename() string {
|
||||
// separate method in order to configure logger before the actual configuration
|
||||
// is parsed and applied.
|
||||
func readLogSettings() (ls *logSettings) {
|
||||
ls = &logSettings{}
|
||||
conf := &configuration{}
|
||||
|
||||
yamlFile, err := readConfigFile()
|
||||
if err != nil {
|
||||
return ls
|
||||
return &logSettings{}
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(yamlFile, ls)
|
||||
err = yaml.Unmarshal(yamlFile, conf)
|
||||
if err != nil {
|
||||
log.Error("Couldn't get logging settings from the configuration: %s", err)
|
||||
}
|
||||
|
||||
return ls
|
||||
return &conf.Log
|
||||
}
|
||||
|
||||
// validateBindHosts returns error if any of binding hosts from configuration is
|
||||
// not a valid IP address.
|
||||
func validateBindHosts(conf *configuration) (err error) {
|
||||
if !conf.BindHost.IsValid() {
|
||||
return errors.Error("bind_host is not a valid ip address")
|
||||
if !conf.HTTPConfig.Address.IsValid() {
|
||||
return errors.Error("http.address is not a valid ip address")
|
||||
}
|
||||
|
||||
for i, addr := range conf.DNS.BindHosts {
|
||||
@@ -462,7 +473,7 @@ func parseConfig() (err error) {
|
||||
}
|
||||
|
||||
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
||||
addPorts(tcpPorts, tcpPort(config.BindPort))
|
||||
addPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port()))
|
||||
|
||||
udpPorts := aghalg.UniqChecker[udpPort]{}
|
||||
addPorts(udpPorts, udpPort(config.DNS.Port))
|
||||
|
||||
@@ -103,7 +103,7 @@ type statusResponse struct {
|
||||
Language string `json:"language"`
|
||||
DNSAddrs []string `json:"dns_addresses"`
|
||||
DNSPort int `json:"dns_port"`
|
||||
HTTPPort int `json:"http_port"`
|
||||
HTTPPort uint16 `json:"http_port"`
|
||||
|
||||
// ProtectionDisabledDuration is the duration of the protection pause in
|
||||
// milliseconds.
|
||||
@@ -158,7 +158,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
Language: config.Language,
|
||||
DNSAddrs: dnsAddrs,
|
||||
DNSPort: config.DNS.Port,
|
||||
HTTPPort: config.BindPort,
|
||||
HTTPPort: config.HTTPConfig.Address.Port(),
|
||||
ProtectionDisabledDuration: protectionDisabledDuration,
|
||||
ProtectionEnabled: protectionEnabled,
|
||||
IsRunning: isRunning(),
|
||||
|
||||
@@ -96,8 +96,9 @@ type checkConfResp struct {
|
||||
func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "validating ports: %w") }()
|
||||
|
||||
portInt := req.Web.Port
|
||||
port := tcpPort(portInt)
|
||||
// TODO(a.garipov): Declare all port variables anywhere as uint16.
|
||||
reqPort := uint16(req.Web.Port)
|
||||
port := tcpPort(reqPort)
|
||||
addPorts(tcpPorts, port)
|
||||
if err = tcpPorts.Validate(); err != nil {
|
||||
// Reset the value for the port to 1 to make sure that validateDNS
|
||||
@@ -108,15 +109,15 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
|
||||
return err
|
||||
}
|
||||
|
||||
switch portInt {
|
||||
case 0, config.BindPort:
|
||||
switch reqPort {
|
||||
case 0, config.HTTPConfig.Address.Port():
|
||||
return nil
|
||||
default:
|
||||
// Go on and check the port binding only if it's not zero or won't be
|
||||
// unbound after install.
|
||||
}
|
||||
|
||||
return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(portInt)))
|
||||
return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, reqPort))
|
||||
}
|
||||
|
||||
// validateDNS returns error if the DNS part of the initial configuration can't
|
||||
@@ -127,11 +128,11 @@ func (req *checkConfReq) validateDNS(
|
||||
) (canAutofix bool, err error) {
|
||||
defer func() { err = errors.Annotate(err, "validating ports: %w") }()
|
||||
|
||||
port := req.DNS.Port
|
||||
port := uint16(req.DNS.Port)
|
||||
switch port {
|
||||
case 0:
|
||||
return false, nil
|
||||
case config.BindPort:
|
||||
case config.HTTPConfig.Address.Port():
|
||||
// Go on and only check the UDP port since the TCP one is already bound
|
||||
// by AdGuard Home for web interface.
|
||||
default:
|
||||
@@ -318,8 +319,7 @@ type applyConfigReq struct {
|
||||
// copyInstallSettings copies the installation parameters between two
|
||||
// configuration structures.
|
||||
func copyInstallSettings(dst, src *configuration) {
|
||||
dst.BindHost = src.BindHost
|
||||
dst.BindPort = src.BindPort
|
||||
dst.HTTPConfig = src.HTTPConfig
|
||||
dst.DNS.BindHosts = src.DNS.BindHosts
|
||||
dst.DNS.Port = src.DNS.Port
|
||||
}
|
||||
@@ -413,8 +413,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
|
||||
copyInstallSettings(curConfig, config)
|
||||
|
||||
Context.firstRun = false
|
||||
config.BindHost = req.Web.IP
|
||||
config.BindPort = req.Web.Port
|
||||
config.HTTPConfig.Address = netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port))
|
||||
config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
|
||||
config.DNS.Port = req.DNS.Port
|
||||
|
||||
@@ -487,7 +486,8 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
|
||||
return nil, false, errors.Error("ports cannot be 0")
|
||||
}
|
||||
|
||||
restartHTTP = config.BindHost != req.Web.IP || config.BindPort != req.Web.Port
|
||||
addrPort := config.HTTPConfig.Address
|
||||
restartHTTP = addrPort.Addr() != req.Web.IP || int(addrPort.Port()) != req.Web.Port
|
||||
if restartHTTP {
|
||||
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port)))
|
||||
if err != nil {
|
||||
|
||||
@@ -157,7 +157,9 @@ func (vr *versionResponse) setAllowedToAutoUpdate() (err error) {
|
||||
Context.tls.WriteDiskConfig(tlsConf)
|
||||
|
||||
canUpdate := true
|
||||
if tlsConfUsesPrivilegedPorts(tlsConf) || config.BindPort < 1024 || config.DNS.Port < 1024 {
|
||||
if tlsConfUsesPrivilegedPorts(tlsConf) ||
|
||||
config.HTTPConfig.Address.Port() < 1024 ||
|
||||
config.DNS.Port < 1024 {
|
||||
canUpdate, err = aghnet.CanBindPrivilegedPorts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking ability to bind privileged ports: %w", err)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
@@ -167,30 +168,77 @@ func initDNSServer(
|
||||
return fmt.Errorf("dnsServer.Prepare: %w", err)
|
||||
}
|
||||
|
||||
if config.Clients.Sources.RDNS {
|
||||
Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS)
|
||||
}
|
||||
|
||||
initRDNS()
|
||||
initWHOIS()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// defaultQueueSize is the size of queue of IPs for rDNS and WHOIS
|
||||
// processing.
|
||||
defaultQueueSize = 255
|
||||
|
||||
// defaultCacheSize is the maximum size of the cache for rDNS and WHOIS
|
||||
// processing. It must be greater than zero.
|
||||
defaultCacheSize = 10_000
|
||||
|
||||
// defaultIPTTL is the Time to Live duration for IP addresses cached by
|
||||
// rDNS and WHOIS.
|
||||
defaultIPTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
// initRDNS initializes the rDNS.
|
||||
func initRDNS() {
|
||||
Context.rdnsCh = make(chan netip.Addr, defaultQueueSize)
|
||||
|
||||
// TODO(s.chzhen): Add ability to disable it on dns server configuration
|
||||
// update in [dnsforward] package.
|
||||
r := rdns.New(&rdns.Config{
|
||||
Exchanger: Context.dnsServer,
|
||||
CacheSize: defaultCacheSize,
|
||||
CacheTTL: defaultIPTTL,
|
||||
})
|
||||
|
||||
go processRDNS(r)
|
||||
}
|
||||
|
||||
// processRDNS processes reverse DNS lookup queries. It is intended to be used
|
||||
// as a goroutine.
|
||||
func processRDNS(r rdns.Interface) {
|
||||
defer log.OnPanic("rdns")
|
||||
|
||||
for ip := range Context.rdnsCh {
|
||||
ok := Context.dnsServer.ShouldResolveClient(ip)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
host, changed := r.Process(ip)
|
||||
if host == "" || !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
ok = Context.clients.AddHost(ip, host, ClientSourceRDNS)
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug(
|
||||
"dns: can't set rdns info for client %q: already set with higher priority source",
|
||||
ip,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// initWHOIS initializes the WHOIS.
|
||||
//
|
||||
// TODO(s.chzhen): Consider making configurable.
|
||||
func initWHOIS() {
|
||||
const (
|
||||
// defaultQueueSize is the size of queue of IPs for WHOIS processing.
|
||||
defaultQueueSize = 255
|
||||
|
||||
// defaultTimeout is the timeout for WHOIS requests.
|
||||
defaultTimeout = 5 * time.Second
|
||||
|
||||
// defaultCacheSize is the maximum size of the cache. If it's zero,
|
||||
// cache size is unlimited.
|
||||
defaultCacheSize = 10_000
|
||||
|
||||
// defaultMaxConnReadSize is an upper limit in bytes for reading from
|
||||
// net.Conn.
|
||||
defaultMaxConnReadSize = 64 * 1024
|
||||
@@ -200,9 +248,6 @@ func initWHOIS() {
|
||||
|
||||
// defaultMaxInfoLen is the maximum length of whois.Info fields.
|
||||
defaultMaxInfoLen = 250
|
||||
|
||||
// defaultIPTTL is the Time to Live duration for cached IP addresses.
|
||||
defaultIPTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
Context.whoisCh = make(chan netip.Addr, defaultQueueSize)
|
||||
@@ -274,11 +319,7 @@ func onDNSRequest(pctx *proxy.DNSContext) {
|
||||
return
|
||||
}
|
||||
|
||||
srcs := config.Clients.Sources
|
||||
if srcs.RDNS && !ip.IsLoopback() {
|
||||
Context.rdns.Begin(ip)
|
||||
}
|
||||
|
||||
Context.rdnsCh <- ip
|
||||
Context.whoisCh <- ip
|
||||
}
|
||||
|
||||
@@ -517,11 +558,7 @@ func startDNSServer() error {
|
||||
|
||||
const topClientsNumber = 100 // the number of clients to get
|
||||
for _, ip := range Context.stats.TopClientsIP(topClientsNumber) {
|
||||
srcs := config.Clients.Sources
|
||||
if srcs.RDNS && !ip.IsLoopback() {
|
||||
Context.rdns.Begin(ip)
|
||||
}
|
||||
|
||||
Context.rdnsCh <- ip
|
||||
Context.whoisCh <- ip
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ type homeContext struct {
|
||||
stats stats.Interface // statistics module
|
||||
queryLog querylog.QueryLog // query log module
|
||||
dnsServer *dnsforward.Server // DNS module
|
||||
rdns *RDNS // rDNS module
|
||||
dhcpServer dhcpd.Interface // DHCP module
|
||||
auth *Auth // HTTP authentication module
|
||||
filters *filtering.DNSFilter // DNS filtering module
|
||||
@@ -83,6 +82,9 @@ type homeContext struct {
|
||||
client *http.Client
|
||||
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
|
||||
|
||||
// rdnsCh is the channel for receiving IPs for rDNS processing.
|
||||
rdnsCh chan netip.Addr
|
||||
|
||||
// whoisCh is the channel for receiving IPs for WHOIS processing.
|
||||
whoisCh chan netip.Addr
|
||||
|
||||
@@ -372,8 +374,26 @@ func initContextClients() (err error) {
|
||||
|
||||
// setupBindOpts overrides bind host/port from the opts.
|
||||
func setupBindOpts(opts options) (err error) {
|
||||
bindAddr := opts.bindAddr
|
||||
if bindAddr != (netip.AddrPort{}) {
|
||||
config.HTTPConfig.Address = bindAddr
|
||||
|
||||
if config.HTTPConfig.Address.Port() != 0 {
|
||||
err = checkPorts()
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.bindPort != 0 {
|
||||
config.BindPort = opts.bindPort
|
||||
config.HTTPConfig.Address = netip.AddrPortFrom(
|
||||
config.HTTPConfig.Address.Addr(),
|
||||
uint16(opts.bindPort),
|
||||
)
|
||||
|
||||
err = checkPorts()
|
||||
if err != nil {
|
||||
@@ -383,20 +403,10 @@ func setupBindOpts(opts options) (err error) {
|
||||
}
|
||||
|
||||
if opts.bindHost.IsValid() {
|
||||
config.BindHost = opts.bindHost
|
||||
}
|
||||
|
||||
// Rewrite deprecated options.
|
||||
bindAddr := opts.bindAddr
|
||||
if bindAddr.IsValid() {
|
||||
config.BindHost = bindAddr.Addr()
|
||||
config.BindPort = int(bindAddr.Port())
|
||||
|
||||
err = checkPorts()
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
config.HTTPConfig.Address = netip.AddrPortFrom(
|
||||
opts.bindHost,
|
||||
config.HTTPConfig.Address.Port(),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -460,7 +470,7 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
|
||||
ServiceName: pcService,
|
||||
TXTSuffix: pcTXTSuffix,
|
||||
CacheTime: cacheTime,
|
||||
CacheSize: conf.SafeBrowsingCacheSize,
|
||||
CacheSize: conf.ParentalCacheSize,
|
||||
})
|
||||
|
||||
conf.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
||||
@@ -480,7 +490,7 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
|
||||
// checkPorts is a helper for ports validation in config.
|
||||
func checkPorts() (err error) {
|
||||
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
||||
addPorts(tcpPorts, tcpPort(config.BindPort))
|
||||
addPorts(tcpPorts, tcpPort(config.HTTPConfig.Address.Port()))
|
||||
|
||||
udpPorts := aghalg.UniqChecker[udpPort]{}
|
||||
addPorts(udpPorts, udpPort(config.DNS.Port))
|
||||
@@ -520,8 +530,8 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *webAPI, err error) {
|
||||
|
||||
webConf := webConfig{
|
||||
firstRun: Context.firstRun,
|
||||
BindHost: config.BindHost,
|
||||
BindPort: config.BindPort,
|
||||
BindHost: config.HTTPConfig.Address.Addr(),
|
||||
BindPort: int(config.HTTPConfig.Address.Port()),
|
||||
|
||||
ReadTimeout: readTimeout,
|
||||
ReadHeaderTimeout: readHdrTimeout,
|
||||
@@ -657,8 +667,8 @@ func initUsers() (auth *Auth, err error) {
|
||||
log.Info("authratelimiter is disabled")
|
||||
}
|
||||
|
||||
sessionTTL := config.WebSessionTTLHours * 60 * 60
|
||||
auth = InitAuth(sessFilename, config.Users, sessionTTL, rateLimiter)
|
||||
sessionTTL := config.HTTPConfig.SessionTTL.Seconds()
|
||||
auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter)
|
||||
if auth == nil {
|
||||
return nil, errors.Error("initializing auth module failed")
|
||||
}
|
||||
@@ -821,20 +831,21 @@ func configureLogger(opts options) (err error) {
|
||||
// getLogSettings returns a log settings object properly initialized from opts.
|
||||
func getLogSettings(opts options) (ls *logSettings) {
|
||||
ls = readLogSettings()
|
||||
configLogSettings := config.Log
|
||||
|
||||
// Command-line arguments can override config settings.
|
||||
if opts.verbose || config.Verbose {
|
||||
if opts.verbose || configLogSettings.Verbose {
|
||||
ls.Verbose = true
|
||||
}
|
||||
|
||||
ls.File = stringutil.Coalesce(opts.logFile, config.File, ls.File)
|
||||
ls.File = stringutil.Coalesce(opts.logFile, configLogSettings.File, ls.File)
|
||||
|
||||
// Handle default log settings overrides.
|
||||
ls.Compress = config.Compress
|
||||
ls.LocalTime = config.LocalTime
|
||||
ls.MaxBackups = config.MaxBackups
|
||||
ls.MaxSize = config.MaxSize
|
||||
ls.MaxAge = config.MaxAge
|
||||
ls.Compress = configLogSettings.Compress
|
||||
ls.LocalTime = configLogSettings.LocalTime
|
||||
ls.MaxBackups = configLogSettings.MaxBackups
|
||||
ls.MaxSize = configLogSettings.MaxSize
|
||||
ls.MaxAge = configLogSettings.MaxAge
|
||||
|
||||
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
||||
// When running as a Windows service, use eventlog by default if
|
||||
@@ -936,7 +947,7 @@ func printHTTPAddresses(proto string) {
|
||||
Context.tls.WriteDiskConfig(&tlsConf)
|
||||
}
|
||||
|
||||
port := config.BindPort
|
||||
port := int(config.HTTPConfig.Address.Port())
|
||||
if proto == aghhttp.SchemeHTTPS {
|
||||
port = tlsConf.PortHTTPS
|
||||
}
|
||||
@@ -948,9 +959,9 @@ func printHTTPAddresses(proto string) {
|
||||
return
|
||||
}
|
||||
|
||||
bindhost := config.BindHost
|
||||
if !bindhost.IsUnspecified() {
|
||||
printWebAddrs(proto, bindhost.String(), port)
|
||||
bindHost := config.HTTPConfig.Address.Addr()
|
||||
if !bindHost.IsUnspecified() {
|
||||
printWebAddrs(proto, bindHost.String(), port)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -961,14 +972,14 @@ func printHTTPAddresses(proto string) {
|
||||
// That's weird, but we'll ignore it.
|
||||
//
|
||||
// TODO(e.burkov): Find out when it happens.
|
||||
printWebAddrs(proto, bindhost.String(), port)
|
||||
printWebAddrs(proto, bindHost.String(), port)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
for _, addr := range iface.Addresses {
|
||||
printWebAddrs(proto, addr.String(), config.BindPort)
|
||||
printWebAddrs(proto, addr.String(), port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// RDNS resolves clients' addresses to enrich their metadata.
|
||||
type RDNS struct {
|
||||
exchanger dnsforward.RDNSExchanger
|
||||
clients *clientsContainer
|
||||
|
||||
// ipCh used to pass client's IP to rDNS workerLoop.
|
||||
ipCh chan netip.Addr
|
||||
|
||||
// ipCache caches the IP addresses to be resolved by rDNS. The resolved
|
||||
// address stays here while it's inside clients. After leaving clients the
|
||||
// address will be resolved once again. If the address couldn't be
|
||||
// resolved, cache prevents further attempts to resolve it for some time.
|
||||
ipCache cache.Cache
|
||||
|
||||
// usePrivate stores the state of current private reverse-DNS resolving
|
||||
// settings.
|
||||
usePrivate atomic.Bool
|
||||
}
|
||||
|
||||
// Default AdGuard Home reverse DNS values.
|
||||
const (
|
||||
revDNSCacheSize = 10000
|
||||
|
||||
// TODO(e.burkov): Make these values configurable.
|
||||
revDNSCacheTTL = 24 * 60 * 60
|
||||
revDNSFailureCacheTTL = 1 * 60 * 60
|
||||
|
||||
revDNSQueueSize = 256
|
||||
)
|
||||
|
||||
// NewRDNS creates and returns initialized RDNS.
|
||||
func NewRDNS(
|
||||
exchanger dnsforward.RDNSExchanger,
|
||||
clients *clientsContainer,
|
||||
usePrivate bool,
|
||||
) (rDNS *RDNS) {
|
||||
rDNS = &RDNS{
|
||||
exchanger: exchanger,
|
||||
clients: clients,
|
||||
ipCache: cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxCount: revDNSCacheSize,
|
||||
}),
|
||||
ipCh: make(chan netip.Addr, revDNSQueueSize),
|
||||
}
|
||||
|
||||
rDNS.usePrivate.Store(usePrivate)
|
||||
|
||||
go rDNS.workerLoop()
|
||||
|
||||
return rDNS
|
||||
}
|
||||
|
||||
// ensurePrivateCache ensures that the state of the RDNS cache is consistent
|
||||
// with the current private client RDNS resolving settings.
|
||||
//
|
||||
// TODO(e.burkov): Clearing cache each time this value changed is not a perfect
|
||||
// approach since only unresolved locally-served addresses should be removed.
|
||||
// Implement when improving the cache.
|
||||
func (r *RDNS) ensurePrivateCache() {
|
||||
usePrivate := r.exchanger.ResolvesPrivatePTR()
|
||||
if r.usePrivate.CompareAndSwap(!usePrivate, usePrivate) {
|
||||
r.ipCache.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// isCached returns true if ip is already cached and not expired yet. It also
|
||||
// caches it otherwise.
|
||||
func (r *RDNS) isCached(ip netip.Addr) (ok bool) {
|
||||
ipBytes := ip.AsSlice()
|
||||
now := uint64(time.Now().Unix())
|
||||
if expire := r.ipCache.Get(ipBytes); len(expire) != 0 {
|
||||
return binary.BigEndian.Uint64(expire) > now
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// cache caches the ip address for ttl seconds.
|
||||
func (r *RDNS) cache(ip netip.Addr, ttl uint64) {
|
||||
ipData := ip.AsSlice()
|
||||
|
||||
ttlData := [8]byte{}
|
||||
binary.BigEndian.PutUint64(ttlData[:], uint64(time.Now().Unix())+ttl)
|
||||
|
||||
r.ipCache.Set(ipData, ttlData[:])
|
||||
}
|
||||
|
||||
// Begin adds the ip to the resolving queue if it is not cached or already
|
||||
// resolved.
|
||||
func (r *RDNS) Begin(ip netip.Addr) {
|
||||
r.ensurePrivateCache()
|
||||
|
||||
if r.isCached(ip) || r.clients.clientSource(ip) > ClientSourceRDNS {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case r.ipCh <- ip:
|
||||
log.Debug("rdns: %q added to queue", ip)
|
||||
default:
|
||||
log.Debug("rdns: queue is full")
|
||||
}
|
||||
}
|
||||
|
||||
// workerLoop handles incoming IP addresses from ipChan and adds it into
|
||||
// clients.
|
||||
func (r *RDNS) workerLoop() {
|
||||
defer log.OnPanic("rdns")
|
||||
|
||||
for ip := range r.ipCh {
|
||||
ttl := uint64(revDNSCacheTTL)
|
||||
|
||||
host, err := r.exchanger.Exchange(ip.AsSlice())
|
||||
if err != nil {
|
||||
log.Debug("rdns: resolving %q: %s", ip, err)
|
||||
if errors.Is(err, dnsforward.ErrRDNSFailed) {
|
||||
// Cache failure for a less time.
|
||||
ttl = revDNSFailureCacheTTL
|
||||
}
|
||||
}
|
||||
|
||||
r.cache(ip, ttl)
|
||||
|
||||
if host != "" {
|
||||
_ = r.clients.AddHost(ip, host, ClientSourceRDNS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRDNS_Begin(t *testing.T) {
|
||||
aghtest.ReplaceLogLevel(t, log.DEBUG)
|
||||
w := &bytes.Buffer{}
|
||||
aghtest.ReplaceLogWriter(t, w)
|
||||
|
||||
ip1234, ip1235 := netip.MustParseAddr("1.2.3.4"), netip.MustParseAddr("1.2.3.5")
|
||||
|
||||
testCases := []struct {
|
||||
cliIDIndex map[string]*Client
|
||||
customChan chan netip.Addr
|
||||
name string
|
||||
wantLog string
|
||||
ip netip.Addr
|
||||
wantCacheHit int
|
||||
wantCacheMiss int
|
||||
}{{
|
||||
cliIDIndex: map[string]*Client{},
|
||||
customChan: nil,
|
||||
name: "cached",
|
||||
wantLog: "",
|
||||
ip: ip1234,
|
||||
wantCacheHit: 1,
|
||||
wantCacheMiss: 0,
|
||||
}, {
|
||||
cliIDIndex: map[string]*Client{},
|
||||
customChan: nil,
|
||||
name: "not_cached",
|
||||
wantLog: "rdns: queue is full",
|
||||
ip: ip1235,
|
||||
wantCacheHit: 0,
|
||||
wantCacheMiss: 1,
|
||||
}, {
|
||||
cliIDIndex: map[string]*Client{"1.2.3.5": {}},
|
||||
customChan: nil,
|
||||
name: "already_in_clients",
|
||||
wantLog: "",
|
||||
ip: ip1235,
|
||||
wantCacheHit: 0,
|
||||
wantCacheMiss: 1,
|
||||
}, {
|
||||
cliIDIndex: map[string]*Client{},
|
||||
customChan: make(chan netip.Addr, 1),
|
||||
name: "add_to_queue",
|
||||
wantLog: `rdns: "1.2.3.5" added to queue`,
|
||||
ip: ip1235,
|
||||
wantCacheHit: 0,
|
||||
wantCacheMiss: 1,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
w.Reset()
|
||||
|
||||
ipCache := cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxCount: revDNSCacheSize,
|
||||
})
|
||||
ttl := make([]byte, binary.Size(uint64(0)))
|
||||
binary.BigEndian.PutUint64(ttl, uint64(time.Now().Add(100*time.Hour).Unix()))
|
||||
|
||||
rdns := &RDNS{
|
||||
ipCache: ipCache,
|
||||
exchanger: &rDNSExchanger{
|
||||
ex: aghtest.NewErrorUpstream(),
|
||||
},
|
||||
clients: &clientsContainer{
|
||||
list: map[string]*Client{},
|
||||
idIndex: tc.cliIDIndex,
|
||||
ipToRC: map[netip.Addr]*RuntimeClient{},
|
||||
allTags: stringutil.NewSet(),
|
||||
},
|
||||
}
|
||||
ipCache.Clear()
|
||||
ipCache.Set(net.IP{1, 2, 3, 4}, ttl)
|
||||
|
||||
if tc.customChan != nil {
|
||||
rdns.ipCh = tc.customChan
|
||||
defer close(tc.customChan)
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rdns.Begin(tc.ip)
|
||||
assert.Equal(t, tc.wantCacheHit, ipCache.Stats().Hit)
|
||||
assert.Equal(t, tc.wantCacheMiss, ipCache.Stats().Miss)
|
||||
assert.Contains(t, w.String(), tc.wantLog)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// rDNSExchanger is a mock dnsforward.RDNSExchanger implementation for tests.
|
||||
type rDNSExchanger struct {
|
||||
ex upstream.Upstream
|
||||
usePrivate bool
|
||||
}
|
||||
|
||||
// Exchange implements dnsforward.RDNSExchanger interface for *RDNSExchanger.
|
||||
func (e *rDNSExchanger) Exchange(ip net.IP) (host string, err error) {
|
||||
rev, err := netutil.IPToReversedAddr(ip)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reversing ip: %w", err)
|
||||
}
|
||||
|
||||
req := &dns.Msg{
|
||||
Question: []dns.Question{{
|
||||
Name: dns.Fqdn(rev),
|
||||
Qclass: dns.ClassINET,
|
||||
Qtype: dns.TypePTR,
|
||||
}},
|
||||
}
|
||||
|
||||
resp, err := e.ex.Exchange(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(resp.Answer) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return resp.Answer[0].Header().Name, nil
|
||||
}
|
||||
|
||||
// Exchange implements dnsforward.RDNSExchanger interface for *RDNSExchanger.
|
||||
func (e *rDNSExchanger) ResolvesPrivatePTR() (ok bool) {
|
||||
return e.usePrivate
|
||||
}
|
||||
|
||||
func TestRDNS_ensurePrivateCache(t *testing.T) {
|
||||
data := []byte{1, 2, 3, 4}
|
||||
|
||||
ipCache := cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxCount: revDNSCacheSize,
|
||||
})
|
||||
|
||||
ex := &rDNSExchanger{
|
||||
ex: aghtest.NewErrorUpstream(),
|
||||
}
|
||||
|
||||
rdns := &RDNS{
|
||||
ipCache: ipCache,
|
||||
exchanger: ex,
|
||||
}
|
||||
|
||||
rdns.ipCache.Set(data, data)
|
||||
require.NotZero(t, rdns.ipCache.Stats().Count)
|
||||
|
||||
ex.usePrivate = !ex.usePrivate
|
||||
|
||||
rdns.ensurePrivateCache()
|
||||
require.Zero(t, rdns.ipCache.Stats().Count)
|
||||
}
|
||||
|
||||
func TestRDNS_WorkerLoop(t *testing.T) {
|
||||
aghtest.ReplaceLogLevel(t, log.DEBUG)
|
||||
w := &bytes.Buffer{}
|
||||
aghtest.ReplaceLogWriter(t, w)
|
||||
|
||||
localIP := netip.MustParseAddr("192.168.1.1")
|
||||
revIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
revIPv6, err := netutil.IPToReversedAddr(net.ParseIP("2a00:1450:400c:c06::93"))
|
||||
require.NoError(t, err)
|
||||
|
||||
locUpstream := &aghtest.UpstreamMock{
|
||||
OnAddress: func() (addr string) { return "local.upstream.example" },
|
||||
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
return aghalg.Coalesce(
|
||||
aghtest.MatchedResponse(req, dns.TypePTR, revIPv4, "local.domain"),
|
||||
aghtest.MatchedResponse(req, dns.TypePTR, revIPv6, "ipv6.domain"),
|
||||
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
|
||||
), nil
|
||||
},
|
||||
}
|
||||
|
||||
errUpstream := aghtest.NewErrorUpstream()
|
||||
|
||||
testCases := []struct {
|
||||
ups upstream.Upstream
|
||||
cliIP netip.Addr
|
||||
wantLog string
|
||||
name string
|
||||
wantClientSource clientSource
|
||||
}{{
|
||||
ups: locUpstream,
|
||||
cliIP: localIP,
|
||||
wantLog: "",
|
||||
name: "all_good",
|
||||
wantClientSource: ClientSourceRDNS,
|
||||
}, {
|
||||
ups: errUpstream,
|
||||
cliIP: netip.MustParseAddr("192.168.1.2"),
|
||||
wantLog: `rdns: resolving "192.168.1.2": test upstream error`,
|
||||
name: "resolve_error",
|
||||
wantClientSource: ClientSourceNone,
|
||||
}, {
|
||||
ups: locUpstream,
|
||||
cliIP: netip.MustParseAddr("2a00:1450:400c:c06::93"),
|
||||
wantLog: "",
|
||||
name: "ipv6_good",
|
||||
wantClientSource: ClientSourceRDNS,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
w.Reset()
|
||||
|
||||
cc := newClientsContainer(t)
|
||||
ch := make(chan netip.Addr)
|
||||
rdns := &RDNS{
|
||||
exchanger: &rDNSExchanger{
|
||||
ex: tc.ups,
|
||||
},
|
||||
clients: cc,
|
||||
ipCh: ch,
|
||||
ipCache: cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxCount: revDNSCacheSize,
|
||||
}),
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
rdns.workerLoop()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
ch <- tc.cliIP
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
|
||||
if tc.wantLog != "" {
|
||||
assert.Contains(t, w.String(), tc.wantLog)
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.wantClientSource, cc.clientSource(tc.cliIP))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -320,7 +320,7 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if setts.Enabled {
|
||||
err = validatePorts(
|
||||
tcpPort(config.BindPort),
|
||||
tcpPort(config.HTTPConfig.Address.Port()),
|
||||
tcpPort(setts.PortHTTPS),
|
||||
tcpPort(setts.PortDNSOverTLS),
|
||||
tcpPort(setts.PortDNSCrypt),
|
||||
@@ -407,7 +407,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if req.Enabled {
|
||||
err = validatePorts(
|
||||
tcpPort(config.BindPort),
|
||||
tcpPort(config.HTTPConfig.Address.Port()),
|
||||
tcpPort(req.PortHTTPS),
|
||||
tcpPort(req.PortDNSOverTLS),
|
||||
tcpPort(req.PortDNSCrypt),
|
||||
|
||||
@@ -3,6 +3,7 @@ package home
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
// currentSchemaVersion is the current schema version.
|
||||
const currentSchemaVersion = 22
|
||||
const currentSchemaVersion = 24
|
||||
|
||||
// These aliases are provided for convenience.
|
||||
type (
|
||||
@@ -96,6 +97,8 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
|
||||
upgradeSchema19to20,
|
||||
upgradeSchema20to21,
|
||||
upgradeSchema21to22,
|
||||
upgradeSchema22to23,
|
||||
upgradeSchema23to24,
|
||||
}
|
||||
|
||||
n := 0
|
||||
@@ -1256,6 +1259,177 @@ func upgradeSchema21to22(diskConf yobj) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeSchema22to23 performs the following changes:
|
||||
//
|
||||
// # BEFORE:
|
||||
// 'bind_host': '1.2.3.4'
|
||||
// 'bind_port': 8080
|
||||
// 'web_session_ttl': 720
|
||||
//
|
||||
// # AFTER:
|
||||
// 'http':
|
||||
// 'address': '1.2.3.4:8080'
|
||||
// 'session_ttl': '720h'
|
||||
func upgradeSchema22to23(diskConf yobj) (err error) {
|
||||
log.Printf("Upgrade yaml: 22 to 23")
|
||||
diskConf["schema_version"] = 23
|
||||
|
||||
bindHostVal, ok := diskConf["bind_host"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindHost, ok := bindHostVal.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type of bind_host: %T", bindHostVal)
|
||||
}
|
||||
|
||||
bindHostAddr, err := netip.ParseAddr(bindHost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid bind_host value: %s", bindHost)
|
||||
}
|
||||
|
||||
bindPortVal, ok := diskConf["bind_port"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindPort, ok := bindPortVal.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type of bind_port: %T", bindPortVal)
|
||||
}
|
||||
|
||||
sessionTTLVal, ok := diskConf["web_session_ttl"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionTTL, ok := sessionTTLVal.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type of web_session_ttl: %T", sessionTTLVal)
|
||||
}
|
||||
|
||||
addr := netip.AddrPortFrom(bindHostAddr, uint16(bindPort))
|
||||
if !addr.IsValid() {
|
||||
return fmt.Errorf("invalid address: %s", addr)
|
||||
}
|
||||
|
||||
diskConf["http"] = yobj{
|
||||
"address": addr.String(),
|
||||
"session_ttl": timeutil.Duration{Duration: time.Duration(sessionTTL) * time.Hour}.String(),
|
||||
}
|
||||
|
||||
delete(diskConf, "bind_host")
|
||||
delete(diskConf, "bind_port")
|
||||
delete(diskConf, "web_session_ttl")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeSchema23to24 performs the following changes:
|
||||
//
|
||||
// # BEFORE:
|
||||
// 'log_file': ""
|
||||
// 'log_max_backups': 0
|
||||
// 'log_max_size': 100
|
||||
// 'log_max_age': 3
|
||||
// 'log_compress': false
|
||||
// 'log_localtime': false
|
||||
// 'verbose': false
|
||||
//
|
||||
// # AFTER:
|
||||
// 'log':
|
||||
// 'file': ""
|
||||
// 'max_backups': 0
|
||||
// 'max_size': 100
|
||||
// 'max_age': 3
|
||||
// 'compress': false
|
||||
// 'local_time': false
|
||||
// 'verbose': false
|
||||
func upgradeSchema23to24(diskConf yobj) (err error) {
|
||||
log.Printf("Upgrade yaml: 23 to 24")
|
||||
diskConf["schema_version"] = 24
|
||||
|
||||
logObj := yobj{}
|
||||
err = coalesceError(
|
||||
moveField[string](diskConf, logObj, "log_file", "file"),
|
||||
moveField[int](diskConf, logObj, "log_max_backups", "max_backups"),
|
||||
moveField[int](diskConf, logObj, "log_max_size", "max_size"),
|
||||
moveField[int](diskConf, logObj, "log_max_age", "max_age"),
|
||||
moveField[bool](diskConf, logObj, "log_compress", "compress"),
|
||||
moveField[bool](diskConf, logObj, "log_localtime", "local_time"),
|
||||
moveField[bool](diskConf, logObj, "verbose", "verbose"),
|
||||
)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
if len(logObj) != 0 {
|
||||
diskConf["log"] = logObj
|
||||
}
|
||||
|
||||
delete(diskConf, "log_file")
|
||||
delete(diskConf, "log_max_backups")
|
||||
delete(diskConf, "log_max_size")
|
||||
delete(diskConf, "log_max_age")
|
||||
delete(diskConf, "log_compress")
|
||||
delete(diskConf, "log_localtime")
|
||||
delete(diskConf, "verbose")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// moveField gets field value for key from diskConf, and then set this value
|
||||
// in newConf for newKey.
|
||||
func moveField[T any](diskConf, newConf yobj, key, newKey string) (err error) {
|
||||
ok, newVal, err := fieldValue[T](diskConf, key)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := newVal.(type) {
|
||||
case int, bool, string:
|
||||
newConf[newKey] = v
|
||||
default:
|
||||
return fmt.Errorf("invalid type of %s: %T", key, newVal)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fieldValue returns the value of type T for key in diskConf object.
|
||||
func fieldValue[T any](diskConf yobj, key string) (ok bool, field any, err error) {
|
||||
fieldVal, ok := diskConf[key]
|
||||
if !ok {
|
||||
return false, new(T), nil
|
||||
}
|
||||
|
||||
f, ok := fieldVal.(T)
|
||||
if !ok {
|
||||
return false, nil, fmt.Errorf("unexpected type of %s: %T", key, fieldVal)
|
||||
}
|
||||
|
||||
return true, f, nil
|
||||
}
|
||||
|
||||
// coalesceError returns the first non-nil error. It is named after function
|
||||
// COALESCE in SQL. If all errors are nil, it returns nil.
|
||||
//
|
||||
// TODO(a.garipov): Consider a similar helper to group errors together to show
|
||||
// as many errors as possible.
|
||||
//
|
||||
// TODO(a.garipov): Think of ways to merge with [aghalg.Coalesce].
|
||||
func coalesceError(errors ...error) (res error) {
|
||||
for _, err := range errors {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Replace with log.Output when we port it to our logging
|
||||
// package.
|
||||
func funcName() string {
|
||||
|
||||
@@ -1253,3 +1253,129 @@ func TestUpgradeSchema21to22(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeSchema22to23(t *testing.T) {
|
||||
const newSchemaVer = 23
|
||||
|
||||
testCases := []struct {
|
||||
in yobj
|
||||
want yobj
|
||||
name string
|
||||
}{{
|
||||
name: "empty",
|
||||
in: yobj{},
|
||||
want: yobj{
|
||||
"schema_version": newSchemaVer,
|
||||
},
|
||||
}, {
|
||||
name: "ok",
|
||||
in: yobj{
|
||||
"bind_host": "1.2.3.4",
|
||||
"bind_port": 8081,
|
||||
"web_session_ttl": 720,
|
||||
},
|
||||
want: yobj{
|
||||
"http": yobj{
|
||||
"address": "1.2.3.4:8081",
|
||||
"session_ttl": "720h",
|
||||
},
|
||||
"schema_version": newSchemaVer,
|
||||
},
|
||||
}, {
|
||||
name: "v6_address",
|
||||
in: yobj{
|
||||
"bind_host": "2001:db8::1",
|
||||
"bind_port": 8081,
|
||||
"web_session_ttl": 720,
|
||||
},
|
||||
want: yobj{
|
||||
"http": yobj{
|
||||
"address": "[2001:db8::1]:8081",
|
||||
"session_ttl": "720h",
|
||||
},
|
||||
"schema_version": newSchemaVer,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := upgradeSchema22to23(tc.in)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.want, tc.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeSchema23to24(t *testing.T) {
|
||||
const newSchemaVer = 24
|
||||
|
||||
testCases := []struct {
|
||||
in yobj
|
||||
want yobj
|
||||
name string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "empty",
|
||||
in: yobj{},
|
||||
want: yobj{
|
||||
"schema_version": newSchemaVer,
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "ok",
|
||||
in: yobj{
|
||||
"log_file": "/test/path.log",
|
||||
"log_max_backups": 1,
|
||||
"log_max_size": 2,
|
||||
"log_max_age": 3,
|
||||
"log_compress": true,
|
||||
"log_localtime": true,
|
||||
"verbose": true,
|
||||
},
|
||||
want: yobj{
|
||||
"log": yobj{
|
||||
"file": "/test/path.log",
|
||||
"max_backups": 1,
|
||||
"max_size": 2,
|
||||
"max_age": 3,
|
||||
"compress": true,
|
||||
"local_time": true,
|
||||
"verbose": true,
|
||||
},
|
||||
"schema_version": newSchemaVer,
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "invalid",
|
||||
in: yobj{
|
||||
"log_file": "/test/path.log",
|
||||
"log_max_backups": 1,
|
||||
"log_max_size": 2,
|
||||
"log_max_age": 3,
|
||||
"log_compress": "",
|
||||
"log_localtime": true,
|
||||
"verbose": true,
|
||||
},
|
||||
want: yobj{
|
||||
"log_file": "/test/path.log",
|
||||
"log_max_backups": 1,
|
||||
"log_max_size": 2,
|
||||
"log_max_age": 3,
|
||||
"log_compress": "",
|
||||
"log_localtime": true,
|
||||
"verbose": true,
|
||||
"schema_version": newSchemaVer,
|
||||
},
|
||||
wantErrMsg: "unexpected type of log_compress: string",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := upgradeSchema23to24(tc.in)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, tc.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,9 @@ func webCheckPortAvailable(port int) (ok bool) {
|
||||
return true
|
||||
}
|
||||
|
||||
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
|
||||
addrPort := netip.AddrPortFrom(config.HTTPConfig.Address.Addr(), uint16(port))
|
||||
|
||||
return aghnet.CheckPort("tcp", addrPort) == nil
|
||||
}
|
||||
|
||||
// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server
|
||||
|
||||
@@ -23,4 +23,5 @@ http:
|
||||
secure_addresses: []
|
||||
timeout: 5s
|
||||
force_https: true
|
||||
verbose: true
|
||||
log:
|
||||
verbose: true
|
||||
|
||||
@@ -24,6 +24,7 @@ enough.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `--check-config` breaking the configuration file ([#4067]).
|
||||
- Inconsistent application of `--work-dir/-w` ([#2598], [#2902]).
|
||||
- The order of `-v/--verbose` and `--version` being significant ([#2893]).
|
||||
|
||||
@@ -36,4 +37,5 @@ enough.
|
||||
[#2598]: https://github.com/AdguardTeam/AdGuardHome/issues/2598
|
||||
[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893
|
||||
[#2902]: https://github.com/AdguardTeam/AdGuardHome/issues/2902
|
||||
[#4067]: https://github.com/AdguardTeam/AdGuardHome/issues/4067
|
||||
[#5676]: https://github.com/AdguardTeam/AdGuardHome/issues/5676
|
||||
|
||||
@@ -16,11 +16,9 @@ import (
|
||||
)
|
||||
|
||||
// Main is the entry point of AdGuard Home.
|
||||
func Main(frontend fs.FS) {
|
||||
func Main(embeddedFrontend fs.FS) {
|
||||
start := time.Now()
|
||||
|
||||
// Initial Configuration
|
||||
|
||||
cmdName := os.Args[0]
|
||||
opts, err := parseOptions(cmdName, os.Args[1:])
|
||||
exitCode, needExit := processOptions(opts, cmdName, err)
|
||||
@@ -39,9 +37,17 @@ func Main(frontend fs.FS) {
|
||||
check(err)
|
||||
}
|
||||
|
||||
// Web Service
|
||||
frontend, err := frontendFromOpts(opts, embeddedFrontend)
|
||||
check(err)
|
||||
|
||||
confMgr, err := configmgr.New(opts.confFile, frontend, start)
|
||||
confMgrConf := &configmgr.Config{
|
||||
Frontend: frontend,
|
||||
WebAddr: opts.webAddr,
|
||||
Start: start,
|
||||
FileName: opts.confFile,
|
||||
}
|
||||
|
||||
confMgr, err := newConfigMgr(confMgrConf)
|
||||
check(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
@@ -53,9 +59,8 @@ func Main(frontend fs.FS) {
|
||||
check(err)
|
||||
|
||||
sigHdlr := newSignalHandler(
|
||||
opts.confFile,
|
||||
frontend,
|
||||
start,
|
||||
confMgrConf,
|
||||
opts.pidFile,
|
||||
web,
|
||||
dns,
|
||||
)
|
||||
@@ -73,6 +78,15 @@ func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), defaultTimeout)
|
||||
}
|
||||
|
||||
// newConfigMgr returns a new configuration manager using defaultTimeout as the
|
||||
// context timeout.
|
||||
func newConfigMgr(c *configmgr.Config) (m *configmgr.Manager, err error) {
|
||||
ctx, cancel := ctxWithDefaultTimeout()
|
||||
defer cancel()
|
||||
|
||||
return configmgr.New(ctx, c)
|
||||
}
|
||||
|
||||
// check is a simple error-checking helper. It must only be used within Main.
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
@@ -25,8 +29,6 @@ type options struct {
|
||||
logFile string
|
||||
|
||||
// pidFile is the path to the file where to store the PID.
|
||||
//
|
||||
// TODO(a.garipov): Use.
|
||||
pidFile string
|
||||
|
||||
// serviceAction is the service control action to perform:
|
||||
@@ -49,15 +51,12 @@ type options struct {
|
||||
// other configuration is read, so all relative paths are relative to it.
|
||||
workDir string
|
||||
|
||||
// webAddrs contains the addresses on which to serve the web UI.
|
||||
//
|
||||
// TODO(a.garipov): Use.
|
||||
webAddrs []netip.AddrPort
|
||||
// webAddr contains the address on which to serve the web UI.
|
||||
webAddr netip.AddrPort
|
||||
|
||||
// checkConfig, if true, instructs AdGuard Home to check the configuration
|
||||
// file and exit with a corresponding exit code.
|
||||
//
|
||||
// TODO(a.garipov): Use.
|
||||
// file, optionally print an error message to stdout, and exit with a
|
||||
// corresponding exit code.
|
||||
checkConfig bool
|
||||
|
||||
// disableUpdate, if true, prevents AdGuard Home from automatically checking
|
||||
@@ -103,7 +102,7 @@ const (
|
||||
pidFileIdx
|
||||
serviceActionIdx
|
||||
workDirIdx
|
||||
webAddrsIdx
|
||||
webAddrIdx
|
||||
checkConfigIdx
|
||||
disableUpdateIdx
|
||||
glinetModeIdx
|
||||
@@ -172,18 +171,17 @@ var commandLineOptions = []*commandLineOption{
|
||||
valueType: "path",
|
||||
},
|
||||
|
||||
webAddrsIdx: {
|
||||
defaultValue: []netip.AddrPort(nil),
|
||||
description: `Address(es) to serve the web UI on, in the host:port format. ` +
|
||||
`Can be used multiple times.`,
|
||||
long: "web-addr",
|
||||
short: "",
|
||||
valueType: "host:port",
|
||||
webAddrIdx: {
|
||||
defaultValue: netip.AddrPort{},
|
||||
description: `Address to serve the web UI on, in the host:port format.`,
|
||||
long: "web-addr",
|
||||
short: "",
|
||||
valueType: "host:port",
|
||||
},
|
||||
|
||||
checkConfigIdx: {
|
||||
defaultValue: false,
|
||||
description: "Check configuration and quit.",
|
||||
description: "Check configuration, print errors to stdout, and quit.",
|
||||
long: "check-config",
|
||||
short: "",
|
||||
valueType: "",
|
||||
@@ -258,7 +256,7 @@ func parseOptions(cmdName string, args []string) (opts *options, err error) {
|
||||
pidFileIdx: &opts.pidFile,
|
||||
serviceActionIdx: &opts.serviceAction,
|
||||
workDirIdx: &opts.workDir,
|
||||
webAddrsIdx: &opts.webAddrs,
|
||||
webAddrIdx: &opts.webAddr,
|
||||
checkConfigIdx: &opts.checkConfig,
|
||||
disableUpdateIdx: &opts.disableUpdate,
|
||||
glinetModeIdx: &opts.glinetMode,
|
||||
@@ -291,23 +289,16 @@ func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) {
|
||||
if o.short != "" {
|
||||
flags.StringVar(fieldPtr, o.short, o.defaultValue.(string), o.description)
|
||||
}
|
||||
case *[]netip.AddrPort:
|
||||
flags.Func(o.long, o.description, func(s string) (err error) {
|
||||
addr, err := netip.ParseAddrPort(s)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
*fieldPtr = append(*fieldPtr, addr)
|
||||
|
||||
return nil
|
||||
})
|
||||
case *bool:
|
||||
flags.BoolVar(fieldPtr, o.long, o.defaultValue.(bool), o.description)
|
||||
if o.short != "" {
|
||||
flags.BoolVar(fieldPtr, o.short, o.defaultValue.(bool), o.description)
|
||||
}
|
||||
case encoding.TextUnmarshaler:
|
||||
flags.TextVar(fieldPtr, o.long, o.defaultValue.(encoding.TextMarshaler), o.description)
|
||||
if o.short != "" {
|
||||
flags.TextVar(fieldPtr, o.short, o.defaultValue.(encoding.TextMarshaler), o.description)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected field pointer type %T", fieldPtr))
|
||||
}
|
||||
@@ -380,13 +371,13 @@ func processOptions(
|
||||
) (exitCode int, needExit bool) {
|
||||
if parseErr != nil {
|
||||
// Assume that usage has already been printed.
|
||||
return 2, true
|
||||
return statusArgumentError, true
|
||||
}
|
||||
|
||||
if opts.help {
|
||||
usage(cmdName, os.Stdout)
|
||||
|
||||
return 0, true
|
||||
return statusSuccess, true
|
||||
}
|
||||
|
||||
if opts.version {
|
||||
@@ -396,8 +387,32 @@ func processOptions(
|
||||
fmt.Printf("AdGuard Home %s\n", version.Version())
|
||||
}
|
||||
|
||||
return 0, true
|
||||
return statusSuccess, true
|
||||
}
|
||||
|
||||
if opts.checkConfig {
|
||||
err := configmgr.Validate(opts.confFile)
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(os.Stdout, err.Error()+"\n")
|
||||
|
||||
return statusError, true
|
||||
}
|
||||
|
||||
return statusSuccess, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// frontendFromOpts returns the frontend to use based on the options.
|
||||
func frontendFromOpts(opts *options, embeddedFrontend fs.FS) (frontend fs.FS, err error) {
|
||||
const frontendSubdir = "build/static"
|
||||
|
||||
if opts.localFrontend {
|
||||
log.Info("warning: using local frontend files")
|
||||
|
||||
return os.DirFS(frontendSubdir), nil
|
||||
}
|
||||
|
||||
return fs.Sub(embeddedFrontend, frontendSubdir)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/google/renameio/maybe"
|
||||
)
|
||||
|
||||
// signalHandler processes incoming signals and shuts services down.
|
||||
type signalHandler struct {
|
||||
// confMgrConf contains the configuration parameters for the configuration
|
||||
// manager.
|
||||
confMgrConf *configmgr.Config
|
||||
|
||||
// signal is the channel to which OS signals are sent.
|
||||
signal chan os.Signal
|
||||
|
||||
// confFile is the path to the configuration file.
|
||||
confFile string
|
||||
|
||||
// frontend is the filesystem with the frontend and other statically
|
||||
// compiled files.
|
||||
frontend fs.FS
|
||||
|
||||
// start is the time at which AdGuard Home has been started.
|
||||
start time.Time
|
||||
// pidFile is the path to the file where to store the PID, if any.
|
||||
pidFile string
|
||||
|
||||
// services are the services that are shut down before application exiting.
|
||||
services []agh.Service
|
||||
@@ -34,6 +31,8 @@ type signalHandler struct {
|
||||
func (h *signalHandler) handle() {
|
||||
defer log.OnPanic("signalHandler.handle")
|
||||
|
||||
h.writePID()
|
||||
|
||||
for sig := range h.signal {
|
||||
log.Info("sighdlr: received signal %q", sig)
|
||||
|
||||
@@ -41,6 +40,8 @@ func (h *signalHandler) handle() {
|
||||
h.reconfigure()
|
||||
} else if aghos.IsShutdownSignal(sig) {
|
||||
status := h.shutdown()
|
||||
h.removePID()
|
||||
|
||||
log.Info("sighdlr: exiting with status %d", status)
|
||||
|
||||
os.Exit(status)
|
||||
@@ -63,7 +64,7 @@ func (h *signalHandler) reconfigure() {
|
||||
// reconfigured without the full shutdown, and the error handling is
|
||||
// currently not the best.
|
||||
|
||||
confMgr, err := configmgr.New(h.confFile, h.frontend, h.start)
|
||||
confMgr, err := newConfigMgr(h.confMgrConf)
|
||||
check(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
@@ -84,8 +85,9 @@ func (h *signalHandler) reconfigure() {
|
||||
|
||||
// Exit status constants.
|
||||
const (
|
||||
statusSuccess = 0
|
||||
statusError = 1
|
||||
statusSuccess = 0
|
||||
statusError = 1
|
||||
statusArgumentError = 2
|
||||
)
|
||||
|
||||
// shutdown gracefully shuts down all services.
|
||||
@@ -109,17 +111,15 @@ func (h *signalHandler) shutdown() (status int) {
|
||||
|
||||
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
||||
func newSignalHandler(
|
||||
confFile string,
|
||||
frontend fs.FS,
|
||||
start time.Time,
|
||||
confMgrConf *configmgr.Config,
|
||||
pidFile string,
|
||||
svcs ...agh.Service,
|
||||
) (h *signalHandler) {
|
||||
h = &signalHandler{
|
||||
signal: make(chan os.Signal, 1),
|
||||
confFile: confFile,
|
||||
frontend: frontend,
|
||||
start: start,
|
||||
services: svcs,
|
||||
confMgrConf: confMgrConf,
|
||||
signal: make(chan os.Signal, 1),
|
||||
pidFile: pidFile,
|
||||
services: svcs,
|
||||
}
|
||||
|
||||
aghos.NotifyShutdownSignal(h.signal)
|
||||
@@ -127,3 +127,41 @@ func newSignalHandler(
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// writePID writes the PID to the file, if needed. Any errors are reported to
|
||||
// log.
|
||||
func (h *signalHandler) writePID() {
|
||||
if h.pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Use 8, since most PIDs will fit.
|
||||
data := make([]byte, 0, 8)
|
||||
data = strconv.AppendInt(data, int64(os.Getpid()), 10)
|
||||
data = append(data, '\n')
|
||||
|
||||
err := maybe.WriteFile(h.pidFile, data, 0o644)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: writing pidfile: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("sighdlr: wrote pid to %q", h.pidFile)
|
||||
}
|
||||
|
||||
// removePID removes the PID file, if any.
|
||||
func (h *signalHandler) removePID() {
|
||||
if h.pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := os.Remove(h.pidFile)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: removing pidfile: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("sighdlr: removed pid at %q", h.pidFile)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package configmgr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
)
|
||||
|
||||
@@ -12,16 +14,49 @@ import (
|
||||
type config struct {
|
||||
DNS *dnsConfig `yaml:"dns"`
|
||||
HTTP *httpConfig `yaml:"http"`
|
||||
Log *logConfig `yaml:"log"`
|
||||
// TODO(a.garipov): Use.
|
||||
SchemaVersion int `yaml:"schema_version"`
|
||||
// TODO(a.garipov): Use.
|
||||
DebugPprof bool `yaml:"debug_pprof"`
|
||||
Verbose bool `yaml:"verbose"`
|
||||
}
|
||||
|
||||
const errNoConf errors.Error = "configuration not found"
|
||||
|
||||
// validate returns an error if the configuration structure is invalid.
|
||||
func (c *config) validate() (err error) {
|
||||
if c == nil {
|
||||
return errNoConf
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Add more validations.
|
||||
|
||||
// Keep this in the same order as the fields in the config.
|
||||
validators := []struct {
|
||||
validate func() (err error)
|
||||
name string
|
||||
}{{
|
||||
validate: c.DNS.validate,
|
||||
name: "dns",
|
||||
}, {
|
||||
validate: c.HTTP.validate,
|
||||
name: "http",
|
||||
}, {
|
||||
validate: c.Log.validate,
|
||||
name: "log",
|
||||
}}
|
||||
|
||||
for _, v := range validators {
|
||||
err = v.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", v.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dnsConfig is the on-disk DNS configuration.
|
||||
//
|
||||
// TODO(a.garipov): Validate.
|
||||
type dnsConfig struct {
|
||||
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||
BootstrapDNS []string `yaml:"bootstrap_dns"`
|
||||
@@ -32,12 +67,57 @@ type dnsConfig struct {
|
||||
UseDNS64 bool `yaml:"use_dns64"`
|
||||
}
|
||||
|
||||
// httpConfig is the on-disk web API configuration.
|
||||
// validate returns an error if the DNS configuration structure is invalid.
|
||||
//
|
||||
// TODO(a.garipov): Validate.
|
||||
// TODO(a.garipov): Add more validations.
|
||||
func (c *dnsConfig) validate() (err error) {
|
||||
// TODO(a.garipov): Add more validations.
|
||||
switch {
|
||||
case c == nil:
|
||||
return errNoConf
|
||||
case c.UpstreamTimeout.Duration <= 0:
|
||||
return newMustBePositiveError("upstream_timeout", c.UpstreamTimeout)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// httpConfig is the on-disk web API configuration.
|
||||
type httpConfig struct {
|
||||
// TODO(a.garipov): Document the configuration change.
|
||||
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
|
||||
Timeout timeutil.Duration `yaml:"timeout"`
|
||||
ForceHTTPS bool `yaml:"force_https"`
|
||||
}
|
||||
|
||||
// validate returns an error if the HTTP configuration structure is invalid.
|
||||
//
|
||||
// TODO(a.garipov): Add more validations.
|
||||
func (c *httpConfig) validate() (err error) {
|
||||
switch {
|
||||
case c == nil:
|
||||
return errNoConf
|
||||
case c.Timeout.Duration <= 0:
|
||||
return newMustBePositiveError("timeout", c.Timeout)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// logConfig is the on-disk web API configuration.
|
||||
type logConfig struct {
|
||||
// TODO(a.garipov): Use.
|
||||
Verbose bool `yaml:"verbose"`
|
||||
}
|
||||
|
||||
// validate returns an error if the HTTP configuration structure is invalid.
|
||||
//
|
||||
// TODO(a.garipov): Add more validations.
|
||||
func (c *logConfig) validate() (err error) {
|
||||
if c == nil {
|
||||
return errNoConf
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// Package configmgr defines the AdGuard Home on-disk configuration entities and
|
||||
// configuration manager.
|
||||
//
|
||||
// TODO(a.garipov): Add tests.
|
||||
package configmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -14,6 +17,10 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/google/renameio/maybe"
|
||||
"golang.org/x/exp/slices"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -21,6 +28,8 @@ import (
|
||||
|
||||
// Manager handles full and partial changes in the configuration, persisting
|
||||
// them to disk if necessary.
|
||||
//
|
||||
// TODO(a.garipov): Support missing configs and default values.
|
||||
type Manager struct {
|
||||
// updMu makes sure that at most one reconfiguration is performed at a time.
|
||||
// updMu protects all fields below.
|
||||
@@ -39,17 +48,69 @@ type Manager struct {
|
||||
fileName string
|
||||
}
|
||||
|
||||
// Validate returns an error if the configuration file with the given name does
|
||||
// not exist or is invalid.
|
||||
func Validate(fileName string) (err error) {
|
||||
conf, err := read(fileName)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return conf.validate()
|
||||
}
|
||||
|
||||
// Config contains the configuration parameters for the configuration manager.
|
||||
type Config struct {
|
||||
// Frontend is the filesystem with the frontend files.
|
||||
Frontend fs.FS
|
||||
|
||||
// WebAddr is the initial or override address for the Web UI. It is not
|
||||
// written to the configuration file.
|
||||
WebAddr netip.AddrPort
|
||||
|
||||
// Start is the time of start of AdGuard Home.
|
||||
Start time.Time
|
||||
|
||||
// FileName is the path to the configuration file.
|
||||
FileName string
|
||||
}
|
||||
|
||||
// New creates a new *Manager that persists changes to the file pointed to by
|
||||
// fileName. It reads the configuration file and populates the service fields.
|
||||
// start is the startup time of AdGuard Home.
|
||||
func New(
|
||||
fileName string,
|
||||
frontend fs.FS,
|
||||
start time.Time,
|
||||
) (m *Manager, err error) {
|
||||
// c.FileName. It reads the configuration file and populates the service
|
||||
// fields. c must not be nil.
|
||||
func New(ctx context.Context, c *Config) (m *Manager, err error) {
|
||||
conf, err := read(c.FileName)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conf.validate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating config: %w", err)
|
||||
}
|
||||
|
||||
m = &Manager{
|
||||
updMu: &sync.RWMutex{},
|
||||
current: conf,
|
||||
fileName: c.FileName,
|
||||
}
|
||||
|
||||
err = m.assemble(ctx, conf, c.Frontend, c.WebAddr, c.Start)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating config manager: %w", err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// read reads and decodes configuration from the provided filename.
|
||||
func read(fileName string) (conf *config, err error) {
|
||||
defer func() { err = errors.Annotate(err, "reading config: %w") }()
|
||||
|
||||
conf := &config{}
|
||||
conf = &config{}
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
@@ -63,27 +124,7 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Validate the configuration structure. Return an error
|
||||
// if it's incorrect.
|
||||
|
||||
m = &Manager{
|
||||
updMu: &sync.RWMutex{},
|
||||
current: conf,
|
||||
fileName: fileName,
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Get the context with the timeout from the arguments?
|
||||
const assemblyTimeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = m.assemble(ctx, conf, frontend, start)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// assemble creates all services and puts them into the corresponding fields.
|
||||
@@ -92,6 +133,7 @@ func (m *Manager) assemble(
|
||||
ctx context.Context,
|
||||
conf *config,
|
||||
frontend fs.FS,
|
||||
webAddr netip.AddrPort,
|
||||
start time.Time,
|
||||
) (err error) {
|
||||
dnsConf := &dnssvc.Config{
|
||||
@@ -116,6 +158,7 @@ func (m *Manager) assemble(
|
||||
Start: start,
|
||||
Addresses: conf.HTTP.Addresses,
|
||||
SecureAddresses: conf.HTTP.SecureAddresses,
|
||||
OverrideAddress: webAddr,
|
||||
Timeout: conf.HTTP.Timeout.Duration,
|
||||
ForceHTTPS: conf.HTTP.ForceHTTPS,
|
||||
}
|
||||
@@ -128,6 +171,23 @@ func (m *Manager) assemble(
|
||||
return nil
|
||||
}
|
||||
|
||||
// write writes the current configuration to disk.
|
||||
func (m *Manager) write() (err error) {
|
||||
b, err := yaml.Marshal(m.current)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding: %w", err)
|
||||
}
|
||||
|
||||
err = maybe.WriteFile(m.fileName, b, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing: %w", err)
|
||||
}
|
||||
|
||||
log.Info("configmgr: written to %q", m.fileName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DNS returns the current DNS service. It is safe for concurrent use.
|
||||
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
m.updMu.RLock()
|
||||
@@ -150,7 +210,9 @@ func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return fmt.Errorf("reassembling dnssvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
m.updateCurrentDNS(c)
|
||||
|
||||
return m.write()
|
||||
}
|
||||
|
||||
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
||||
@@ -172,6 +234,17 @@ func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateCurrentDNS updates the DNS configuration in the current config.
|
||||
func (m *Manager) updateCurrentDNS(c *dnssvc.Config) {
|
||||
m.current.DNS.Addresses = slices.Clone(c.Addresses)
|
||||
m.current.DNS.BootstrapDNS = slices.Clone(c.BootstrapServers)
|
||||
m.current.DNS.UpstreamDNS = slices.Clone(c.UpstreamServers)
|
||||
m.current.DNS.DNS64Prefixes = slices.Clone(c.DNS64Prefixes)
|
||||
m.current.DNS.UpstreamTimeout = timeutil.Duration{Duration: c.UpstreamTimeout}
|
||||
m.current.DNS.BootstrapPreferIPv6 = c.BootstrapPreferIPv6
|
||||
m.current.DNS.UseDNS64 = c.UseDNS64
|
||||
}
|
||||
|
||||
// Web returns the current web service. It is safe for concurrent use.
|
||||
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
|
||||
m.updMu.RLock()
|
||||
@@ -194,7 +267,9 @@ func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
return fmt.Errorf("reassembling websvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
m.updateCurrentWeb(c)
|
||||
|
||||
return m.write()
|
||||
}
|
||||
|
||||
// updateWeb recreates the web service. m.upd is expected to be locked.
|
||||
@@ -213,3 +288,11 @@ func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateCurrentWeb updates the web configuration in the current config.
|
||||
func (m *Manager) updateCurrentWeb(c *websvc.Config) {
|
||||
m.current.HTTP.Addresses = slices.Clone(c.Addresses)
|
||||
m.current.HTTP.SecureAddresses = slices.Clone(c.SecureAddresses)
|
||||
m.current.HTTP.Timeout = timeutil.Duration{Duration: c.Timeout}
|
||||
m.current.HTTP.ForceHTTPS = c.ForceHTTPS
|
||||
}
|
||||
|
||||
27
internal/next/configmgr/error.go
Normal file
27
internal/next/configmgr/error.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package configmgr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// numberOrDuration is the constraint for integer types along with
|
||||
// timeutil.Duration.
|
||||
type numberOrDuration interface {
|
||||
constraints.Integer | timeutil.Duration
|
||||
}
|
||||
|
||||
// newMustBePositiveError returns an error about the value that must be positive
|
||||
// but isn't. prop is the name of the property to mention in the error message.
|
||||
//
|
||||
// TODO(a.garipov): Consider moving such helpers to golibs and use in AdGuardDNS
|
||||
// as well.
|
||||
func newMustBePositiveError[T numberOrDuration](prop string, v T) (err error) {
|
||||
if s, ok := any(v).(fmt.Stringer); ok {
|
||||
return fmt.Errorf("%s must be positive, got %s", prop, s)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s must be positive, got %d", prop, v)
|
||||
}
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/testutil/fakenet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWaitListener_Accept(t *testing.T) {
|
||||
var accepted atomic.Bool
|
||||
var l net.Listener = &aghtest.Listener{
|
||||
var l net.Listener = &fakenet.Listener{
|
||||
OnAccept: func() (conn net.Conn, err error) {
|
||||
accepted.Store(true)
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ type Config struct {
|
||||
// Start is the time of start of AdGuard Home.
|
||||
Start time.Time
|
||||
|
||||
// OverrideAddress is the initial or override address for the HTTP API. If
|
||||
// set, it is used instead of [Addresses] and [SecureAddresses].
|
||||
OverrideAddress netip.AddrPort
|
||||
|
||||
// Addresses are the addresses on which to serve the plain HTTP API.
|
||||
Addresses []netip.AddrPort
|
||||
|
||||
@@ -71,13 +75,14 @@ type Config struct {
|
||||
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||
// [agh.Service] that does nothing.
|
||||
type Service struct {
|
||||
confMgr ConfigManager
|
||||
frontend fs.FS
|
||||
tls *tls.Config
|
||||
start time.Time
|
||||
servers []*http.Server
|
||||
timeout time.Duration
|
||||
forceHTTPS bool
|
||||
confMgr ConfigManager
|
||||
frontend fs.FS
|
||||
tls *tls.Config
|
||||
start time.Time
|
||||
overrideAddr netip.AddrPort
|
||||
servers []*http.Server
|
||||
timeout time.Duration
|
||||
forceHTTPS bool
|
||||
}
|
||||
|
||||
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||
@@ -91,54 +96,60 @@ func New(c *Config) (svc *Service, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
frontend, err := fs.Sub(c.Frontend, "build/static")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("frontend fs: %w", err)
|
||||
}
|
||||
|
||||
svc = &Service{
|
||||
confMgr: c.ConfigManager,
|
||||
frontend: frontend,
|
||||
tls: c.TLS,
|
||||
start: c.Start,
|
||||
timeout: c.Timeout,
|
||||
forceHTTPS: c.ForceHTTPS,
|
||||
confMgr: c.ConfigManager,
|
||||
frontend: c.Frontend,
|
||||
tls: c.TLS,
|
||||
start: c.Start,
|
||||
overrideAddr: c.OverrideAddress,
|
||||
timeout: c.Timeout,
|
||||
forceHTTPS: c.ForceHTTPS,
|
||||
}
|
||||
|
||||
mux := newMux(svc)
|
||||
|
||||
for _, a := range c.Addresses {
|
||||
addr := a.String()
|
||||
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
|
||||
svc.servers = append(svc.servers, &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ErrorLog: errLog,
|
||||
ReadTimeout: c.Timeout,
|
||||
WriteTimeout: c.Timeout,
|
||||
IdleTimeout: c.Timeout,
|
||||
ReadHeaderTimeout: c.Timeout,
|
||||
})
|
||||
}
|
||||
if svc.overrideAddr != (netip.AddrPort{}) {
|
||||
svc.servers = []*http.Server{newSrv(svc.overrideAddr, nil, mux, c.Timeout)}
|
||||
} else {
|
||||
for _, a := range c.Addresses {
|
||||
svc.servers = append(svc.servers, newSrv(a, nil, mux, c.Timeout))
|
||||
}
|
||||
|
||||
for _, a := range c.SecureAddresses {
|
||||
addr := a.String()
|
||||
errLog := log.StdLog("websvc: https: "+addr, log.ERROR)
|
||||
svc.servers = append(svc.servers, &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
TLSConfig: c.TLS,
|
||||
ErrorLog: errLog,
|
||||
ReadTimeout: c.Timeout,
|
||||
WriteTimeout: c.Timeout,
|
||||
IdleTimeout: c.Timeout,
|
||||
ReadHeaderTimeout: c.Timeout,
|
||||
})
|
||||
for _, a := range c.SecureAddresses {
|
||||
svc.servers = append(svc.servers, newSrv(a, c.TLS, mux, c.Timeout))
|
||||
}
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// newSrv returns a new *http.Server with the given parameters.
|
||||
func newSrv(
|
||||
addr netip.AddrPort,
|
||||
tlsConf *tls.Config,
|
||||
h http.Handler,
|
||||
timeout time.Duration,
|
||||
) (srv *http.Server) {
|
||||
addrStr := addr.String()
|
||||
srv = &http.Server{
|
||||
Addr: addrStr,
|
||||
Handler: h,
|
||||
TLSConfig: tlsConf,
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
IdleTimeout: timeout,
|
||||
ReadHeaderTimeout: timeout,
|
||||
}
|
||||
|
||||
if tlsConf == nil {
|
||||
srv.ErrorLog = log.StdLog("websvc: plain http: "+addrStr, log.ERROR)
|
||||
} else {
|
||||
srv.ErrorLog = log.StdLog("websvc: https: "+addrStr, log.ERROR)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
// newMux returns a new HTTP request multiplexer for the AdGuard Home web
|
||||
// service.
|
||||
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
@@ -205,23 +216,23 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
// ":0" addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||
for _, srv := range svc.servers {
|
||||
addrPort, err := netip.ParseAddrPort(srv.Addr)
|
||||
if err != nil {
|
||||
// Technically shouldn't happen, since all servers must have a valid
|
||||
// address.
|
||||
panic(fmt.Errorf("websvc: server %q: bad address: %w", srv.Addr, err))
|
||||
}
|
||||
if svc.overrideAddr != (netip.AddrPort{}) {
|
||||
return []netip.AddrPort{svc.overrideAddr}, nil
|
||||
}
|
||||
|
||||
// srv.Serve will set TLSConfig to an almost empty value, so, instead of
|
||||
// relying only on the nilness of TLSConfig, check the length of the
|
||||
for _, srv := range svc.servers {
|
||||
// Use MustParseAddrPort, since no errors should technically happen
|
||||
// here, because all servers must have a valid address.
|
||||
addrPort := netip.MustParseAddrPort(srv.Addr)
|
||||
|
||||
// [srv.Serve] will set TLSConfig to an almost empty value, so, instead
|
||||
// of relying only on the nilness of TLSConfig, check the length of the
|
||||
// certificates field as well.
|
||||
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
|
||||
addrs = append(addrs, addrPort)
|
||||
} else {
|
||||
secureAddrs = append(secureAddrs, addrPort)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return addrs, secureAddrs
|
||||
|
||||
132
internal/rdns/rdns.go
Normal file
132
internal/rdns/rdns.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Package rdns processes reverse DNS lookup queries.
|
||||
package rdns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/bluele/gcache"
|
||||
)
|
||||
|
||||
// Interface processes rDNS queries.
|
||||
type Interface interface {
|
||||
// Process makes rDNS request and returns domain name. changed indicates
|
||||
// that domain name was updated since last request.
|
||||
Process(ip netip.Addr) (host string, changed bool)
|
||||
}
|
||||
|
||||
// Empty is an empty [Inteface] implementation which does nothing.
|
||||
type Empty struct{}
|
||||
|
||||
// type check
|
||||
var _ Interface = (*Empty)(nil)
|
||||
|
||||
// Process implements the [Interface] interface for Empty.
|
||||
func (Empty) Process(_ netip.Addr) (host string, changed bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Exchanger is a resolver for clients' addresses.
|
||||
type Exchanger interface {
|
||||
// Exchange tries to resolve the ip in a suitable way, i.e. either as local
|
||||
// or as external.
|
||||
Exchange(ip netip.Addr) (host string, err error)
|
||||
}
|
||||
|
||||
// Config is the configuration structure for Default.
|
||||
type Config struct {
|
||||
// Exchanger resolves IP addresses to domain names.
|
||||
Exchanger Exchanger
|
||||
|
||||
// CacheSize is the maximum size of the cache. It must be greater than
|
||||
// zero.
|
||||
CacheSize int
|
||||
|
||||
// CacheTTL is the Time to Live duration for cached IP addresses.
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// Default is the default rDNS query processor.
|
||||
type Default struct {
|
||||
// cache is the cache containing IP addresses of clients. An active IP
|
||||
// address is resolved once again after it expires. If IP address couldn't
|
||||
// be resolved, it stays here for some time to prevent further attempts to
|
||||
// resolve the same IP.
|
||||
cache gcache.Cache
|
||||
|
||||
// exchanger resolves IP addresses to domain names.
|
||||
exchanger Exchanger
|
||||
|
||||
// cacheTTL is the Time to Live duration for cached IP addresses.
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// New returns a new default rDNS query processor. conf must not be nil.
|
||||
func New(conf *Config) (r *Default) {
|
||||
return &Default{
|
||||
cache: gcache.New(conf.CacheSize).LRU().Build(),
|
||||
exchanger: conf.Exchanger,
|
||||
cacheTTL: conf.CacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Interface = (*Default)(nil)
|
||||
|
||||
// Process implements the [Interface] interface for Default.
|
||||
func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
|
||||
fromCache, expired := r.findInCache(ip)
|
||||
if !expired {
|
||||
return fromCache, false
|
||||
}
|
||||
|
||||
host, err := r.exchanger.Exchange(ip)
|
||||
if err != nil {
|
||||
log.Debug("rdns: resolving %q: %s", ip, err)
|
||||
}
|
||||
|
||||
item := &cacheItem{
|
||||
expiry: time.Now().Add(r.cacheTTL),
|
||||
host: host,
|
||||
}
|
||||
|
||||
err = r.cache.Set(ip, item)
|
||||
if err != nil {
|
||||
log.Debug("rdns: cache: adding item %q: %s", ip, err)
|
||||
}
|
||||
|
||||
return host, fromCache == "" || host != fromCache
|
||||
}
|
||||
|
||||
// findInCache finds domain name in the cache. expired is true if host is not
|
||||
// valid anymore.
|
||||
func (r *Default) findInCache(ip netip.Addr) (host string, expired bool) {
|
||||
val, err := r.cache.Get(ip)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gcache.KeyNotFoundError) {
|
||||
log.Debug("rdns: cache: retrieving %q: %s", ip, err)
|
||||
}
|
||||
|
||||
return "", true
|
||||
}
|
||||
|
||||
item, ok := val.(*cacheItem)
|
||||
if !ok {
|
||||
log.Debug("rdns: cache: %q bad type %T", ip, val)
|
||||
|
||||
return "", true
|
||||
}
|
||||
|
||||
return item.host, time.Now().After(item.expiry)
|
||||
}
|
||||
|
||||
// cacheItem represents an item that we will store in the cache.
|
||||
type cacheItem struct {
|
||||
// expiry is the time when cacheItem will expire.
|
||||
expiry time.Time
|
||||
|
||||
// host is the domain name of a runtime client.
|
||||
host string
|
||||
}
|
||||
105
internal/rdns/rdns_test.go
Normal file
105
internal/rdns/rdns_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package rdns_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fakeRDNSExchanger is a mock [rdns.Exchanger] implementation for tests.
|
||||
type fakeRDNSExchanger struct {
|
||||
OnExchange func(ip netip.Addr) (host string, err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ rdns.Exchanger = (*fakeRDNSExchanger)(nil)
|
||||
|
||||
// Exchange implements [rdns.Exchanger] interface for *fakeRDNSExchanger.
|
||||
func (e *fakeRDNSExchanger) Exchange(ip netip.Addr) (host string, err error) {
|
||||
return e.OnExchange(ip)
|
||||
}
|
||||
|
||||
func TestDefault_Process(t *testing.T) {
|
||||
ip1 := netip.MustParseAddr("1.2.3.4")
|
||||
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
ip2 := netip.MustParseAddr("4.3.2.1")
|
||||
revAddr2, err := netutil.IPToReversedAddr(ip2.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
localIP := netip.MustParseAddr("192.168.0.1")
|
||||
localRevAddr1, err := netutil.IPToReversedAddr(localIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &rdns.Config{
|
||||
CacheSize: 100,
|
||||
CacheTTL: time.Hour,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
addr netip.Addr
|
||||
want string
|
||||
}{{
|
||||
name: "first",
|
||||
addr: ip1,
|
||||
want: revAddr1,
|
||||
}, {
|
||||
name: "second",
|
||||
addr: ip2,
|
||||
want: revAddr2,
|
||||
}, {
|
||||
name: "empty",
|
||||
addr: netip.MustParseAddr("0.0.0.0"),
|
||||
want: "",
|
||||
}, {
|
||||
name: "private",
|
||||
addr: localIP,
|
||||
want: localRevAddr1,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hit := 0
|
||||
onExchange := func(ip netip.Addr) (host string, err error) {
|
||||
hit++
|
||||
|
||||
switch ip {
|
||||
case ip1:
|
||||
return revAddr1, nil
|
||||
case ip2:
|
||||
return revAddr2, nil
|
||||
case localIP:
|
||||
return localRevAddr1, nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
exchanger := &fakeRDNSExchanger{
|
||||
OnExchange: onExchange,
|
||||
}
|
||||
|
||||
config.Exchanger = exchanger
|
||||
r := rdns.New(config)
|
||||
|
||||
got, changed := r.Process(tc.addr)
|
||||
require.True(t, changed)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, 1, hit)
|
||||
|
||||
// From cache.
|
||||
got, changed = r.Process(tc.addr)
|
||||
require.False(t, changed)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, 1, hit)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,10 @@ require (
|
||||
github.com/kyoh86/looppointer v0.2.1
|
||||
github.com/securego/gosec/v2 v2.16.0
|
||||
github.com/uudashr/gocognit v1.0.6
|
||||
golang.org/x/tools v0.10.0
|
||||
golang.org/x/vuln v0.1.0
|
||||
honnef.co/go/tools v0.4.3
|
||||
golang.org/x/tools v0.11.0
|
||||
golang.org/x/vuln v0.2.0
|
||||
// TODO(a.garipov): Return to tagged releases once a new one appears.
|
||||
honnef.co/go/tools v0.5.0-0.dev.0.20230709092525-bc759185c5ee
|
||||
mvdan.cc/gofumpt v0.5.0
|
||||
mvdan.cc/unparam v0.0.0-20230610194454-9ea02bef9868
|
||||
)
|
||||
@@ -26,9 +27,9 @@ require (
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20230711023510-fffb14384f22 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
@@ -53,21 +52,21 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df h1:jfUqBujZx2dktJVEmZpCkyngz7MWrVv1y9kLOqFNsqw=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230711023510-fffb14384f22 h1:e8iSCQYXZ4EB6q3kIfy2fgPFTvDbozqzRe4OuIOyrL4=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230711023510-fffb14384f22/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -83,8 +82,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -97,10 +96,10 @@ golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4X
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||
golang.org/x/vuln v0.1.0 h1:9GRdj6wAIkDrsMevuolY+SXERPjQPp2P1ysYA0jpZe0=
|
||||
golang.org/x/vuln v0.1.0/go.mod h1:/YuzZYjGbwB8y19CisAppfyw3uTZnuCz3r+qgx/QRzU=
|
||||
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
|
||||
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
|
||||
golang.org/x/vuln v0.2.0 h1:Dlz47lW0pvPHU7tnb10S8vbMn9GnV2B6eyT7Tem5XBI=
|
||||
golang.org/x/vuln v0.2.0/go.mod h1:V0eyhHwaAaHrt42J9bgrN6rd12f6GU4T0Lu0ex2wDg4=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -108,8 +107,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw=
|
||||
honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
|
||||
honnef.co/go/tools v0.5.0-0.dev.0.20230709092525-bc759185c5ee h1:mpyvMqtlVZTwEv78QL3S2ZDTMHMO1fgNwr2kC7+K7oU=
|
||||
honnef.co/go/tools v0.5.0-0.dev.0.20230709092525-bc759185c5ee/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
|
||||
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
|
||||
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
|
||||
mvdan.cc/unparam v0.0.0-20230610194454-9ea02bef9868 h1:F4Q7pXcrU9UiU1fq0ZWqSOxKjNAteRuDr7JDk7uVLRQ=
|
||||
|
||||
@@ -288,6 +288,16 @@ func (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed
|
||||
return wi, false
|
||||
}
|
||||
|
||||
return w.requestInfo(ctx, ip, wi)
|
||||
}
|
||||
|
||||
// requestInfo makes WHOIS request and returns WHOIS info. changed is false if
|
||||
// received information is equal to cached.
|
||||
func (w *Default) requestInfo(
|
||||
ctx context.Context,
|
||||
ip netip.Addr,
|
||||
cached *Info,
|
||||
) (wi *Info, changed bool) {
|
||||
var info Info
|
||||
|
||||
defer func() {
|
||||
@@ -311,12 +321,14 @@ func (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed
|
||||
Orgname: kv["orgname"],
|
||||
}
|
||||
|
||||
changed = cached == nil || info != *cached
|
||||
|
||||
// Don't return an empty struct so that the frontend doesn't get confused.
|
||||
if (info == Info{}) {
|
||||
return nil, true
|
||||
return nil, changed
|
||||
}
|
||||
|
||||
return &info, wi == nil || info != *wi
|
||||
return &info, changed
|
||||
}
|
||||
|
||||
// findInCache finds Info in the cache. expired indicates that Info is valid.
|
||||
|
||||
@@ -18,7 +18,7 @@ Run `make init` from the project root.
|
||||
|
||||
|
||||
|
||||
## `make/`: Makefile Scripts
|
||||
## `make/`: Makefile scripts
|
||||
|
||||
The release channels are: `development` (the default), `edge`, `beta`, and
|
||||
`release`. If verbosity levels aren't documented here, there are only two: `0`,
|
||||
@@ -26,7 +26,7 @@ don't print anything, and `1`, be verbose.
|
||||
|
||||
|
||||
|
||||
### `build-docker.sh`: Build A Multi-Architecture Docker Image
|
||||
### `build-docker.sh`: Build a multi-architecture Docker image
|
||||
|
||||
Required environment:
|
||||
|
||||
@@ -51,7 +51,7 @@ Optional environment:
|
||||
|
||||
|
||||
|
||||
### `build-release.sh`: Build A Release For All Platforms
|
||||
### `build-release.sh`: Build a release for all platforms
|
||||
|
||||
Required environment:
|
||||
|
||||
@@ -101,7 +101,22 @@ Required environment:
|
||||
|
||||
|
||||
|
||||
### `go-build.sh`: Build The Backend
|
||||
### `go-bench.sh`: Run backend benchmarks
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
* `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is
|
||||
`--timeout=30s`.
|
||||
|
||||
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
|
||||
Go package that is processed. `2` also shows subcommands and environment.
|
||||
The default value is `0`, don't be verbose.
|
||||
|
||||
|
||||
|
||||
### `go-build.sh`: Build the backend
|
||||
|
||||
Optional environment:
|
||||
|
||||
@@ -135,7 +150,7 @@ Required environment:
|
||||
|
||||
|
||||
|
||||
### `go-deps.sh`: Install Backend Dependencies
|
||||
### `go-deps.sh`: Install backend dependencies
|
||||
|
||||
Optional environment:
|
||||
|
||||
@@ -147,7 +162,25 @@ Optional environment:
|
||||
|
||||
|
||||
|
||||
### `go-lint.sh`: Run Backend Static Analyzers
|
||||
### `go-fuzz.sh`: Run backend fuzz tests
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
* `FUZZTIME_FLAGS`: set fuss flags for tests. The default value is
|
||||
`--fuzztime=20s`.
|
||||
|
||||
* `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is
|
||||
`--timeout=30s`.
|
||||
|
||||
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
|
||||
Go package that is processed. `2` also shows subcommands and environment.
|
||||
The default value is `0`, don't be verbose.
|
||||
|
||||
|
||||
|
||||
### `go-lint.sh`: Run backend static analyzers
|
||||
|
||||
Don't forget to run `make go-tools` once first!
|
||||
|
||||
@@ -163,7 +196,7 @@ Optional environment:
|
||||
|
||||
|
||||
|
||||
### `go-test.sh`: Run Backend Tests
|
||||
### `go-test.sh`: Run backend tests
|
||||
|
||||
Optional environment:
|
||||
|
||||
@@ -173,7 +206,7 @@ Optional environment:
|
||||
`1`, use the race detector.
|
||||
|
||||
* `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is
|
||||
`--timeout 30s`.
|
||||
`--timeout=30s`.
|
||||
|
||||
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
|
||||
Go package that is processed. `2` also shows subcommands. The default
|
||||
@@ -181,7 +214,7 @@ Optional environment:
|
||||
|
||||
|
||||
|
||||
### `go-tools.sh`: Install Backend Tooling
|
||||
### `go-tools.sh`: Install backend tooling
|
||||
|
||||
Installs the Go static analysis and other tools into `${PWD}/bin`. Either add
|
||||
`${PWD}/bin` to your `$PATH` before all other entries, or use the commands
|
||||
|
||||
@@ -62,21 +62,22 @@ readonly docker_output
|
||||
case "$channel"
|
||||
in
|
||||
('release')
|
||||
docker_version_tag="${docker_image_name}:${version}"
|
||||
docker_channel_tag="${docker_image_name}:latest"
|
||||
docker_version_tag="--tag=${docker_image_name}:${version}"
|
||||
docker_channel_tag="--tag=${docker_image_name}:latest"
|
||||
;;
|
||||
('beta')
|
||||
docker_version_tag="${docker_image_name}:${version}"
|
||||
docker_channel_tag="${docker_image_name}:beta"
|
||||
docker_version_tag="--tag=${docker_image_name}:${version}"
|
||||
docker_channel_tag="--tag=${docker_image_name}:beta"
|
||||
;;
|
||||
('edge')
|
||||
# Don't set the version tag when pushing to the edge channel.
|
||||
docker_version_tag="${docker_image_name}:edge"
|
||||
docker_channel_tag="${docker_image_name}"
|
||||
# Set the version tag to an empty string when pushing to the edge channel.
|
||||
docker_version_tag=''
|
||||
docker_channel_tag="--tag=${docker_image_name}:edge"
|
||||
;;
|
||||
('development')
|
||||
docker_version_tag="${docker_image_name}"
|
||||
docker_channel_tag="${docker_image_name}"
|
||||
# Set both tags to an empty string for development builds.
|
||||
docker_version_tag=''
|
||||
docker_channel_tag=''
|
||||
;;
|
||||
(*)
|
||||
echo "invalid channel '$channel', supported values are\
|
||||
@@ -106,18 +107,11 @@ cp "${dist_dir}/AdGuardHome_linux_arm_7/AdGuardHome/AdGuardHome"\
|
||||
cp "${dist_dir}/AdGuardHome_linux_ppc64le/AdGuardHome/AdGuardHome"\
|
||||
"${dist_docker}/AdGuardHome_linux_ppc64le_"
|
||||
|
||||
# Copy the helper scripts. See file docker/Dockerfile.
|
||||
dist_docker_scripts="${dist_docker}/scripts"
|
||||
readonly dist_docker_scripts
|
||||
|
||||
mkdir -p "$dist_docker_scripts"
|
||||
cp "./docker/dns-bind.awk"\
|
||||
"${dist_docker_scripts}/dns-bind.awk"
|
||||
cp "./docker/web-bind.awk"\
|
||||
"${dist_docker_scripts}/web-bind.awk"
|
||||
cp "./docker/healthcheck.sh"\
|
||||
"${dist_docker_scripts}/healthcheck.sh"
|
||||
|
||||
# Don't use quotes with $docker_version_tag and $docker_channel_tag, because we
|
||||
# want word splitting and or an empty space if tags are empty.
|
||||
#
|
||||
# TODO(a.garipov): Once flag --tag of docker buildx build supports commas, use
|
||||
# them instead.
|
||||
$sudo_cmd docker\
|
||||
"$debug_flags"\
|
||||
buildx build\
|
||||
@@ -127,7 +121,7 @@ $sudo_cmd docker\
|
||||
--build-arg VERSION="$version"\
|
||||
--output "$docker_output"\
|
||||
--platform "$docker_platforms"\
|
||||
--tag="$docker_version_tag"\
|
||||
--tag="$docker_channel_tag"\
|
||||
$docker_version_tag\
|
||||
$docker_channel_tag\
|
||||
-f ./docker/Dockerfile\
|
||||
.
|
||||
|
||||
55
scripts/make/go-bench.sh
Normal file
55
scripts/make/go-bench.sh
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose="${VERBOSE:-0}"
|
||||
readonly verbose
|
||||
|
||||
# Verbosity levels:
|
||||
# 0 = Don't print anything except for errors.
|
||||
# 1 = Print commands, but not nested commands.
|
||||
# 2 = Print everything.
|
||||
if [ "$verbose" -gt '1' ]
|
||||
then
|
||||
set -x
|
||||
v_flags='-v=1'
|
||||
x_flags='-x=1'
|
||||
elif [ "$verbose" -gt '0' ]
|
||||
then
|
||||
set -x
|
||||
v_flags='-v=1'
|
||||
x_flags='-x=0'
|
||||
else
|
||||
set +x
|
||||
v_flags='-v=0'
|
||||
x_flags='-x=0'
|
||||
fi
|
||||
readonly v_flags x_flags
|
||||
|
||||
set -e -f -u
|
||||
|
||||
if [ "${RACE:-1}" -eq '0' ]
|
||||
then
|
||||
race_flags='--race=0'
|
||||
else
|
||||
race_flags='--race=1'
|
||||
fi
|
||||
readonly race_flags
|
||||
|
||||
go="${GO:-go}"
|
||||
|
||||
count_flags='--count=1'
|
||||
shuffle_flags='--shuffle=on'
|
||||
timeout_flags="${TIMEOUT_FLAGS:---timeout=30s}"
|
||||
readonly go count_flags shuffle_flags timeout_flags
|
||||
|
||||
"$go" test\
|
||||
"$count_flags"\
|
||||
"$shuffle_flags"\
|
||||
"$race_flags"\
|
||||
"$timeout_flags"\
|
||||
"$x_flags"\
|
||||
"$v_flags"\
|
||||
--bench='.'\
|
||||
--benchmem\
|
||||
--benchtime=1s\
|
||||
--run='^$'\
|
||||
./...
|
||||
58
scripts/make/go-fuzz.sh
Normal file
58
scripts/make/go-fuzz.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose="${VERBOSE:-0}"
|
||||
readonly verbose
|
||||
|
||||
# Verbosity levels:
|
||||
# 0 = Don't print anything except for errors.
|
||||
# 1 = Print commands, but not nested commands.
|
||||
# 2 = Print everything.
|
||||
if [ "$verbose" -gt '1' ]
|
||||
then
|
||||
set -x
|
||||
v_flags='-v=1'
|
||||
x_flags='-x=1'
|
||||
elif [ "$verbose" -gt '0' ]
|
||||
then
|
||||
set -x
|
||||
v_flags='-v=1'
|
||||
x_flags='-x=0'
|
||||
else
|
||||
set +x
|
||||
v_flags='-v=0'
|
||||
x_flags='-x=0'
|
||||
fi
|
||||
readonly v_flags x_flags
|
||||
|
||||
set -e -f -u
|
||||
|
||||
if [ "${RACE:-1}" -eq '0' ]
|
||||
then
|
||||
race_flags='--race=0'
|
||||
else
|
||||
race_flags='--race=1'
|
||||
fi
|
||||
readonly race_flags
|
||||
|
||||
go="${GO:-go}"
|
||||
|
||||
count_flags='--count=1'
|
||||
shuffle_flags='--shuffle=on'
|
||||
timeout_flags="${TIMEOUT_FLAGS:---timeout=30s}"
|
||||
fuzztime_flags="${FUZZTIME_FLAGS:---fuzztime=20s}"
|
||||
|
||||
readonly go count_flags shuffle_flags timeout_flags fuzztime_flags
|
||||
|
||||
# TODO(a.garipov): File an issue about using --fuzz with multiple packages.
|
||||
"$go" test\
|
||||
"$count_flags"\
|
||||
"$shuffle_flags"\
|
||||
"$race_flags"\
|
||||
"$timeout_flags"\
|
||||
"$x_flags"\
|
||||
"$v_flags"\
|
||||
"$fuzztime_flags"\
|
||||
--fuzz='.'\
|
||||
--run='^$'\
|
||||
./internal/filtering/rulelist/\
|
||||
;
|
||||
@@ -176,9 +176,13 @@ run_linter gocognit --over 10\
|
||||
./internal/aghchan/\
|
||||
./internal/aghhttp/\
|
||||
./internal/aghio/\
|
||||
./internal/tools/\
|
||||
./internal/filtering/hashprefix/\
|
||||
./internal/filtering/rulelist/\
|
||||
./internal/next/\
|
||||
./internal/rdns/\
|
||||
./internal/tools/\
|
||||
./internal/version/\
|
||||
./internal/whois/\
|
||||
;
|
||||
|
||||
run_linter ineffassign ./...
|
||||
@@ -197,11 +201,29 @@ run_linter nilness ./...
|
||||
|
||||
run_linter -e shadow --strict ./...
|
||||
|
||||
# TODO(a.garipov): Enable in v0.108.0.
|
||||
# run_linter gosec --quiet ./...
|
||||
# TODO(a.garipov): Enable for all.
|
||||
run_linter gosec --quiet\
|
||||
./internal/aghalg\
|
||||
./internal/aghchan\
|
||||
./internal/aghhttp\
|
||||
./internal/aghio\
|
||||
./internal/aghnet\
|
||||
./internal/aghos\
|
||||
./internal/aghtest\
|
||||
./internal/dhcpd\
|
||||
./internal/dhcpsvc\
|
||||
./internal/dnsforward\
|
||||
./internal/filtering/hashprefix/\
|
||||
./internal/filtering/rulelist/\
|
||||
./internal/next\
|
||||
./internal/schedule\
|
||||
./internal/stats\
|
||||
./internal/tools\
|
||||
./internal/version\
|
||||
./internal/whois\
|
||||
;
|
||||
|
||||
# TODO(a.garipov): Enable --blank?
|
||||
run_linter errcheck --asserts ./...
|
||||
run_linter errcheck ./...
|
||||
|
||||
staticcheck_matrix='
|
||||
darwin: GOOS=darwin
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
conf_file="${SNAP_DATA}/AdGuardHome.yaml"
|
||||
readonly conf_file
|
||||
|
||||
if ! [ -f "$conf_file" ]
|
||||
then
|
||||
xdg-open 'http://localhost:3000'
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Get the admin interface port from the configuration.
|
||||
bind_port="$( grep -e 'bind_port' "${SNAP_DATA}/AdGuardHome.yaml" | awk -F ' ' '{print $2}' )"
|
||||
awk_prog='/^[^[:space:]]/ { is_http = /^http:/ };/^[[:space:]]+address:/ { if (is_http) print $2 }'
|
||||
readonly awk_prog
|
||||
|
||||
bind_port="$( awk "$awk_prog" "$conf_file" | awk -F ':' '{print $NF}' )"
|
||||
readonly bind_port
|
||||
|
||||
if [ "$bind_port" = '' ]
|
||||
|
||||
Reference in New Issue
Block a user