Compare commits
211 Commits
v0.108.0-b
...
v0.107.35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48ee2f8a42 | ||
|
|
ec83d0eb86 | ||
|
|
19347d263a | ||
|
|
b22b16d98c | ||
|
|
cadb765b7d | ||
|
|
1116da8b83 | ||
|
|
c65700923a | ||
|
|
7030c7c24c | ||
|
|
09718a2170 | ||
|
|
77cda2c2c5 | ||
|
|
d9c57cdd9a | ||
|
|
0dad53b5f7 | ||
|
|
9a7315dbea | ||
|
|
a21558f418 | ||
|
|
4f928be393 | ||
|
|
f543b47261 | ||
|
|
66b831072c | ||
|
|
80eb339896 | ||
|
|
c69639c013 | ||
|
|
5f6fbe8e08 | ||
|
|
b40bbf0260 | ||
|
|
a11c8e91ab | ||
|
|
618d0e596c | ||
|
|
fde9ea5cb1 | ||
|
|
03d9803238 | ||
|
|
bd64b8b014 | ||
|
|
67fe064fcf | ||
|
|
471668d19a | ||
|
|
42762dfe54 | ||
|
|
c9314610d4 | ||
|
|
16755c37d8 | ||
|
|
73fcbd6ea2 | ||
|
|
30244f361f | ||
|
|
083991fb21 | ||
|
|
e3200d5046 | ||
|
|
21f6ed36fe | ||
|
|
77d04d44eb | ||
|
|
b34d119255 | ||
|
|
63bd71a10c | ||
|
|
faf2b32389 | ||
|
|
d23da1b757 | ||
|
|
beb8e36eee | ||
|
|
fe70161c01 | ||
|
|
39fa4b1f8e | ||
|
|
c7a8883201 | ||
|
|
3fd467413c | ||
|
|
9728dd856f | ||
|
|
ecadf78d60 | ||
|
|
eba4612d72 | ||
|
|
9200163f85 | ||
|
|
3c17853344 | ||
|
|
993a3fc42c | ||
|
|
7bb9b2416b | ||
|
|
2de321ce24 | ||
|
|
30b2b85ff1 | ||
|
|
6ea4788f56 | ||
|
|
3c52a021b9 | ||
|
|
0ceea9af5f | ||
|
|
39b404be19 | ||
|
|
56dc3eab02 | ||
|
|
554a38eeb1 | ||
|
|
c8d3afe869 | ||
|
|
44222c604c | ||
|
|
cbf221585e | ||
|
|
48322f6d0d | ||
|
|
d5a213c639 | ||
|
|
8166c4bc33 | ||
|
|
133cd9ef6b | ||
|
|
11146f73ed | ||
|
|
1beb18db47 | ||
|
|
f7bc2273a7 | ||
|
|
d1e735a003 | ||
|
|
af4ff5c748 | ||
|
|
fc951c1226 | ||
|
|
f81fd42472 | ||
|
|
1029ea5966 | ||
|
|
c0abdb4bc7 | ||
|
|
6681178ad3 | ||
|
|
e73605c4c5 | ||
|
|
c7017d49aa | ||
|
|
191d3bde49 | ||
|
|
18876a8e5c | ||
|
|
aa4a0d9880 | ||
|
|
d03d731d65 | ||
|
|
33b58a42fe | ||
|
|
2e9e708647 | ||
|
|
8ad22841ab | ||
|
|
32cf02264c | ||
|
|
0e8445b38f | ||
|
|
cb27ecd6c0 | ||
|
|
535220b3df | ||
|
|
7b9cfa94f8 | ||
|
|
b3f2e88e9c | ||
|
|
aa7a8d45e4 | ||
|
|
49cdef3d6a | ||
|
|
fecd146552 | ||
|
|
b01efd8c98 | ||
|
|
bd4dfb261c | ||
|
|
e754e4d2f6 | ||
|
|
b220e35c99 | ||
|
|
4f5131f423 | ||
|
|
dcb043df5f | ||
|
|
86e5756262 | ||
|
|
ba0cf5739b | ||
|
|
c4a13b92d2 | ||
|
|
723279121a | ||
|
|
3ad7649f7d | ||
|
|
2898a49d86 | ||
|
|
1547f9d35e | ||
|
|
adadd55c42 | ||
|
|
33b0225aa4 | ||
|
|
97d4058d80 | ||
|
|
86207e719d | ||
|
|
113f94ff46 | ||
|
|
5673deb391 | ||
|
|
3548a393ed | ||
|
|
254515f274 | ||
|
|
bccbecc6ea | ||
|
|
66f53803af | ||
|
|
faef005ce7 | ||
|
|
941cd2a562 | ||
|
|
6a4a9a0239 | ||
|
|
b9dbe6f1b6 | ||
|
|
7fec111ef8 | ||
|
|
5e1bd99718 | ||
|
|
9d75f72ceb | ||
|
|
d98d96db1a | ||
|
|
6a0ef2df15 | ||
|
|
75c2eb4c8a | ||
|
|
d021a67d66 | ||
|
|
4ed97cab12 | ||
|
|
a38742eed7 | ||
|
|
5efa95ed26 | ||
|
|
04db7db607 | ||
|
|
d17c6c6bb3 | ||
|
|
b2052f2ef1 | ||
|
|
cddcf852c2 | ||
|
|
1def426b45 | ||
|
|
b114fd5279 | ||
|
|
d27c3284f6 | ||
|
|
ba24a26b53 | ||
|
|
3e6678b6b4 | ||
|
|
83fd6f9782 | ||
|
|
52bc1b3f10 | ||
|
|
dd2153b7ac | ||
|
|
dd96a34861 | ||
|
|
daf26ee25a | ||
|
|
7e140eaaac | ||
|
|
d07a712988 | ||
|
|
95863288bf | ||
|
|
ea12be658b | ||
|
|
faa7c9aae5 | ||
|
|
e3653e8c25 | ||
|
|
b40cb24822 | ||
|
|
74004c1aa0 | ||
|
|
3e240741f1 | ||
|
|
6cfdbef1a5 | ||
|
|
d9bde6425b | ||
|
|
e2ae9e1591 | ||
|
|
5ebcbfa9ad | ||
|
|
e276bd7a31 | ||
|
|
659b2529bf | ||
|
|
97b3ed43ab | ||
|
|
767d6d3f28 | ||
|
|
31fc9bfc52 | ||
|
|
3f06b02409 | ||
|
|
5bf958ec6b | ||
|
|
959d9ff9a0 | ||
|
|
4813b4de25 | ||
|
|
119100924c | ||
|
|
bd584de4ee | ||
|
|
ede85ab2f2 | ||
|
|
12c20288e4 | ||
|
|
5bbbf89c10 | ||
|
|
d55393ecd5 | ||
|
|
2b5927306f | ||
|
|
4f016b6ed7 | ||
|
|
3a2a6d10ec | ||
|
|
2491426b09 | ||
|
|
5ebdd1390e | ||
|
|
b7f0247575 | ||
|
|
e28186a28a | ||
|
|
de1a7ce48f | ||
|
|
48480fb33b | ||
|
|
f41332fe6b | ||
|
|
1f8b340b8f | ||
|
|
fdaf1d09d3 | ||
|
|
b9682c4f10 | ||
|
|
69dcb4effd | ||
|
|
d50fd0ba91 | ||
|
|
c2c7b4c731 | ||
|
|
952d5f3a3d | ||
|
|
3f126c9ec9 | ||
|
|
0be58ef918 | ||
|
|
8f9053e2fc | ||
|
|
68452e5330 | ||
|
|
2eacc46eaa | ||
|
|
74dcc91ea7 | ||
|
|
dd7bf61323 | ||
|
|
2819d6cace | ||
|
|
75355a6883 | ||
|
|
e9c007d56b | ||
|
|
84c9085516 | ||
|
|
9f36e57c1e | ||
|
|
7528699fc2 | ||
|
|
d280151c18 | ||
|
|
b44c755d25 | ||
|
|
e4078e87a1 | ||
|
|
be36204756 | ||
|
|
b5409d6d00 | ||
|
|
f3d6bce03e |
60
.github/ISSUE_TEMPLATE/bug.yml
vendored
60
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -32,31 +32,33 @@
|
||||
- 'attributes':
|
||||
'description': 'On which Platform does the issue occur?'
|
||||
'label': 'Platform (OS and CPU architecture)'
|
||||
# NOTE: Keep the 386 at the bottom for each OS, because a lot of people
|
||||
# Seem to confuse them with AMD64, which is what they actually need.
|
||||
'options':
|
||||
- 'Darwin (aka macOS)/AMD64 (aka x86_64)'
|
||||
- 'Darwin (aka macOS)/ARM64'
|
||||
- 'FreeBSD/386'
|
||||
- 'FreeBSD/AMD64 (aka x86_64)'
|
||||
- 'FreeBSD/ARM64'
|
||||
- 'FreeBSD/ARMv5'
|
||||
- 'FreeBSD/ARMv6'
|
||||
- 'FreeBSD/ARMv7'
|
||||
- 'Linux/386'
|
||||
- 'Linux/AMD64 (aka x86_64)'
|
||||
- 'Linux/ARM64'
|
||||
- 'Linux/ARMv5'
|
||||
- 'Linux/ARMv6'
|
||||
- 'Linux/ARMv7'
|
||||
- 'Linux/MIPS LE'
|
||||
- 'Linux/MIPS'
|
||||
- 'Linux/MIPS64 LE'
|
||||
- 'Linux/MIPS64'
|
||||
- 'Linux/PPC64 LE'
|
||||
- 'OpenBSD/AMD64 (aka x86_64)'
|
||||
- 'OpenBSD/ARM64'
|
||||
- 'Windows/386'
|
||||
- 'Windows/AMD64 (aka x86_64)'
|
||||
- 'Windows/ARM64'
|
||||
- 'Darwin (aka macOS), AMD64 (aka x86_64)'
|
||||
- 'Darwin (aka macOS), ARM64'
|
||||
- 'FreeBSD, AMD64 (aka x86_64)'
|
||||
- 'FreeBSD, ARM64'
|
||||
- 'FreeBSD, ARMv5'
|
||||
- 'FreeBSD, ARMv6'
|
||||
- 'FreeBSD, ARMv7'
|
||||
- 'FreeBSD, 32-bit Intel (aka 386)'
|
||||
- 'Linux, AMD64 (aka x86_64)'
|
||||
- 'Linux, ARM64'
|
||||
- 'Linux, ARMv5'
|
||||
- 'Linux, ARMv6'
|
||||
- 'Linux, ARMv7'
|
||||
- 'Linux, MIPS LE'
|
||||
- 'Linux, MIPS'
|
||||
- 'Linux, MIPS64 LE'
|
||||
- 'Linux, MIPS64'
|
||||
- 'Linux, PPC64 LE'
|
||||
- 'Linux, 32-bit Intel (aka 386)'
|
||||
- 'OpenBSD, AMD64 (aka x86_64)'
|
||||
- 'OpenBSD, ARM64'
|
||||
- 'Windows, AMD64 (aka x86_64)'
|
||||
- 'Windows, ARM64'
|
||||
- 'Windows, 32-bit Intel (aka 386)'
|
||||
- 'Custom (please mention in the description)'
|
||||
'id': 'os'
|
||||
'type': 'dropdown'
|
||||
@@ -142,8 +144,10 @@
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': false
|
||||
'description': >
|
||||
Open a bug report. Please do not open bug reports for questions or help
|
||||
with configuring clients. If you want to ask for help, use the Discussions
|
||||
section.
|
||||
# NOTE: GitHub limits the description length to 200 characters. Also, Markdown
|
||||
# doesn't work here.
|
||||
'description': |
|
||||
For help, use the Discussions section instead. Write the title in English
|
||||
to make it easier for other people to search for duplicates. (Any language
|
||||
is fine in the body.)
|
||||
'name': 'Bug'
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature.yml
vendored
6
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -48,7 +48,11 @@
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': false
|
||||
'description': 'Suggest a feature or an enhancement for AdGuard Home'
|
||||
# NOTE: GitHub limits the description length to 200 characters. Also, Markdown
|
||||
# doesn't work here.
|
||||
'description': |
|
||||
Write the title in English to make it easier for other people to search for
|
||||
duplicates. (Any language is fine in the body.)
|
||||
'labels':
|
||||
- 'feature request'
|
||||
'name': 'Feature request or enhancement'
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.19.10'
|
||||
'GO_VERSION': '1.19.11'
|
||||
'NODE_VERSION': '14'
|
||||
|
||||
'on':
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.19.10'
|
||||
'GO_VERSION': '1.19.11'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
*.db
|
||||
*.log
|
||||
*.snap
|
||||
*.test
|
||||
/agh-backup/
|
||||
/bin/
|
||||
/build/*
|
||||
|
||||
170
CHANGELOG.md
170
CHANGELOG.md
@@ -14,15 +14,135 @@ and this project adheres to
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.33] - 2023-06-28 (APPROX.)
|
||||
## [v0.107.36] - 2023-08-09 (APPROX.)
|
||||
|
||||
See also the [v0.107.33 GitHub milestone][ms-v0.107.33].
|
||||
See also the [v0.107.36 GitHub milestone][ms-v0.107.36].
|
||||
|
||||
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
|
||||
[ms-v0.107.36]: https://github.com/AdguardTeam/AdGuardHome/milestone/71?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [v0.107.35] - 2023-07-26
|
||||
|
||||
See also the [v0.107.35 GitHub milestone][ms-v0.107.35].
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved reliability filtering-rule list updates on Unix systems.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Occasional client information lookup failures that could lead to the DNS
|
||||
server getting stuck ([#6006]).
|
||||
- `bufio.Scanner: token too long` and other errors when trying to add
|
||||
filtering-rule lists with lines over 1024 bytes long or containing cosmetic
|
||||
rules ([#6003]).
|
||||
|
||||
### Removed
|
||||
|
||||
- Default exposure of the non-standard ports 784 and 8853 for DNS-over-QUIC in
|
||||
the `Dockerfile`.
|
||||
|
||||
[#6003]: https://github.com/AdguardTeam/AdGuardHome/issues/6003
|
||||
[#6006]: https://github.com/AdguardTeam/AdGuardHome/issues/6006
|
||||
|
||||
[ms-v0.107.35]: https://github.com/AdguardTeam/AdGuardHome/milestone/70?closed=1
|
||||
|
||||
|
||||
|
||||
## [v0.107.34] - 2023-07-12
|
||||
|
||||
See also the [v0.107.34 GitHub milestone][ms-v0.107.34].
|
||||
|
||||
### Security
|
||||
|
||||
- Go version has been updated to prevent the possibility of exploiting the
|
||||
CVE-2023-29406 Go vulnerability fixed in [Go 1.19.11][go-1.19.11].
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to ignore queries for the root domain, such as `NS .` queries
|
||||
([#5990]).
|
||||
|
||||
### 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
|
||||
[#5990]: https://github.com/AdguardTeam/AdGuardHome/issues/5990
|
||||
|
||||
[go-1.19.11]: https://groups.google.com/g/golang-announce/c/2q13H6LEEx0/m/sduSepLLBwAJ
|
||||
[ms-v0.107.34]: https://github.com/AdguardTeam/AdGuardHome/milestone/69?closed=1
|
||||
|
||||
|
||||
|
||||
## [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 +157,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 +257,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 +283,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 +2270,14 @@ 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.36...HEAD
|
||||
[v0.107.36]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.35...v0.107.36
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.35...HEAD
|
||||
[v0.107.35]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.34...v0.107.35
|
||||
[v0.107.34]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...v0.107.34
|
||||
[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
|
||||
|
||||
14
Makefile
14
Makefile
@@ -37,8 +37,6 @@ SIGN = 1
|
||||
VERSION = v0.0.0
|
||||
YARN = yarn
|
||||
|
||||
NEXTAPI = 0
|
||||
|
||||
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
||||
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
||||
# into BUILD_RELEASE_DEPS_0, and so both frontend and backend
|
||||
@@ -66,7 +64,6 @@ ENV = env\
|
||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||
RACE='$(RACE)'\
|
||||
SIGN='$(SIGN)'\
|
||||
NEXTAPI='$(NEXTAPI)'\
|
||||
VERBOSE="$(VERBOSE.MACRO)"\
|
||||
VERSION='$(VERSION)'\
|
||||
|
||||
@@ -78,7 +75,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 +101,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
|
||||
|
||||
@@ -128,3 +127,10 @@ openapi-lint: ; cd ./openapi/ && $(YARN) test
|
||||
openapi-show: ; cd ./openapi/ && $(YARN) start
|
||||
|
||||
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
||||
|
||||
# TODO(a.garipov): Consider adding to scripts/ and the common project
|
||||
# structure.
|
||||
go-upd-tools:
|
||||
cd ./internal/tools/ &&\
|
||||
"$(GO.MACRO)" get -u &&\
|
||||
"$(GO.MACRO)" mod tidy
|
||||
|
||||
@@ -416,7 +416,8 @@ There are three options how you can install an unstable version:
|
||||
### <a href="#reporting-issues" id="reporting-issues" name="reporting-issues">Report issues</a>
|
||||
|
||||
If you run into any problem or have a suggestion, head to [this page][iss] and
|
||||
click on the “New issue” button.
|
||||
click on the “New issue” button. Please follow the instructions in the issue
|
||||
form carefully and don't forget to start by searching for duplicates.
|
||||
|
||||
[iss]: https://github.com/AdguardTeam/AdGuardHome/issues
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.8'
|
||||
|
||||
'stages':
|
||||
- 'Build frontend':
|
||||
@@ -272,7 +272,7 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.8'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
@@ -287,4 +287,4 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.8'
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.8'
|
||||
'snapcraftChannel': 'edge'
|
||||
|
||||
'stages':
|
||||
@@ -191,7 +191,7 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.8'
|
||||
'snapcraftChannel': 'beta'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
@@ -207,5 +207,5 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.8'
|
||||
'snapcraftChannel': 'candidate'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.8'
|
||||
|
||||
'stages':
|
||||
- 'Tests':
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Opravdu chcete odstranit klienta \"{{key}}\"?",
|
||||
"list_confirm_delete": "Opravdu chcete smazat tento seznam?",
|
||||
"auto_clients_title": "Spuštění klienti",
|
||||
"auto_clients_desc": "Zařízení, která nejsou na seznamu stálých klientů, a mohou nadále používat AdGuard Home",
|
||||
"auto_clients_desc": "Informace o IP adresách zařízení, která používají nebo mohou používat AdGuard Home. Tyto informace se získávají z několika zdrojů, včetně souborů hosts, reverzního DNS atd.",
|
||||
"access_title": "Nastavení přístupu",
|
||||
"access_desc": "Zde můžete konfigurovat pravidla přístupu pro server DNS AdGuard Home",
|
||||
"access_allowed_title": "Povolení klienti",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Sikker på, at du vil slette klient \"{{key}}\"?",
|
||||
"list_confirm_delete": "Sikker på, at du vil slette denne liste?",
|
||||
"auto_clients_title": "Klienter (runtime)",
|
||||
"auto_clients_desc": "Enheder, som ikke er på listen over Permanente klienter, kan stadig bruge AdGuard Home",
|
||||
"auto_clients_desc": "Oplysninger om IP-adresser på enheder, som (måske) bruger AdGuard Home. Disse oplysninger indsamles fra flere kilder, herunder hosts-filer, reverse DNS mv.",
|
||||
"access_title": "Adgangsindstillinger",
|
||||
"access_desc": "Her kan adgangsregler for AdGuard Home DNS-serveren opsættes",
|
||||
"access_allowed_title": "Tilladte klienter",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Möchten Sie den Client „{{key}}“ wirklich löschen?",
|
||||
"list_confirm_delete": "Möchten Sie diese Liste wirklich löschen?",
|
||||
"auto_clients_title": "Laufzeit-Clients",
|
||||
"auto_clients_desc": "Geräte, die nicht auf der Liste der persistenten Clients stehen und trotzdem AdGuard Home verwenden dürfen",
|
||||
"auto_clients_desc": "Informationen über IP-Adressen der Geräten, die AdGuard Home nutzen oder nutzen könnten. Diese Informationen werden aus verschiedenen Quellen gesammelt, darunter Hosts-Dateien, Reverse-DNS usw.",
|
||||
"access_title": "Zugriffsrechte",
|
||||
"access_desc": "Hier können Sie die Zugriffsregeln für den DNS-Server von AdGuard Home konfigurieren",
|
||||
"access_allowed_title": "Zugelassene Clients",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
|
||||
"list_confirm_delete": "Are you sure you want to delete this list?",
|
||||
"auto_clients_title": "Runtime clients",
|
||||
"auto_clients_desc": "Devices not on the list of Persistent clients that may still use AdGuard Home",
|
||||
"auto_clients_desc": "Information about IP addresses of devices that are using or may use AdGuard Home. This information is gathered from several sources, including hosts files, reverse DNS, etc.",
|
||||
"access_title": "Access settings",
|
||||
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server",
|
||||
"access_allowed_title": "Allowed clients",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "¿Estás seguro de que deseas eliminar el cliente \"{{key}}\"?",
|
||||
"list_confirm_delete": "¿Estás seguro de que deseas eliminar esta lista?",
|
||||
"auto_clients_title": "Clientes activos",
|
||||
"auto_clients_desc": "Dispositivos que no están en la lista de clientes persistentes que aún pueden utilizar AdGuard Home",
|
||||
"auto_clients_desc": "Información sobre las direcciones IP de los dispositivos que usan o pueden usar AdGuard Home. Esta información se recopila de varias fuentes, incluidos ficheros de host, DNS inverso, etc.",
|
||||
"access_title": "Configuración de acceso",
|
||||
"access_desc": "Aquí puedes configurar las reglas de acceso para el servidor DNS de AdGuard Home",
|
||||
"access_allowed_title": "Clientes permitidos",
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
"client_settings": "Päätelaiteasetukset",
|
||||
"example_upstream_reserved": "ylävirta <0>tietyille verkkotunnuksille</0>;",
|
||||
"example_upstream_comment": "kommentti.",
|
||||
"upstream_parallel": "Käytä rinnakkaisia pyyntöjä ja nopeuta selvitystä käyttämällä kaikkia ylävirran palvelimia samanaikaisesti.",
|
||||
"upstream_parallel": "Käytä rinnakkaisia pyyntöjä ja nopeuta selvitystä käyttämällä kaikkia ylävirtapalvelimia samanaikaisesti.",
|
||||
"parallel_requests": "Rinnakkaiset pyynnöt",
|
||||
"load_balancing": "Kuormantasaus",
|
||||
"load_balancing_desc": "Lähetä pyyntö yhdelle ylävirran palvelimelle kerrallaan. AdGuard Home pyrkii valitsemaan nopeimman palvelimen painotetun satunnaisalgoritminsa avulla.",
|
||||
"load_balancing_desc": "Lähetä pyyntö yhdelle ylävirtapalvelimelle kerrallaan. AdGuard Home pyrkii valitsemaan nopeimman palvelimen painotetun satunnaisalgoritminsa avulla.",
|
||||
"bootstrap_dns": "Bootstrap DNS-palvelimet",
|
||||
"bootstrap_dns_desc": "Bootstrap DNS-palvelimia käytetään ylävirroiksi määritettyjen DoH/DoT-resolvereiden IP-osoitteiden selvitykseen.",
|
||||
"local_ptr_title": "Yksityiset käänteiset DNS-palvelimet",
|
||||
"local_ptr_desc": "DNS-palvelimet, joita AdGuard Home käyttää paikallisille PTR-pyynnöille. Näitä palvelimia käytetään yksityistä IP-osoitetta käyttävien PTR-pyyntöjen osoitteiden, kuten \"192.168.12.34\", selvitykseen käänteisen DNS:n avulla. Jos ei käytössä, AdGuard Home käyttää käyttöjärjestelmän oletusarvoisia DNS-resolvereita, poislukien AdGuard Homen omat osoitteet.",
|
||||
"local_ptr_default_resolver": "Oletusarvoisesti AdGuard Home käyttää seuraavia käänteisiä DNS-resolvereita: {{ip}}.",
|
||||
"local_ptr_no_default_resolver": "AdGuard Home ei voinut määrittää tälle järjestelmälle sopivaa yksityistä käänteistä DNS-resolveria.",
|
||||
"local_ptr_title": "Yksityiset käänteis-DNS-palvelimet",
|
||||
"local_ptr_desc": "DNS-palvelimet, joita AdGuard Home käyttää paikallisille PTR-pyynnöille. Näitä palvelimia käytetään yksityistä IP-osoitetta käyttävien PTR-pyyntöjen osoitteiden, kuten \"192.168.12.34\", selvitykseen käänteis-DNS:n avulla. Jos ei käytössä, AdGuard Home käyttää käyttöjärjestelmän oletusarvoisia DNS-resolvereita, poislukien AdGuard Homen omat osoitteet.",
|
||||
"local_ptr_default_resolver": "Oletusarvoisesti AdGuard Home käyttää seuraavia käänteis-DNS-resolvereita: {{ip}}.",
|
||||
"local_ptr_no_default_resolver": "AdGuard Home ei voinut määrittää tälle järjestelmälle sopivaa yksityistä käänteis-DNS-resolveria.",
|
||||
"local_ptr_placeholder": "Syötä yksi palvelimen osoite per rivi",
|
||||
"resolve_clients_title": "Käytä päätelaitteiden IP-osoitteille käänteistä selvitystä",
|
||||
"resolve_clients_desc": "Selvitä päätelaitteiden IP-osoitteiden isäntänimet käänteisesti lähettämällä PTR-pyynnöt sopiville resolvereille (yksityiset DNS-palvelimet paikallisille päätelaitteille, lähtevät palvelimet päätelaitteille, joilla on julkiset IP-osoitteet).",
|
||||
"use_private_ptr_resolvers_title": "Käytä yksityisiä käänteisiä DNS-resolvereita",
|
||||
"use_private_ptr_resolvers_desc": "Suorita käänteiset DNS-selvitykset paikallisesti tarjotuille osoitteille käyttäen näitä ylävirran palvelimia. Jos ei käytössä, vastaa AdGuard Home kaikkiin sen tyyppisiin PTR-pyyntöihin NXDOMAIN-arvolla, pois lukien DHCP, /etc/hosts, yms. -tiedoista tunnistettut päätelaitteet.",
|
||||
"resolve_clients_desc": "Selvitä päätelaitteiden IP-osoitteiden isäntänimet käänteisesti lähettämällä PTR-pyynnöt sopiville resolvereille (yksityiset DNS-palvelimet paikallisille päätelaitteille, ylävirtapalvelimet päätelaitteille, joilla on julkiset IP-osoitteet).",
|
||||
"use_private_ptr_resolvers_title": "Käytä yksityisiä käänteis-DNS-resolvereita",
|
||||
"use_private_ptr_resolvers_desc": "Suorita käänteis-DNS-selvitykset paikallisesti tarjotuille osoitteille käyttäen näitä ylävirtapalvelimia. Jos ei käytössä, vastaa AdGuard Home kaikkiin sen tyyppisiin PTR-pyyntöihin NXDOMAIN-arvolla, pois lukien DHCP, /etc/hosts, yms. -tiedoista tunnistettut päätelaitteet.",
|
||||
"check_dhcp_servers": "Etsi DHCP-palvelimia",
|
||||
"save_config": "Tallenna asetukset",
|
||||
"enabled_dhcp": "DHCP-palvelin otettiin käyttöön",
|
||||
@@ -220,7 +220,7 @@
|
||||
"example_upstream_tcp_port": "tavallinen DNS (TCP, portti);",
|
||||
"example_upstream_tcp_hostname": "tavallinen DNS (TCP, isäntänimi);",
|
||||
"all_lists_up_to_date_toast": "Kaikki listat ovat ajan tasalla",
|
||||
"updated_upstream_dns_toast": "Ylävirtojen palvelimet tallennettiin",
|
||||
"updated_upstream_dns_toast": "Ylävirtapalvelimet tallennettiin",
|
||||
"dns_test_ok_toast": "Määritetyt DNS-palvelimet toimivat oikein",
|
||||
"dns_test_not_ok_toast": "Palvelin \"{{key}}\": Ei voitu käyttää, tarkista oikeinkirjoitus",
|
||||
"dns_test_warning_toast": "Datavuon \"{{key}}\" ei vastaa testipyyntöihin eikä välttämättä toimi kunnolla",
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Haluatko varmasti poistaa päätelaitteen \"{{key}}\"?",
|
||||
"list_confirm_delete": "Haluatko varmasti poistaa tämän listan?",
|
||||
"auto_clients_title": "Määrittämättömät päätelaitteet",
|
||||
"auto_clients_desc": "Päätelaitteet, joita ei ole määritetty pysyviksi ja jotka voivat silti käyttää AdGuard Homea.",
|
||||
"auto_clients_desc": "Päätelaitteet, joita ei ole määritetty pysyviksi ja jotka voivat silti käyttää AdGuard Homea. Näitä tietoja kertään useista lähteistä, mm. hosts-tiedostoista ja kääteis-DNS:llä.",
|
||||
"access_title": "Käytön asetukset",
|
||||
"access_desc": "Tässä voidaan määrittää AdGuard Homen DNS-palvelimen käyttöoikeussääntöjä.",
|
||||
"access_allowed_title": "Sallitut päätelaitteet",
|
||||
@@ -623,7 +623,7 @@
|
||||
"enter_cache_size": "Syötä välimuistin koko (tavuina)",
|
||||
"enter_cache_ttl_min_override": "Syötä vähimmäis-TTL (sekunteina)",
|
||||
"enter_cache_ttl_max_override": "Syötä enimmäis-TTL (sekunteina)",
|
||||
"cache_ttl_min_override_desc": "Pidennä ylävirran palvelimelta vastaanotettuja, lyhyitä elinaika-arvoja (sekunteina) tallennettaessa DNS-vastauksia välimuistiin.",
|
||||
"cache_ttl_min_override_desc": "Pidennä ylävirtapalvelimelta vastaanotettuja, lyhyitä elinaika-arvoja (sekunteina) tallennettaessa DNS-vastauksia välimuistiin.",
|
||||
"cache_ttl_max_override_desc": "Määritä DNS-välimuistin kohteiden enimmäiselinaika (sekunteina).",
|
||||
"ttl_cache_validation": "Välimuistin vähimmäiselinajan on oltava pienempi tai sama kuin enimmäiselinajan",
|
||||
"cache_optimistic": "Optimistinen välimuisti",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Voulez-vous vraiment supprimer le client « {{key}} » ?",
|
||||
"list_confirm_delete": "Voulez-vous vraiment supprimer cette liste ?",
|
||||
"auto_clients_title": "Clients d'exécution",
|
||||
"auto_clients_desc": "Appareils ne figurant pas sur la liste des clients persistants qui peuvent encore utiliser AdGuard Home.",
|
||||
"auto_clients_desc": "Informations sur les adresses IP des appareils qui utilisent ou pourraient utiliser AdGuard Home. Ces informations sont recueillies à partir de plusieurs sources, notamment les fichiers hosts, le DNS inverse, etc.",
|
||||
"access_title": "Paramètres d'accès",
|
||||
"access_desc": "Ici vous pouvez configurer les règles d'accès au serveur DNS AdGuard Home",
|
||||
"access_allowed_title": "Clients autorisés",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Sei sicuro di voler eliminare il client \"{{key}}\"?",
|
||||
"list_confirm_delete": "Sei sicuro di voler eliminare questo elenco?",
|
||||
"auto_clients_title": "Client in tempo reale",
|
||||
"auto_clients_desc": "Dispositivi non presenti nell'elenco dei client Persistenti che possono ancora utilizzare AdGuard Home",
|
||||
"auto_clients_desc": "Informazioni sugli indirizzi IP dei dispositivi che utilizzano o potrebbero utilizzare AdGuard Home. Queste informazioni vengono raccolte da diverse fonti, inclusi file host, DNS inverso, ecc.",
|
||||
"access_title": "Impostazioni di accesso",
|
||||
"access_desc": "Qui puoi configurare le regole d'accesso per il server DNS di AdGuard Home",
|
||||
"access_allowed_title": "Client permessi",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "クライアント \"{{key}}\" を削除してもよろしいですか?",
|
||||
"list_confirm_delete": "このリストを削除してもよろしいですか?",
|
||||
"auto_clients_title": "ランタイムクライアント",
|
||||
"auto_clients_desc": "永続的クライアントのリストに未登録で、AdGuard Homeを使用する場合があるデバイスのリスト。",
|
||||
"auto_clients_desc": "AdGuard Home を使用している、または使用する可能性のあるデバイスの IP アドレスに関する情報です。この情報は、hosts ファイル、リバース DNS など、複数の情報源から収集されます。",
|
||||
"access_title": "アクセス設定",
|
||||
"access_desc": "こちらでは、AdGuard Home DNSサーバーのアクセスルールを設定できます。",
|
||||
"access_allowed_title": "許可されたクライアント",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "정말 클라이언트 '{{key}}'을(를) 삭제하시겠습니까?",
|
||||
"list_confirm_delete": "정말로 이 목록을 제거하시겠습니까?",
|
||||
"auto_clients_title": "런타임 클라이언트",
|
||||
"auto_clients_desc": "AdGuard Home을 계속 사용할 수 있는 영구 클라이언트 목록에 없는 디바이스입니다",
|
||||
"auto_clients_desc": "AdGuard Home을 사용 중이거나 사용할 수 있는 기기의 IP 주소에 대한 정보가 표시됩니다. 이 정보는 호스트 파일, 역방향 DNS 등 여러 소스에서 수집됩니다.",
|
||||
"access_title": "접근 설정",
|
||||
"access_desc": "여기에서 AdGuard Home DNS 서버에 대한 액세스 규칙을 설정할 수 있습니다",
|
||||
"access_allowed_title": "허용된 클라이언트",
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
"cancel_btn": "Annuleren",
|
||||
"enter_name_hint": "Voeg naam toe",
|
||||
"enter_url_or_path_hint": "Voer een URL in of het pad van de lijst",
|
||||
"check_updates_btn": "Controleer op updates",
|
||||
"check_updates_btn": "Controleren op updates",
|
||||
"new_blocklist": "Nieuwe blokkeerlijst",
|
||||
"new_allowlist": "Nieuwe toelatingslijst",
|
||||
"edit_blocklist": "Blokkeerlijst beheren",
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Ben je zeker dat je deze gebruiker \"{{key}}\" wilt verwijderen?",
|
||||
"list_confirm_delete": "Ben je zeker om deze lijst te verwijderen?",
|
||||
"auto_clients_title": "Runtime-clients",
|
||||
"auto_clients_desc": "Apparaten die niet op de lijst van permanente clients staan die mogelijk nog steeds AdGuard Home gebruiken",
|
||||
"auto_clients_desc": "Informatie over IP-adressen van apparaten die AdGuard Home gebruiken of kunnen gebruiken. Deze informatie wordt verzameld uit verschillende bronnen, waaronder hosts-bestanden, reverse DNS, enz.",
|
||||
"access_title": "Toegangs instellingen",
|
||||
"access_desc": "Hier kan je toegangsregels voor de AdGuard Home DNS-server instellen",
|
||||
"access_allowed_title": "Toegestane gebruikers",
|
||||
@@ -456,7 +456,7 @@
|
||||
"access_settings_saved": "Toegangsinstellingen succesvol opgeslagen",
|
||||
"updates_checked": "Een nieuwe versie van AdGuard Home is beschikbaar\n",
|
||||
"updates_version_equal": "AdGuard Home is actueel",
|
||||
"check_updates_now": "Controleer op updates",
|
||||
"check_updates_now": "Nu controleren op updates",
|
||||
"version_request_error": "Updatecontrole mislukt. Controleer je internetverbinding.",
|
||||
"dns_privacy": "DNS Privacy",
|
||||
"setup_dns_privacy_1": "<0>DNS-via-TLS:</0> Gebruik <1>{{address}}</1> string.",
|
||||
@@ -573,7 +573,7 @@
|
||||
"tags_title": "Labels",
|
||||
"tags_desc": "Je kunt labels selecteren die overeenkomen met de client. Labels kunnen worden opgenomen in de filterregels om ze \n nauwkeuriger toe te passen. <0>Meer informatie</0>.",
|
||||
"form_select_tags": "Client tags selecteren",
|
||||
"check_title": "Controleer de filtering",
|
||||
"check_title": "De filtering controleren",
|
||||
"check_desc": "Controleren of een hostnaam wordt gefilterd.",
|
||||
"check": "Controleren",
|
||||
"form_enter_host": "Voer een hostnaam in",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Czy na pewno chcesz usunąć klienta \"{{key}}\"?",
|
||||
"list_confirm_delete": "Czy na pewno chcesz usunąć tę listę?",
|
||||
"auto_clients_title": "Uruchomieni klienci",
|
||||
"auto_clients_desc": "Urządzenia, których nie ma na liście stałych klientów, które mogą nadal korzystać z AdGuard Home",
|
||||
"auto_clients_desc": "Informacje o adresach IP urządzeń korzystających lub mogących korzystać z AdGuard Home. Te informacje są gromadzone z wielu źródeł takich jak pliki hosta, odwrotna translacja DNS, itp.",
|
||||
"access_title": "Ustawienia dostępu",
|
||||
"access_desc": "Tutaj możesz skonfigurować reguły dostępu dla serwera DNS AdGuard Home",
|
||||
"access_allowed_title": "Dozwoleni klienci",
|
||||
@@ -470,7 +470,7 @@
|
||||
"setup_dns_privacy_ios_2": "Aplikacja <0>AdGuard dla iOS</0> obsługuje <1>DNS-over-HTTPS</1> i <1>DNS-over-TLS</1>.",
|
||||
"setup_dns_privacy_other_title": "Inne implementacje",
|
||||
"setup_dns_privacy_other_1": "Sam AdGuard Home może być bezpiecznym klientem DNS na dowolnej platformie.",
|
||||
"setup_dns_privacy_other_2": "<0>dnsproxy</0> obsługuje wszystkie znane bezpieczne protokoły DNS.\n\n",
|
||||
"setup_dns_privacy_other_2": "<0>dnsproxy</0> obsługuje wszystkie znane bezpieczne protokoły DNS.",
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> obsługuje <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> obsługuje <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "Znajdziesz więcej implementacji <0>tutaj</0> i <1>tutaj</1>.",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Você tem certeza de que deseja excluir o cliente \"{{key}}\"?",
|
||||
"list_confirm_delete": "Você tem certeza de que deseja excluir essa lista?",
|
||||
"auto_clients_title": "Clientes ativos",
|
||||
"auto_clients_desc": "Dispositivo não está na lista de dispositivos persistentes que podem ser utilizados no AdGuard Home",
|
||||
"auto_clients_desc": "Informações sobre endereços IP de dispositivos que usam ou podem usar o AdGuard Home. Essas informações são coletadas de várias fontes, incluindo arquivos de hosts, DNS reverso, etc.",
|
||||
"access_title": "Configurações de acessos",
|
||||
"access_desc": "Aqui você pode configurar as regras de acesso para o servidores de DNS do AdGuard Home",
|
||||
"access_allowed_title": "Clientes permitidos",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Tem a certeza de que deseja excluir o cliente \"{{key}}\"?",
|
||||
"list_confirm_delete": "Você tem certeza de que deseja excluir essa lista?",
|
||||
"auto_clients_title": "Clientes ativos",
|
||||
"auto_clients_desc": "Dispositivo não está na lista de dispositivos persistentes que podem ser utilizados no AdGuard Home",
|
||||
"auto_clients_desc": "Informações sobre endereços IP de dispositivos que estão a utilizar ou podem utilizar o AdGuard Home. Estas informações são recolhidas a partir de várias fontes, incluindo ficheiros hosts, DNS reverso etc.",
|
||||
"access_title": "Definições de acesso",
|
||||
"access_desc": "Aqui pode configurar as regras de acesso para o servidores de DNS do AdGuard Home",
|
||||
"access_allowed_title": "Clientes permitidos",
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
"number_of_dns_query_to_safe_search": "Количество запросов DNS для поисковых систем, для которых был применён Безопасный поиск",
|
||||
"average_processing_time": "Среднее время обработки запроса",
|
||||
"average_processing_time_hint": "Среднее время для обработки запроса DNS в миллисекундах",
|
||||
"block_domain_use_filters_and_hosts": "Блокировать домены с использованием фильтров и файлов хостов",
|
||||
"block_domain_use_filters_and_hosts": "Блокировать домены с использованием фильтров и файлов hosts",
|
||||
"filters_block_toggle_hint": "Вы можете настроить правила блокировки в <a>«Фильтрах»</a>.",
|
||||
"use_adguard_browsing_sec": "Включить Безопасную навигацию AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home проверит, включён ли домен в веб-службу безопасности браузера. Он будет использовать API, чтобы выполнить проверку: на сервер отправляется только короткий префикс имени домена SHA256.",
|
||||
@@ -296,7 +296,7 @@
|
||||
"rate_limit_desc": "Ограничение на количество запросов в секунду для каждого клиента (0 — неограниченно).",
|
||||
"blocking_ipv4_desc": "IP-адрес, возвращаемый при блокировке A-запроса",
|
||||
"blocking_ipv6_desc": "IP-адрес, возвращаемый при блокировке AAAA-запроса",
|
||||
"blocking_mode_default": "Стандартный: Отвечает с нулевым IP-адресом, (0.0.0.0 для A; :: для AAAA) когда заблокировано правилом в стиле Adblock; отвечает с IP-адресом, указанным в правиле, когда заблокировано правилом в стиле /etc/hosts-style",
|
||||
"blocking_mode_default": "Стандартный: Отвечает с нулевым IP-адресом, (0.0.0.0 для A; :: для AAAA) когда заблокировано правилом в стиле Adblock; отвечает с IP-адресом, указанным в правиле, когда заблокировано правилом в стиле файлов hosts",
|
||||
"blocking_mode_refused": "REFUSED: Отвечает с кодом REFUSED",
|
||||
"blocking_mode_nxdomain": "NXDOMAIN: Отвечает с кодом NXDOMAIN\n",
|
||||
"blocking_mode_null_ip": "Нулевой IP: Отвечает с нулевым IP-адресом (0.0.0.0 для A; :: для AAAA)",
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Вы уверены, что хотите удалить клиента «{{key}}»?",
|
||||
"list_confirm_delete": "Вы уверены, что хотите удалить этот список?",
|
||||
"auto_clients_title": "Клиенты (runtime)",
|
||||
"auto_clients_desc": "Несохранённые клиенты, которые могут пользоваться AdGuard Home",
|
||||
"auto_clients_desc": "Информация об IP-адресах устройств, которые используют или могут использовать AdGuard Home. Эта информация собирается из нескольких источников, включая файлы hosts, обратный DNS и так далее.",
|
||||
"access_title": "Настройки доступа",
|
||||
"access_desc": "Здесь вы можете настроить правила доступа к DNS-серверу AdGuard Home",
|
||||
"access_allowed_title": "Разрешённые клиенты",
|
||||
|
||||
@@ -435,6 +435,7 @@
|
||||
"updates_checked": "ඇඩ්ගාර්ඩ් හෝම් හි නව අනුවාදයක් තිබේ",
|
||||
"updates_version_equal": "ඇඩ්ගාර්ඩ් හෝම් යාවත්කාලීනයි",
|
||||
"check_updates_now": "දැන් යාවත්කාල පරීක්ෂා කරන්න",
|
||||
"version_request_error": "යාවත්කාලීන පරීක්ෂාවට අසමත් විය. ඔබගේ අන්තර්ජාල සම්බන්ධතාවය පරීක්ෂා කරන්න.",
|
||||
"dns_privacy": "ව.නා.ප. රහස්යතා",
|
||||
"setup_dns_privacy_1": "<0>TLS-මගින්-ව.නා.ප.</0> සඳහා <1>{{address}}</1>.",
|
||||
"setup_dns_privacy_2": "<0>HTTPS-මගින්-ව.නා.ප.</0> සඳහා <1>{{address}}</1>.",
|
||||
@@ -453,7 +454,9 @@
|
||||
"setup_dns_notice": "ඔබට <1>HTTPS-මගින්-ව.නා.ප.</1> හෝ <1>DNS-මගින්-ව.නා.ප.</1> භාවිතයට ඇඩ්ගාර්ඩ් හෝම් සැකසුම් තුළ <0>සංකේතනය වින්යාසගත</0> කළ යුතුය.",
|
||||
"rewrite_added": "\"{{key}}\" සඳහා ව.නා.ප. නැවත ලිවීම සාර්ථකව එකතු කෙරිණි",
|
||||
"rewrite_deleted": "\"{{key}}\" සඳහා ව.නා.ප. නැවත ලිවීම ඉවත් කෙරිණි",
|
||||
"rewrite_add": "ව.නා.ප. නැවත ලිවීමක් එකතු කරන්න",
|
||||
"rewrite_updated": "ව.නා.ප. නැවත ලිවීම සාර්ථකව යාවත්කාලීන කෙරිණි",
|
||||
"rewrite_add": "ව.නා.ප. නැවත ලිවීමක් යොදන්න",
|
||||
"rewrite_edit": "ව.නා.ප. නැවත ලිවීම සංස්කරණය",
|
||||
"rewrite_not_found": "ව.නා.ප. නැවත ලිවීම් හමු නොවිණි",
|
||||
"rewrite_confirm_delete": "\"{{key}}\" සඳහා ව.නා.ප. නැවත ලිවීම ඉවත් කිරීමට අවශ්ය බව ඔබට විශ්වාසද?",
|
||||
"rewrite_desc": "නිශ්චිත වසම් නාමයක් සඳහා අභිරුචි ව.නා.ප. ප්රතිචාර පහසුවෙන් වින්යාසගත කිරීමට ඉඩ දෙයි.",
|
||||
@@ -611,9 +614,12 @@
|
||||
"safe_browsing": "ආරක්ෂිත පිරික්සුම",
|
||||
"served_from_cache": "{{value}} <i>(නිහිතයෙන් ගැනිණි)</i>",
|
||||
"form_error_password_length": "මුරපදය අවම වශයෙන් අකුරු {{value}} ක් දිගු විය යුතුමයි",
|
||||
"anonymizer_notification": "<0>සටහන:</0> අ.ජා.කෙ. නිර්නාමිකකරණය සබලයි. ඔබට එය <1>පොදු සැකසුම්</1> හරහා අබල කිරීමට හැකිය .",
|
||||
"confirm_dns_cache_clear": "ඔබට ව.නා.ප. නිහිතය හිස් කිරීමට වුවමනාද?",
|
||||
"cache_cleared": "ව.නා.ප. නිහිතය හිස් කෙරිණි",
|
||||
"clear_cache": "නිහිතය මකන්න",
|
||||
"make_static": "ස්ථිතික කරන්න",
|
||||
"theme_auto_desc": "ස්වයං (උපාංගයේ වර්ණ පරිපාටිය මත පදනම්ව)",
|
||||
"theme_dark_desc": "අඳුරු තේමාව",
|
||||
"theme_light_desc": "දීප්ත තේමාව",
|
||||
"disable_for_seconds": "තත්පර {{count}} ක්",
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
"encryption_key": "Súkromný kľúč",
|
||||
"encryption_key_input": "Skopírujte a prilepte sem svoj súkromný kľúč vo formáte PEM pre Váš certifikát.",
|
||||
"encryption_enable": "Zapnite šifrovanie (HTTPS, DNS-cez-HTTPS a DNS-cez-TLS)",
|
||||
"encryption_enable_desc": "Ak je šifrovanie zapnuté, AdGuard Home administrátorské rozhranie bude pracovať cez HTTPS a DNS server bude počúvať požiadavky cez DNS-cez-HTTPS a DNS-cez-TLS.",
|
||||
"encryption_enable_desc": "Ak je šifrovanie zapnuté, AdGuard Home administrátorské rozhranie bude pracovať cez HTTPS a DNS server bude počúvať dopyty cez DNS-cez-HTTPS a DNS-cez-TLS.",
|
||||
"encryption_chain_valid": "Certifikačný reťazec je platný",
|
||||
"encryption_chain_invalid": "Certifikačný reťazec je neplatný",
|
||||
"encryption_key_valid": "Toto je platný {{type}} súkromný kľúč",
|
||||
@@ -497,7 +497,7 @@
|
||||
"blocked_services": "Blokované služby",
|
||||
"blocked_services_desc": "Umožňuje rýchlo blokovať populárne stránky a služby.",
|
||||
"blocked_services_saved": "Blokované služby boli úspešne uložené",
|
||||
"blocked_services_global": "Použite globálne blokované služby",
|
||||
"blocked_services_global": "Použiť globálne blokované služby",
|
||||
"blocked_service": "Blokované služby",
|
||||
"block_all": "Blokovať všetko",
|
||||
"unblock_all": "Odblokovať všetko",
|
||||
@@ -554,7 +554,7 @@
|
||||
"whois": "WHOIS",
|
||||
"filtering_rules_learn_more": "<0>Dozvedieť sa viac</0> o tvorbe vlastných zoznamov hostiteľov.",
|
||||
"blocked_by_response": "Blokované pomocou CNAME alebo IP v odpovedi",
|
||||
"blocked_by_cname_or_ip": "Zablokované na základe CNAME alebo IP",
|
||||
"blocked_by_cname_or_ip": "Blokované pomocou CNAME alebo IP",
|
||||
"try_again": "Skúste znova",
|
||||
"domain_desc": "Zadajte meno domény alebo zástupný znak, ktorý chcete prepísať.",
|
||||
"example_rewrite_domain": "prepísať odpovede iba pre toto meno domény.",
|
||||
@@ -571,7 +571,7 @@
|
||||
"autofix_warning_list": "Bude vykonávať tieto úlohy: <0>Deaktivovať systém DNSStubListener</0> <0>Nastaviť adresu servera DNS na 127.0.0.1</0> <0>Nahradiť cieľový symbolický odkaz /etc/resolv.conf na /run/systemd/resolve/resolv.conf</0> <0>Zastaviť službu DNSStubListener (znova načítať službu systemd-resolved)</0>",
|
||||
"autofix_warning_result": "Výsledkom bude, že všetky DNS dopyty z Vášho systému budú štandardne spracované službou AdGuard Home.",
|
||||
"tags_title": "Tagy",
|
||||
"tags_desc": "Môžete vybrať značky, ktoré zodpovedajú klientovi. Zahrňte značky do pravidiel filtrovania, aby ste ich použili presnejšie. <0>Viac informácií</0>.",
|
||||
"tags_desc": "Môžete vybrať značky, ktoré zodpovedajú klientovi. Zahrňte značky do pravidiel filtrácie, aby ste ich použili presnejšie. <0>Viac informácií</0>.",
|
||||
"form_select_tags": "Zvoľte tagy klienta",
|
||||
"check_title": "Skontrolujte filtráciu",
|
||||
"check_desc": "Skontrolujte, či je názov hostiteľa filtrovaný.",
|
||||
@@ -608,7 +608,7 @@
|
||||
"show_whitelisted_responses": "Obsiahnuté v bielej listine",
|
||||
"show_processed_responses": "Spracované",
|
||||
"blocked_safebrowsing": "Zablokované modulom Bezpečné prehliadanie",
|
||||
"blocked_adult_websites": "Zablokovaná stránka pre dospelých",
|
||||
"blocked_adult_websites": "Zablokované Rodičovskou kontrolou",
|
||||
"blocked_threats": "Zablokované hrozby",
|
||||
"allowed": "Povolené",
|
||||
"filtered": "Filtrované",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "Ali ste prepričani, da želite izbrisati odjemalca \"{{key}}\"?",
|
||||
"list_confirm_delete": "Ali ste prepričani, da želite izbrisati ta seznam?",
|
||||
"auto_clients_title": "Odjemalci izvajanja",
|
||||
"auto_clients_desc": "Naprave, ki niso na seznamu trajnih odjemalcev, ki morda še vedno uporabljajo AdGuard Home",
|
||||
"auto_clients_desc": "Informacije o naslovih IP naprav, ki uporabljajo ali bi lahko uporabljale AdGuard Home. Te informacije so zbrane iz več virov, vključno z datotekami gostiteljev, povratnim DNS-jem itd.",
|
||||
"access_title": "Nastavitve dostopa",
|
||||
"access_desc": "Tukaj lahko nastavite pravila dostopa strežnika DNS AdGuard Home",
|
||||
"access_allowed_title": "Dovoljeni odjemalci",
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"enabled_parental_toast": "Uključena roditeljska kontrola",
|
||||
"disabled_safe_search_toast": "Isključena sigurna pretraga",
|
||||
"enabled_save_search_toast": "Uključeno sigurno pretraživanje",
|
||||
"updated_save_search_toast": "Ažurirane postavke bezbedne pretrage",
|
||||
"enabled_table_header": "Uključeno",
|
||||
"name_table_header": "Ime",
|
||||
"list_url_table_header": "URL do liste",
|
||||
@@ -256,12 +257,12 @@
|
||||
"query_log_cleared": "Dnevnik unosa je uspešno očišćen",
|
||||
"query_log_updated": "Dnevnik zapisa je uspešno ažuriran",
|
||||
"query_log_clear": "Očisti dnevnike unosa",
|
||||
"query_log_retention": "Zadržavanje dnevnika unosa",
|
||||
"query_log_retention": "Rotacija evidencija upita",
|
||||
"query_log_enable": "Uključi dnevnik",
|
||||
"query_log_configuration": "Konfiguracija dnevnika",
|
||||
"query_log_disabled": "Dnevnik unosa je isključen ali se može konfigurisati u <0>postavkama</0>",
|
||||
"query_log_strict_search": "Koristi duple navodnike za striktnu pretragu",
|
||||
"query_log_retention_confirm": "Jeste li sigurni da želite da promenite zadržavanje dnevnika unosa? Ako smanjite vrednost intervala, neki podaci će biti izgubljeni",
|
||||
"query_log_retention_confirm": "Želite li zaista da promenite rotaciju evidencije upita? Ako smanjite vrednost intervala, neki podaci će biti izgubljeni",
|
||||
"anonymize_client_ip": "Anonimizuj IP klijenta",
|
||||
"anonymize_client_ip_desc": "Ne čuvaj punu IP adresu klijenta u dnevnicima i statistikama",
|
||||
"dns_config": "Konfiguracija DNS servera",
|
||||
@@ -290,6 +291,8 @@
|
||||
"rate_limit": "Ograničenje brzine",
|
||||
"edns_enable": "Uključi EDNS Client Subnet",
|
||||
"edns_cs_desc": "Dodajte opciju podmreži EDNS klijenta (ECS) uzvodnim zahtevima i evidentirajte vrednosti koje klijenti šalju u evidenciji upita.",
|
||||
"edns_use_custom_ip": "Koristi prilagođeni IP za EDNS",
|
||||
"edns_use_custom_ip_desc": "Dozvoli korišćenje prilagođenog IP-a za EDNS",
|
||||
"rate_limit_desc": "Broj zahteva u sekundi dozvoljen po klijentu. Postavljanje na 0 znači da nema ograničenja.",
|
||||
"blocking_ipv4_desc": "IP adresa koja će biti vraćena za blokirane zahteve",
|
||||
"blocking_ipv6_desc": "IP adresa koja će biti vraćena za blokirane AAAA zahteve",
|
||||
@@ -441,7 +444,7 @@
|
||||
"client_confirm_delete": "Jeste li sigurni da želite da izbrišete klijenta \"{{key}}\"?",
|
||||
"list_confirm_delete": "Jeste li sigurni da želite da izbrišete ovu listu?",
|
||||
"auto_clients_title": "Klijenti (runtime)",
|
||||
"auto_clients_desc": "Uređaji koji nisu na listi upornih klijenata koji i dalje mogu da koriste AdGuard Home",
|
||||
"auto_clients_desc": "Podaci o klijentima koji koriste AdGuard Home, ali nisu sačuvani u konfiguraciji",
|
||||
"access_title": "Postavke pristupa",
|
||||
"access_desc": "Ovde možete konfigurisati pravila pristupa za AdGuard Home DNS server",
|
||||
"access_allowed_title": "Dozvoljeni klijenti",
|
||||
@@ -525,6 +528,10 @@
|
||||
"statistics_retention_confirm": "Jeste li sigurni da želite da promenite zadržavanje statistike? Ako smanjite vrednost intervala, neki podaci će biti izgubljeni",
|
||||
"statistics_cleared": "Statistika je uspešno očišćena",
|
||||
"statistics_enable": "Uključi statistiku",
|
||||
"ignore_domains": "Zanemari domene (razdvojene novom linijom)",
|
||||
"ignore_domains_title": "Zanemareni domeni",
|
||||
"ignore_domains_desc_stats": "Upiti za ove domene nisu upisani u statistiku",
|
||||
"ignore_domains_desc_query": "Upiti za ove domene nisu upisani u evidenciju upita",
|
||||
"interval_hours": "{{count}} čas",
|
||||
"interval_hours_plural": "{{count}} časova",
|
||||
"filters_configuration": "Konfiguracija filtera",
|
||||
@@ -645,5 +652,29 @@
|
||||
"confirm_dns_cache_clear": "Želite li zaista da obrišite DNS keš?",
|
||||
"cache_cleared": "DNS keš je uspešno očišćen",
|
||||
"clear_cache": "Obriši keš memoriju",
|
||||
"protection_section_label": "Zaštita"
|
||||
"make_static": "Učini statičnim",
|
||||
"theme_auto_desc": "Automatski (na osnovu šeme boja uređaja)",
|
||||
"theme_dark_desc": "Tamna tema",
|
||||
"theme_light_desc": "Svetla tema",
|
||||
"disable_for_seconds": "Za {{count}} sekund",
|
||||
"disable_for_seconds_plural": "Za {{count}} sekundi",
|
||||
"disable_for_minutes": "Za {{count}} minut",
|
||||
"disable_for_minutes_plural": "Za {{count}} minuta",
|
||||
"disable_for_hours": "Za {{count}} sat",
|
||||
"disable_for_hours_plural": "Za {{count}} sati",
|
||||
"disable_until_tomorrow": "Do sutra",
|
||||
"disable_notify_for_seconds": "Isključi zaštitu na {{count}} sekund",
|
||||
"disable_notify_for_seconds_plural": "Isključi zaštitu na {{count}} sekundi",
|
||||
"disable_notify_for_minutes": "Isključi zaštitu na {{count}} minut",
|
||||
"disable_notify_for_minutes_plural": "Isključi zaštitu na {{count}} minuta",
|
||||
"disable_notify_for_hours": "Isključi zaštitu na {{count}} sat",
|
||||
"disable_notify_for_hours_plural": "Isključi zaštitu na {{count}} sati",
|
||||
"disable_notify_until_tomorrow": "Isključi zaštitu do sutra",
|
||||
"enable_protection_timer": "Zaštita će biti uključena u {{time}}",
|
||||
"custom_retention_input": "Unesite zadržavanje u časovima",
|
||||
"custom_rotation_input": "Unesite rotaciju u časovima",
|
||||
"protection_section_label": "Zaštita",
|
||||
"log_and_stats_section_label": "Evidencija upita i statistika",
|
||||
"ignore_query_log": "Zanemari ovog klijenta u evidenciji upita",
|
||||
"ignore_statistics": "Zanemari ovog klijenta u statističkim podacima"
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "\"{{key}}\" istemcisini silmek istediğinizden emin misiniz?",
|
||||
"list_confirm_delete": "Bu listeyi silmek istediğinizden emin misiniz?",
|
||||
"auto_clients_title": "Çalışma zamanı istemcileri",
|
||||
"auto_clients_desc": "Henüz AdGuard Home'u kullanabilecek Kalıcı istemciler listesinde olmayan cihazlar",
|
||||
"auto_clients_desc": "AdGuard Home'u kullanan veya kullanabilecek cihazların IP adresleri hakkında bilgiler. Bu bilgiler, hosts dosyaları, ters DNS, vb. dahil olmak üzere çeşitli kaynaklardan toplanır.",
|
||||
"access_title": "Erişim ayarları",
|
||||
"access_desc": "AdGuard Home DNS sunucusu için erişim kurallarını buradan yapılandırabilirsiniz",
|
||||
"access_allowed_title": "İzin verilen istemciler",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "您确定要删除客户端 \"{{key}}\"?",
|
||||
"list_confirm_delete": "您确定要删除此列表吗?",
|
||||
"auto_clients_title": "客户端(运行时间)",
|
||||
"auto_clients_desc": "不在可继续使用 AdGuard Home 的持久客户端列表中的设备。",
|
||||
"auto_clients_desc": "有关正在使用或可能使用 AdGuard Home 的设备的 IP 地址的信息。此信息是从多个来源收集的,包括 hosts 文件、反向 DNS 等。",
|
||||
"access_title": "访问设置",
|
||||
"access_desc": "您可以在此处配置 AdGuard Home 的 DNS 服务器的访问规则",
|
||||
"access_allowed_title": "允许的客户端",
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
"disabled_parental_toast": "已停用家長監護",
|
||||
"enabled_parental_toast": "已啟用家長監護",
|
||||
"disabled_safe_search_toast": "已停用安全搜尋",
|
||||
"enabled_save_search_toast": "已啟用安全搜尋",
|
||||
"updated_save_search_toast": "已更新安全搜尋設定",
|
||||
"enabled_table_header": "啟用",
|
||||
"name_table_header": "名稱",
|
||||
"list_url_table_header": "清單 URL 網址",
|
||||
@@ -211,6 +211,10 @@
|
||||
"example_upstream_doq": "加密 <0>DNS-over-QUIC</0>",
|
||||
"example_upstream_sdns": "您可以使透過 <0>DNS Stamps</0> 來解析 <1>DNSCrypt</1> 或 <2>DNS-over-HTTPS</2>",
|
||||
"example_upstream_tcp": "一般 DNS(透過 TCP)",
|
||||
"example_upstream_regular_port": "一般 DNS(透過 UDP,連接埠)",
|
||||
"example_upstream_udp": "一般 DNS(透過 UDP,主機名稱)",
|
||||
"example_upstream_tcp_port": "一般 DNS(透過 TCP,連接埠)",
|
||||
"example_upstream_tcp_hostname": "一般 DNS(透過 TCP,主機名稱)",
|
||||
"all_lists_up_to_date_toast": "所有清單已更新至最新",
|
||||
"dns_test_ok_toast": "設定中的 DNS 上游運作正常",
|
||||
"dns_test_not_ok_toast": "DNS 設定中的 \"{{key}}\" 出現錯誤,請確認是否正確輸入",
|
||||
@@ -468,6 +472,7 @@
|
||||
"rewrite_added": "「{{key}}」的 DNS 覆寫新增成功",
|
||||
"rewrite_deleted": "「{{key}}」的 DNS 覆寫刪除成功",
|
||||
"rewrite_add": "新增 DNS 覆寫",
|
||||
"rewrite_edit": "編輯 DNS 覆寫",
|
||||
"rewrite_not_found": "找不到 DNS 覆寫",
|
||||
"rewrite_confirm_delete": "您確定要刪除 \"{{key}}\" 的 DNS 覆寫?",
|
||||
"rewrite_desc": "提供簡單的方式對特定網域自訂 DNS 回應。",
|
||||
@@ -501,6 +506,7 @@
|
||||
"interval_days": "{{count}} 天",
|
||||
"interval_days_plural": "{{count}} 天",
|
||||
"domain": "網域",
|
||||
"ecs": "EDNS 子網",
|
||||
"punycode": "Punycode",
|
||||
"answer": "回應",
|
||||
"filter_added_successfully": "已成功新增清單",
|
||||
@@ -513,6 +519,9 @@
|
||||
"statistics_clear_confirm": "您確定要清除統計資料嗎?",
|
||||
"statistics_retention_confirm": "您確定要更改統計資料保存時間嗎?如果您縮短期限部分資料可能將會遺失",
|
||||
"statistics_cleared": "已清除統計資料",
|
||||
"statistics_enable": "啟用統計數據",
|
||||
"ignore_domains": "已忽略網域(每行一個)",
|
||||
"ignore_domains_title": "已忽略網域",
|
||||
"interval_hours": "{{count}} 小時",
|
||||
"interval_hours_plural": "{{count}} 小時",
|
||||
"filters_configuration": "過濾器設定",
|
||||
@@ -625,6 +634,7 @@
|
||||
"safe_browsing": "安全瀏覽",
|
||||
"served_from_cache": "{{value}} <i>(由快取回應)</i>",
|
||||
"form_error_password_length": "密碼必須至少 {{value}} 個字元長度",
|
||||
"make_static": "新增為靜態",
|
||||
"theme_dark_desc": "深色主題",
|
||||
"theme_light_desc": "淺色主題",
|
||||
"disable_for_seconds": "{{count}} 秒",
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
"client_confirm_delete": "您確定您想要刪除用戶端 \"{{key}}\" 嗎?",
|
||||
"list_confirm_delete": "您確定您想要刪除該清單嗎?",
|
||||
"auto_clients_title": "執行時期用戶端",
|
||||
"auto_clients_desc": "未於可能仍然使用 AdGuard Home 的持續性用戶端之清單上的裝置",
|
||||
"auto_clients_desc": "AdGuard Home 使用或可能使用的裝置的 IP 地址資訊。這些資訊來自多個來源,包括主機檔案、反向 DNS 等。",
|
||||
"access_title": "存取設定",
|
||||
"access_desc": "於此您可配置用於 AdGuard Home DNS 伺服器之存取規則",
|
||||
"access_allowed_title": "已允許的用戶端",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,7 +57,7 @@ const ClientsTable = ({
|
||||
};
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
const config = values;
|
||||
const config = { ...values };
|
||||
|
||||
if (values) {
|
||||
if (values.blocked_services) {
|
||||
|
||||
@@ -64,12 +64,6 @@ export default {
|
||||
"homepage": "https://github.com/MasterKia/PersianBlocker",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_19.txt"
|
||||
},
|
||||
"ITA_filtri_dns": {
|
||||
"name": "ITA: Filtri-DNS",
|
||||
"categoryId": "regional",
|
||||
"homepage": "https://filtri-dns.ga/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_18.txt"
|
||||
},
|
||||
"KOR_list_kr": {
|
||||
"name": "KOR: List-KR DNS",
|
||||
"categoryId": "regional",
|
||||
@@ -166,14 +160,20 @@ export default {
|
||||
"homepage": "https://github.com/DandelionSprout/adfilt",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_12.txt"
|
||||
},
|
||||
"dandelion_sprouts_anti_push_notifications": {
|
||||
"name": "Dandelion Sprout's Anti Push Notifications",
|
||||
"categoryId": "other",
|
||||
"homepage": "https://github.com/DandelionSprout/adfilt",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_39.txt"
|
||||
},
|
||||
"dandelion_sprouts_game_console_adblock_list": {
|
||||
"name": "Dandelion Sprout's Game Console Adblock List",
|
||||
"categoryId": "other",
|
||||
"homepage": "https://github.com/DandelionSprout/adfilt",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_6.txt"
|
||||
},
|
||||
"hagezi_personal": {
|
||||
"name": "HaGeZi Personal Black \u0026 White",
|
||||
"hagezi_multinormal": {
|
||||
"name": "HaGeZi Multi NORMAL",
|
||||
"categoryId": "general",
|
||||
"homepage": "https://github.com/hagezi/dns-blocklists",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_34.txt"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"timeUpdated": "2023-06-26T13:46:24.414Z",
|
||||
"timeUpdated": "2023-07-15T00:10:47.501Z",
|
||||
"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,
|
||||
@@ -13941,10 +14088,11 @@
|
||||
"companyId": "qihoo_360_technology"
|
||||
},
|
||||
"qq.com": {
|
||||
"name": "qq.com",
|
||||
"categoryId": 8,
|
||||
"url": "http://www.qq.com/",
|
||||
"companyId": "qq.com"
|
||||
"name": "QQ International",
|
||||
"categoryId": 2,
|
||||
"url": "https://www.qq.com/",
|
||||
"companyId": "tencent",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"qrius": {
|
||||
"name": "Qrius",
|
||||
@@ -16484,6 +16632,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,
|
||||
@@ -20354,6 +20509,7 @@
|
||||
"amazon.com.au": "amazon",
|
||||
"amazon-corp.com": "amazon",
|
||||
"a2z.com": "amazon",
|
||||
"firetvcaptiveportal.com": "amazon",
|
||||
"amazon-adsystem.com": "amazon_adsystem",
|
||||
"serving-sys.com": "amazon_adsystem",
|
||||
"sizmek.com": "amazon_adsystem",
|
||||
@@ -20598,6 +20754,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 +21369,7 @@
|
||||
"2mdn.net": "doubleclick",
|
||||
"doubleclick.net": "doubleclick",
|
||||
"invitemedia.com": "doubleclick",
|
||||
"doubleclick.com": "doubleclick",
|
||||
"doublepimp.com": "doublepimp",
|
||||
"doublepimpssl.com": "doublepimp",
|
||||
"redcourtside.com": "doublepimp",
|
||||
@@ -21435,12 +21593,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 +21660,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 +21825,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 +22118,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 +22500,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",
|
||||
@@ -22535,6 +22999,7 @@
|
||||
"mrskincash.com": "mrskincash",
|
||||
"e-msedge.net": "msedge",
|
||||
"l-msedge.net": "msedge",
|
||||
"s-msedge.net": "msedge",
|
||||
"msn.com": "msn",
|
||||
"s-msn.com": "msn",
|
||||
"musculahq.appspot.com": "muscula",
|
||||
@@ -22847,6 +23312,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 +24118,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,18 @@ 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)
|
||||
# 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)
|
||||
#
|
||||
# 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
|
||||
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 443/udp 853/tcp\
|
||||
853/udp 3000/tcp 3000/udp 5443/tcp 5443/udp 6060/tcp
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
19
go.mod
19
go.mod
@@ -3,9 +3,8 @@ module github.com/AdguardTeam/AdGuardHome
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
// TODO(a.garipov): Update to a tagged version when it's released.
|
||||
github.com/AdguardTeam/dnsproxy v0.50.3-0.20230628054307-31e374065768
|
||||
github.com/AdguardTeam/golibs v0.13.3
|
||||
github.com/AdguardTeam/dnsproxy v0.52.0
|
||||
github.com/AdguardTeam/golibs v0.13.6
|
||||
github.com/AdguardTeam/urlfilter v0.16.1
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.7
|
||||
@@ -16,7 +15,7 @@ require (
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/renameio v1.0.1
|
||||
github.com/google/renameio/v2 v2.0.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230612134759-b20c9ba983df
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||
@@ -28,14 +27,14 @@ require (
|
||||
// own code for that. Perhaps, use gopacket.
|
||||
github.com/mdlayher/raw v0.1.0
|
||||
github.com/miekg/dns v1.1.55
|
||||
github.com/quic-go/quic-go v0.35.1
|
||||
github.com/quic-go/quic-go v0.36.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/ti-mo/netfilter v0.5.0
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
golang.org/x/crypto v0.10.0
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
|
||||
golang.org/x/net v0.11.0
|
||||
golang.org/x/sys v0.9.0
|
||||
golang.org/x/crypto v0.11.0
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
|
||||
golang.org/x/net v0.12.0
|
||||
golang.org/x/sys v0.10.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
howett.net/plist v1.0.0
|
||||
@@ -62,6 +61,6 @@ require (
|
||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
)
|
||||
|
||||
36
go.sum
36
go.sum
@@ -1,9 +1,9 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.50.3-0.20230628054307-31e374065768 h1:5Ia6wA+tqAlTyzuaOVGSlHmb0osLWXeJUs3NxCuC4gA=
|
||||
github.com/AdguardTeam/dnsproxy v0.50.3-0.20230628054307-31e374065768/go.mod h1:CQhZTkqC8X0ID6glrtyaxgqRRdiYfn1gJulC1cZ5Dn8=
|
||||
github.com/AdguardTeam/dnsproxy v0.52.0 h1:uZxCXflHSAwtJ7uTYXP6qgWcxaBsH0pJvldpwTqIDJk=
|
||||
github.com/AdguardTeam/dnsproxy v0.52.0/go.mod h1:Jo2zeRe97Rxt3yikXc+fn0LdLtqCj0Xlyh1PNBj6bpM=
|
||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||
github.com/AdguardTeam/golibs v0.13.3 h1:RT3QbzThtaLiFLkIUDS6/hlGEXrh0zYvdf4bd7UWpGo=
|
||||
github.com/AdguardTeam/golibs v0.13.3/go.mod h1:wkJ6EUsN4np/9Gp7+9QeooY9E2U2WCLJYAioLCzkHsI=
|
||||
github.com/AdguardTeam/golibs v0.13.6 h1:z/0Q25pRLdaQxtoxvfSaooz5mdv8wj0R8KREj54q8yQ=
|
||||
github.com/AdguardTeam/golibs v0.13.6/go.mod h1:hOtcb8dPfKcFjWTPA904hTA4dl1aWvzeebdJpE72IPk=
|
||||
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
|
||||
github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
|
||||
@@ -52,8 +52,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
|
||||
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
|
||||
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
|
||||
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -108,8 +108,8 @@ github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc8
|
||||
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
||||
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
||||
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
|
||||
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
|
||||
github.com/quic-go/quic-go v0.36.1 h1:WsG73nVtnDy1TiACxFxhQ3TqaW+DipmqzLEtNlAwZyY=
|
||||
github.com/quic-go/quic-go v0.36.1/go.mod h1:zPetvwDlILVxt15n3hr3Gf/I3mDf7LpLKPhR4Ez0AZQ=
|
||||
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
|
||||
github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -134,10 +134,10 @@ go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -152,8 +152,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
@@ -177,16 +177,16 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package aghio
|
||||
package aghio_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -31,7 +32,7 @@ func TestLimitReader(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := LimitReader(nil, tc.n)
|
||||
_, err := aghio.LimitReader(nil, tc.n)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
@@ -57,7 +58,7 @@ func TestLimitedReader_Read(t *testing.T) {
|
||||
limit: 3,
|
||||
want: 0,
|
||||
}, {
|
||||
err: &LimitReachedError{
|
||||
err: &aghio.LimitReachedError{
|
||||
Limit: 0,
|
||||
},
|
||||
name: "limit_reached",
|
||||
@@ -74,7 +75,7 @@ func TestLimitedReader_Read(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
readCloser := io.NopCloser(strings.NewReader(tc.rStr))
|
||||
lreader, err := LimitReader(readCloser, tc.limit)
|
||||
lreader, err := aghio.LimitReader(readCloser, tc.limit)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, lreader)
|
||||
|
||||
@@ -89,7 +90,7 @@ func TestLimitedReader_Read(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLimitedReader_LimitReachedError(t *testing.T) {
|
||||
testutil.AssertErrorMsg(t, "attempted to read more than 0 bytes", &LimitReachedError{
|
||||
testutil.AssertErrorMsg(t, "attempted to read more than 0 bytes", &aghio.LimitReachedError{
|
||||
Limit: 0,
|
||||
})
|
||||
}
|
||||
|
||||
43
internal/aghnet/addr.go
Normal file
43
internal/aghnet/addr.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
)
|
||||
|
||||
// NormalizeDomain returns a lowercased version of host without the final dot,
|
||||
// unless host is ".", in which case it returns it unchanged. That is a special
|
||||
// case that to allow matching queries like:
|
||||
//
|
||||
// dig IN NS '.'
|
||||
func NormalizeDomain(host string) (norm string) {
|
||||
if host == "." {
|
||||
return host
|
||||
}
|
||||
|
||||
return strings.ToLower(strings.TrimSuffix(host, "."))
|
||||
}
|
||||
|
||||
// NewDomainNameSet returns nil and error, if list has duplicate or empty domain
|
||||
// name. Otherwise returns a set, which contains domain names normalized using
|
||||
// [NormalizeDomain].
|
||||
func NewDomainNameSet(list []string) (set *stringutil.Set, err error) {
|
||||
set = stringutil.NewSet()
|
||||
|
||||
for i, host := range list {
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("at index %d: hostname is empty", i)
|
||||
}
|
||||
|
||||
host = NormalizeDomain(host)
|
||||
if set.Has(host) {
|
||||
return nil, fmt.Errorf("duplicate hostname %q at index %d", host, i)
|
||||
}
|
||||
|
||||
set.Add(host)
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
59
internal/aghnet/addr_test.go
Normal file
59
internal/aghnet/addr_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package aghnet_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewDomainNameSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
in []string
|
||||
}{{
|
||||
name: "nil",
|
||||
wantErrMsg: "",
|
||||
in: nil,
|
||||
}, {
|
||||
name: "success",
|
||||
wantErrMsg: "",
|
||||
in: []string{
|
||||
"Domain.Example",
|
||||
".",
|
||||
},
|
||||
}, {
|
||||
name: "dups",
|
||||
wantErrMsg: `duplicate hostname "domain.example" at index 1`,
|
||||
in: []string{
|
||||
"Domain.Example",
|
||||
"domain.example",
|
||||
},
|
||||
}, {
|
||||
name: "bad_domain",
|
||||
wantErrMsg: "at index 0: hostname is empty",
|
||||
in: []string{
|
||||
"",
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
set, err := aghnet.NewDomainNameSet(tc.in)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, host := range tc.in {
|
||||
assert.Truef(t, set.Has(aghnet.NormalizeDomain(host)), "%q not matched", host)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
)
|
||||
|
||||
// GenerateHostname generates the hostname from ip. In case of using IPv4 the
|
||||
@@ -29,32 +25,8 @@ func GenerateHostname(ip netip.Addr) (hostname string) {
|
||||
hostname = ip.StringExpanded()
|
||||
|
||||
if ip.Is4() {
|
||||
return strings.Replace(hostname, ".", "-", -1)
|
||||
return strings.ReplaceAll(hostname, ".", "-")
|
||||
}
|
||||
|
||||
return strings.Replace(hostname, ":", "-", -1)
|
||||
}
|
||||
|
||||
// NewDomainNameSet returns nil and error, if list has duplicate or empty
|
||||
// domain name. Otherwise returns a set, which contains non-FQDN domain names,
|
||||
// and nil error.
|
||||
func NewDomainNameSet(list []string) (set *stringutil.Set, err error) {
|
||||
set = stringutil.NewSet()
|
||||
|
||||
for i, v := range list {
|
||||
host := strings.ToLower(strings.TrimSuffix(v, "."))
|
||||
// TODO(a.garipov): Think about ignoring empty (".") names in the
|
||||
// future.
|
||||
if host == "" {
|
||||
return nil, errors.Error("host name is empty")
|
||||
}
|
||||
|
||||
if set.Has(host) {
|
||||
return nil, fmt.Errorf("duplicate host name %q at index %d", host, i)
|
||||
}
|
||||
|
||||
set.Add(host)
|
||||
}
|
||||
|
||||
return set, nil
|
||||
return strings.ReplaceAll(hostname, ":", "-")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -134,9 +141,9 @@ type HostsRecord struct {
|
||||
Canonical string
|
||||
}
|
||||
|
||||
// equal returns true if all fields of rec are equal to field in other or they
|
||||
// Equal returns true if all fields of rec are equal to field in other or they
|
||||
// both are nil.
|
||||
func (rec *HostsRecord) equal(other *HostsRecord) (ok bool) {
|
||||
func (rec *HostsRecord) Equal(other *HostsRecord) (ok bool) {
|
||||
if rec == nil {
|
||||
return other == nil
|
||||
} else if other == nil {
|
||||
@@ -488,7 +495,7 @@ func (hc *HostsContainer) refresh() (err error) {
|
||||
}
|
||||
|
||||
// hc.last is nil on the first refresh, so let that one through.
|
||||
if hc.last != nil && maps.EqualFunc(hp.table, hc.last, (*HostsRecord).equal) {
|
||||
if hc.last != nil && maps.EqualFunc(hp.table, hc.last, (*HostsRecord).Equal) {
|
||||
log.Debug("%s: no changes detected", hostsContainerPrefix)
|
||||
|
||||
return nil
|
||||
|
||||
144
internal/aghnet/hostscontainer_internal_test.go
Normal file
144
internal/aghnet/hostscontainer_internal_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"path"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil/fakefs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const nl = "\n"
|
||||
|
||||
func TestHostsContainer_PathsToPatterns(t *testing.T) {
|
||||
gsfs := fstest.MapFS{
|
||||
"dir_0/file_1": &fstest.MapFile{Data: []byte{1}},
|
||||
"dir_0/file_2": &fstest.MapFile{Data: []byte{2}},
|
||||
"dir_0/dir_1/file_3": &fstest.MapFile{Data: []byte{3}},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
paths []string
|
||||
want []string
|
||||
}{{
|
||||
name: "no_paths",
|
||||
paths: nil,
|
||||
want: nil,
|
||||
}, {
|
||||
name: "single_file",
|
||||
paths: []string{"dir_0/file_1"},
|
||||
want: []string{"dir_0/file_1"},
|
||||
}, {
|
||||
name: "several_files",
|
||||
paths: []string{"dir_0/file_1", "dir_0/file_2"},
|
||||
want: []string{"dir_0/file_1", "dir_0/file_2"},
|
||||
}, {
|
||||
name: "whole_dir",
|
||||
paths: []string{"dir_0"},
|
||||
want: []string{"dir_0/*"},
|
||||
}, {
|
||||
name: "file_and_dir",
|
||||
paths: []string{"dir_0/file_1", "dir_0/dir_1"},
|
||||
want: []string{"dir_0/file_1", "dir_0/dir_1/*"},
|
||||
}, {
|
||||
name: "non-existing",
|
||||
paths: []string{path.Join("dir_0", "file_3")},
|
||||
want: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
patterns, err := pathsToPatterns(gsfs, tc.paths)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.want, patterns)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("bad_file", func(t *testing.T) {
|
||||
const errStat errors.Error = "bad file"
|
||||
|
||||
badFS := &fakefs.StatFS{
|
||||
OnOpen: func(_ string) (f fs.File, err error) { panic("not implemented") },
|
||||
OnStat: func(name string) (fi fs.FileInfo, err error) {
|
||||
return nil, errStat
|
||||
},
|
||||
}
|
||||
|
||||
_, err := pathsToPatterns(badFS, []string{""})
|
||||
assert.ErrorIs(t, err, errStat)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUniqueRules_ParseLine(t *testing.T) {
|
||||
ip := netutil.IPv4Localhost()
|
||||
ipStr := ip.String()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
line string
|
||||
wantIP netip.Addr
|
||||
wantHosts []string
|
||||
}{{
|
||||
name: "simple",
|
||||
line: ipStr + ` hostname`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"hostname"},
|
||||
}, {
|
||||
name: "aliases",
|
||||
line: ipStr + ` hostname alias`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"hostname", "alias"},
|
||||
}, {
|
||||
name: "invalid_line",
|
||||
line: ipStr,
|
||||
wantIP: netip.Addr{},
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "invalid_line_hostname",
|
||||
line: ipStr + ` # hostname`,
|
||||
wantIP: ip,
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "commented_aliases",
|
||||
line: ipStr + ` hostname # alias`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"hostname"},
|
||||
}, {
|
||||
name: "whole_comment",
|
||||
line: `# ` + ipStr + ` hostname`,
|
||||
wantIP: netip.Addr{},
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "partial_comment",
|
||||
line: ipStr + ` host#name`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"host"},
|
||||
}, {
|
||||
name: "empty",
|
||||
line: ``,
|
||||
wantIP: netip.Addr{},
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "bad_hosts",
|
||||
line: ipStr + ` bad..host bad._tld empty.tld. ok.host`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"ok.host"},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
hp := hostsParser{}
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, hosts := hp.parseLine(tc.line)
|
||||
assert.Equal(t, tc.wantIP, got)
|
||||
assert.Equal(t, tc.wantHosts, hosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package aghnet
|
||||
package aghnet_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"path"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -12,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
@@ -24,10 +23,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
nl = "\n"
|
||||
sp = " "
|
||||
)
|
||||
const nl = "\n"
|
||||
|
||||
func TestNewHostsContainer(t *testing.T) {
|
||||
const dirname = "dir"
|
||||
@@ -48,11 +44,11 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
name: "one_file",
|
||||
paths: []string{p},
|
||||
}, {
|
||||
wantErr: ErrNoHostsPaths,
|
||||
wantErr: aghnet.ErrNoHostsPaths,
|
||||
name: "no_files",
|
||||
paths: []string{},
|
||||
}, {
|
||||
wantErr: ErrNoHostsPaths,
|
||||
wantErr: aghnet.ErrNoHostsPaths,
|
||||
name: "non-existent_file",
|
||||
paths: []string{path.Join(dirname, filename+"2")},
|
||||
}, {
|
||||
@@ -77,7 +73,7 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
return eventsCh
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(0, testFS, &aghtest.FSWatcher{
|
||||
hc, err := aghnet.NewHostsContainer(0, testFS, &aghtest.FSWatcher{
|
||||
OnEvents: onEvents,
|
||||
OnAdd: onAdd,
|
||||
OnClose: func() (err error) { return nil },
|
||||
@@ -103,7 +99,7 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
|
||||
t.Run("nil_fs", func(t *testing.T) {
|
||||
require.Panics(t, func() {
|
||||
_, _ = NewHostsContainer(0, nil, &aghtest.FSWatcher{
|
||||
_, _ = aghnet.NewHostsContainer(0, nil, &aghtest.FSWatcher{
|
||||
// Those shouldn't panic.
|
||||
OnEvents: func() (e <-chan struct{}) { return nil },
|
||||
OnAdd: func(name string) (err error) { return nil },
|
||||
@@ -114,7 +110,7 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
|
||||
t.Run("nil_watcher", func(t *testing.T) {
|
||||
require.Panics(t, func() {
|
||||
_, _ = NewHostsContainer(0, testFS, nil, p)
|
||||
_, _ = aghnet.NewHostsContainer(0, testFS, nil, p)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -127,7 +123,7 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
OnClose: func() (err error) { return nil },
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(0, testFS, errWatcher, p)
|
||||
hc, err := aghnet.NewHostsContainer(0, testFS, errWatcher, p)
|
||||
require.ErrorIs(t, err, errOnAdd)
|
||||
|
||||
assert.Nil(t, hc)
|
||||
@@ -158,11 +154,11 @@ func TestHostsContainer_refresh(t *testing.T) {
|
||||
OnClose: func() (err error) { return nil },
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(0, testFS, w, "dir")
|
||||
hc, err := aghnet.NewHostsContainer(0, testFS, w, "dir")
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, hc.Close)
|
||||
|
||||
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
||||
checkRefresh := func(t *testing.T, want *aghnet.HostsRecord) {
|
||||
t.Helper()
|
||||
|
||||
upd, ok := aghchan.MustReceive(hc.Upd(), 1*time.Second)
|
||||
@@ -175,11 +171,11 @@ func TestHostsContainer_refresh(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, rec)
|
||||
|
||||
assert.Truef(t, rec.equal(want), "%+v != %+v", rec, want)
|
||||
assert.Truef(t, rec.Equal(want), "%+v != %+v", rec, want)
|
||||
}
|
||||
|
||||
t.Run("initial_refresh", func(t *testing.T) {
|
||||
checkRefresh(t, &HostsRecord{
|
||||
checkRefresh(t, &aghnet.HostsRecord{
|
||||
Aliases: stringutil.NewSet(),
|
||||
Canonical: "hostname",
|
||||
})
|
||||
@@ -189,7 +185,7 @@ func TestHostsContainer_refresh(t *testing.T) {
|
||||
testFS["dir/file2"] = &fstest.MapFile{Data: []byte(ipStr + ` alias` + nl)}
|
||||
eventsCh <- event{}
|
||||
|
||||
checkRefresh(t, &HostsRecord{
|
||||
checkRefresh(t, &aghnet.HostsRecord{
|
||||
Aliases: stringutil.NewSet("alias"),
|
||||
Canonical: "hostname",
|
||||
})
|
||||
@@ -228,66 +224,6 @@ func TestHostsContainer_refresh(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestHostsContainer_PathsToPatterns(t *testing.T) {
|
||||
gsfs := fstest.MapFS{
|
||||
"dir_0/file_1": &fstest.MapFile{Data: []byte{1}},
|
||||
"dir_0/file_2": &fstest.MapFile{Data: []byte{2}},
|
||||
"dir_0/dir_1/file_3": &fstest.MapFile{Data: []byte{3}},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
paths []string
|
||||
want []string
|
||||
}{{
|
||||
name: "no_paths",
|
||||
paths: nil,
|
||||
want: nil,
|
||||
}, {
|
||||
name: "single_file",
|
||||
paths: []string{"dir_0/file_1"},
|
||||
want: []string{"dir_0/file_1"},
|
||||
}, {
|
||||
name: "several_files",
|
||||
paths: []string{"dir_0/file_1", "dir_0/file_2"},
|
||||
want: []string{"dir_0/file_1", "dir_0/file_2"},
|
||||
}, {
|
||||
name: "whole_dir",
|
||||
paths: []string{"dir_0"},
|
||||
want: []string{"dir_0/*"},
|
||||
}, {
|
||||
name: "file_and_dir",
|
||||
paths: []string{"dir_0/file_1", "dir_0/dir_1"},
|
||||
want: []string{"dir_0/file_1", "dir_0/dir_1/*"},
|
||||
}, {
|
||||
name: "non-existing",
|
||||
paths: []string{path.Join("dir_0", "file_3")},
|
||||
want: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
patterns, err := pathsToPatterns(gsfs, tc.paths)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.want, patterns)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("bad_file", func(t *testing.T) {
|
||||
const errStat errors.Error = "bad file"
|
||||
|
||||
badFS := &aghtest.StatFS{
|
||||
OnStat: func(name string) (fs.FileInfo, error) {
|
||||
return nil, errStat
|
||||
},
|
||||
}
|
||||
|
||||
_, err := pathsToPatterns(badFS, []string{""})
|
||||
assert.ErrorIs(t, err, errStat)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHostsContainer_Translate(t *testing.T) {
|
||||
stubWatcher := aghtest.FSWatcher{
|
||||
OnEvents: func() (e <-chan struct{}) { return nil },
|
||||
@@ -297,7 +233,7 @@ func TestHostsContainer_Translate(t *testing.T) {
|
||||
|
||||
require.NoError(t, fstest.TestFS(testdata, "etc_hosts"))
|
||||
|
||||
hc, err := NewHostsContainer(0, testdata, &stubWatcher, "etc_hosts")
|
||||
hc, err := aghnet.NewHostsContainer(0, testdata, &stubWatcher, "etc_hosts")
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, hc.Close)
|
||||
|
||||
@@ -527,7 +463,7 @@ func TestHostsContainer(t *testing.T) {
|
||||
OnClose: func() (err error) { return nil },
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(listID, testdata, &stubWatcher, "etc_hosts")
|
||||
hc, err := aghnet.NewHostsContainer(listID, testdata, &stubWatcher, "etc_hosts")
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, hc.Close)
|
||||
|
||||
@@ -558,69 +494,3 @@ func TestHostsContainer(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueRules_ParseLine(t *testing.T) {
|
||||
ip := netutil.IPv4Localhost()
|
||||
ipStr := ip.String()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
line string
|
||||
wantIP netip.Addr
|
||||
wantHosts []string
|
||||
}{{
|
||||
name: "simple",
|
||||
line: ipStr + ` hostname`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"hostname"},
|
||||
}, {
|
||||
name: "aliases",
|
||||
line: ipStr + ` hostname alias`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"hostname", "alias"},
|
||||
}, {
|
||||
name: "invalid_line",
|
||||
line: ipStr,
|
||||
wantIP: netip.Addr{},
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "invalid_line_hostname",
|
||||
line: ipStr + ` # hostname`,
|
||||
wantIP: ip,
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "commented_aliases",
|
||||
line: ipStr + ` hostname # alias`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"hostname"},
|
||||
}, {
|
||||
name: "whole_comment",
|
||||
line: `# ` + ipStr + ` hostname`,
|
||||
wantIP: netip.Addr{},
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "partial_comment",
|
||||
line: ipStr + ` host#name`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"host"},
|
||||
}, {
|
||||
name: "empty",
|
||||
line: ``,
|
||||
wantIP: netip.Addr{},
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "bad_hosts",
|
||||
line: ipStr + ` bad..host bad._tld empty.tld. ok.host`,
|
||||
wantIP: ip,
|
||||
wantHosts: []string{"ok.host"},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
hp := hostsParser{}
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, hosts := hp.parseLine(tc.line)
|
||||
assert.Equal(t, tc.wantIP, got)
|
||||
assert.Equal(t, tc.wantHosts, hosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package aghnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,6 +16,10 @@ import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// DialContextFunc is the semantic alias for dialing functions, such as
|
||||
// [http.Transport.DialContext].
|
||||
type DialContextFunc = func(ctx context.Context, network, addr string) (conn net.Conn, err error)
|
||||
|
||||
// Variables and functions to substitute in tests.
|
||||
var (
|
||||
// aghosRunCommand is the function to run shell commands.
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/testutil/fakefs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestIfaceSetStaticIP(t *testing.T) {
|
||||
Data: []byte(`nameserver 1.1.1.1`),
|
||||
},
|
||||
}
|
||||
panicFsys := &aghtest.FS{
|
||||
panicFsys := &fakefs.FS{
|
||||
OnOpen: func(name string) (fs.File, error) { panic("not implemented") },
|
||||
}
|
||||
|
||||
|
||||
334
internal/aghnet/net_internal_test.go
Normal file
334
internal/aghnet/net_internal_test.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testdata is the filesystem containing data for testing the package.
|
||||
var testdata fs.FS = os.DirFS("./testdata")
|
||||
|
||||
// substRootDirFS replaces the aghos.RootDirFS function used throughout the
|
||||
// package with fsys for tests ran under t.
|
||||
func substRootDirFS(t testing.TB, fsys fs.FS) {
|
||||
t.Helper()
|
||||
|
||||
prev := rootDirFS
|
||||
t.Cleanup(func() { rootDirFS = prev })
|
||||
rootDirFS = fsys
|
||||
}
|
||||
|
||||
// RunCmdFunc is the signature of aghos.RunCommand function.
|
||||
type RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)
|
||||
|
||||
// substShell replaces the the aghos.RunCommand function used throughout the
|
||||
// package with rc for tests ran under t.
|
||||
func substShell(t testing.TB, rc RunCmdFunc) {
|
||||
t.Helper()
|
||||
|
||||
prev := aghosRunCommand
|
||||
t.Cleanup(func() { aghosRunCommand = prev })
|
||||
aghosRunCommand = rc
|
||||
}
|
||||
|
||||
// mapShell is a substitution of aghos.RunCommand that maps the command to it's
|
||||
// execution result. It's only needed to simplify testing.
|
||||
//
|
||||
// TODO(e.burkov): Perhaps put all the shell interactions behind an interface.
|
||||
type mapShell map[string]struct {
|
||||
err error
|
||||
out string
|
||||
code int
|
||||
}
|
||||
|
||||
// theOnlyCmd returns mapShell that only handles a single command and arguments
|
||||
// combination from cmd.
|
||||
func theOnlyCmd(cmd string, code int, out string, err error) (s mapShell) {
|
||||
return mapShell{cmd: {code: code, out: out, err: err}}
|
||||
}
|
||||
|
||||
// RunCmd is a RunCmdFunc handled by s.
|
||||
func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err error) {
|
||||
key := strings.Join(append([]string{cmd}, args...), " ")
|
||||
ret, ok := s[key]
|
||||
if !ok {
|
||||
return 0, nil, fmt.Errorf("unexpected shell command %q", key)
|
||||
}
|
||||
|
||||
return ret.code, []byte(ret.out), ret.err
|
||||
}
|
||||
|
||||
// ifaceAddrsFunc is the signature of net.InterfaceAddrs function.
|
||||
type ifaceAddrsFunc func() (ifaces []net.Addr, err error)
|
||||
|
||||
// substNetInterfaceAddrs replaces the the net.InterfaceAddrs function used
|
||||
// throughout the package with f for tests ran under t.
|
||||
func substNetInterfaceAddrs(t *testing.T, f ifaceAddrsFunc) {
|
||||
t.Helper()
|
||||
|
||||
prev := netInterfaceAddrs
|
||||
t.Cleanup(func() { netInterfaceAddrs = prev })
|
||||
netInterfaceAddrs = f
|
||||
}
|
||||
|
||||
func TestGatewayIP(t *testing.T) {
|
||||
const ifaceName = "ifaceName"
|
||||
const cmd = "ip route show dev " + ifaceName
|
||||
|
||||
testCases := []struct {
|
||||
shell mapShell
|
||||
want netip.Addr
|
||||
name string
|
||||
}{{
|
||||
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
||||
want: netip.MustParseAddr("1.2.3.4"),
|
||||
name: "success_v4",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
||||
want: netip.MustParseAddr("::ffff"),
|
||||
name: "success_v6",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
||||
want: netip.Addr{},
|
||||
name: "bad_output",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
||||
want: netip.Addr{},
|
||||
name: "err_runcmd",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 1, "", nil),
|
||||
want: netip.Addr{},
|
||||
name: "bad_code",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
substShell(t, tc.shell.RunCmd)
|
||||
|
||||
assert.Equal(t, tc.want, GatewayIP(ifaceName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceByIP(t *testing.T) {
|
||||
ifaces, err := GetValidNetInterfacesForWeb()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, ifaces)
|
||||
|
||||
for _, iface := range ifaces {
|
||||
t.Run(iface.Name, func(t *testing.T) {
|
||||
require.NotEmpty(t, iface.Addresses)
|
||||
|
||||
for _, ip := range iface.Addresses {
|
||||
ifaceName := InterfaceByIP(ip)
|
||||
require.Equal(t, iface.Name, ifaceName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastFromIPNet(t *testing.T) {
|
||||
known4 := netip.MustParseAddr("192.168.0.1")
|
||||
fullBroadcast4 := netip.MustParseAddr("255.255.255.255")
|
||||
|
||||
known6 := netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:f10")
|
||||
|
||||
testCases := []struct {
|
||||
pref netip.Prefix
|
||||
want netip.Addr
|
||||
name string
|
||||
}{{
|
||||
pref: netip.PrefixFrom(known4, 0),
|
||||
want: fullBroadcast4,
|
||||
name: "full",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(known4, 20),
|
||||
want: netip.MustParseAddr("192.168.15.255"),
|
||||
name: "full",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(known6, netutil.IPv6BitLen),
|
||||
want: known6,
|
||||
name: "ipv6_no_mask",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(known4, netutil.IPv4BitLen),
|
||||
want: known4,
|
||||
name: "ipv4_no_mask",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
|
||||
want: fullBroadcast4,
|
||||
name: "unspecified",
|
||||
}, {
|
||||
pref: netip.Prefix{},
|
||||
want: netip.Addr{},
|
||||
name: "invalid",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, BroadcastFromPref(tc.pref))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPort(t *testing.T) {
|
||||
laddr := netip.AddrPortFrom(netutil.IPv4Localhost(), 0)
|
||||
|
||||
t.Run("tcp_bound", func(t *testing.T) {
|
||||
l, err := net.Listen("tcp", laddr.String())
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||
|
||||
ipp := testutil.RequireTypeAssert[*net.TCPAddr](t, l.Addr()).AddrPort()
|
||||
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||
require.NotZero(t, ipp.Port())
|
||||
|
||||
err = CheckPort("tcp", ipp)
|
||||
target := &net.OpError{}
|
||||
require.ErrorAs(t, err, &target)
|
||||
|
||||
assert.Equal(t, "listen", target.Op)
|
||||
})
|
||||
|
||||
t.Run("udp_bound", func(t *testing.T) {
|
||||
conn, err := net.ListenPacket("udp", laddr.String())
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, conn.Close)
|
||||
|
||||
ipp := testutil.RequireTypeAssert[*net.UDPAddr](t, conn.LocalAddr()).AddrPort()
|
||||
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||
require.NotZero(t, ipp.Port())
|
||||
|
||||
err = CheckPort("udp", ipp)
|
||||
target := &net.OpError{}
|
||||
require.ErrorAs(t, err, &target)
|
||||
|
||||
assert.Equal(t, "listen", target.Op)
|
||||
})
|
||||
|
||||
t.Run("bad_network", func(t *testing.T) {
|
||||
err := CheckPort("bad_network", netip.AddrPortFrom(netip.Addr{}, 0))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("can_bind", func(t *testing.T) {
|
||||
err := CheckPort("udp", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCollectAllIfacesAddrs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
addrs []net.Addr
|
||||
wantAddrs []string
|
||||
}{{
|
||||
name: "success",
|
||||
wantErrMsg: ``,
|
||||
addrs: []net.Addr{&net.IPNet{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
|
||||
}, &net.IPNet{
|
||||
IP: net.IP{4, 3, 2, 1},
|
||||
Mask: net.CIDRMask(16, netutil.IPv4BitLen),
|
||||
}},
|
||||
wantAddrs: []string{"1.2.3.4", "4.3.2.1"},
|
||||
}, {
|
||||
name: "not_cidr",
|
||||
wantErrMsg: `parsing cidr: invalid CIDR address: 1.2.3.4`,
|
||||
addrs: []net.Addr{&net.IPAddr{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
}},
|
||||
wantAddrs: nil,
|
||||
}, {
|
||||
name: "empty",
|
||||
wantErrMsg: ``,
|
||||
addrs: []net.Addr{},
|
||||
wantAddrs: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
substNetInterfaceAddrs(t, func() ([]net.Addr, error) { return tc.addrs, nil })
|
||||
|
||||
addrs, err := CollectAllIfacesAddrs()
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.wantAddrs, addrs)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("internal_error", func(t *testing.T) {
|
||||
const errAddrs errors.Error = "can't get addresses"
|
||||
const wantErrMsg string = `getting interfaces addresses: ` + string(errAddrs)
|
||||
|
||||
substNetInterfaceAddrs(t, func() ([]net.Addr, error) { return nil, errAddrs })
|
||||
|
||||
_, err := CollectAllIfacesAddrs()
|
||||
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsAddrInUse(t *testing.T) {
|
||||
t.Run("addr_in_use", func(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "0.0.0.0:0")
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||
|
||||
_, err = net.Listen(l.Addr().Network(), l.Addr().String())
|
||||
assert.True(t, IsAddrInUse(err))
|
||||
})
|
||||
|
||||
t.Run("another", func(t *testing.T) {
|
||||
const anotherErr errors.Error = "not addr in use"
|
||||
|
||||
assert.False(t, IsAddrInUse(anotherErr))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetInterface_MarshalJSON(t *testing.T) {
|
||||
const want = `{` +
|
||||
`"hardware_address":"aa:bb:cc:dd:ee:ff",` +
|
||||
`"flags":"up|multicast",` +
|
||||
`"ip_addresses":["1.2.3.4","aaaa::1"],` +
|
||||
`"name":"iface0",` +
|
||||
`"mtu":1500` +
|
||||
`}` + "\n"
|
||||
|
||||
ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
|
||||
require.True(t, ok)
|
||||
|
||||
ip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
require.True(t, ok)
|
||||
|
||||
net4 := netip.PrefixFrom(ip4, 24)
|
||||
net6 := netip.PrefixFrom(ip6, 8)
|
||||
|
||||
iface := &NetInterface{
|
||||
Addresses: []netip.Addr{ip4, ip6},
|
||||
Subnets: []netip.Prefix{net4, net6},
|
||||
Name: "iface0",
|
||||
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
||||
Flags: net.FlagUp | net.FlagMulticast,
|
||||
MTU: 1500,
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
err := json.NewEncoder(b).Encode(iface)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, want, b.String())
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/google/renameio/maybe"
|
||||
"github.com/google/renameio/v2/maybe"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
package aghnet
|
||||
package aghnet_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -24,315 +14,3 @@ func TestMain(m *testing.M) {
|
||||
|
||||
// testdata is the filesystem containing data for testing the package.
|
||||
var testdata fs.FS = os.DirFS("./testdata")
|
||||
|
||||
// substRootDirFS replaces the aghos.RootDirFS function used throughout the
|
||||
// package with fsys for tests ran under t.
|
||||
func substRootDirFS(t testing.TB, fsys fs.FS) {
|
||||
t.Helper()
|
||||
|
||||
prev := rootDirFS
|
||||
t.Cleanup(func() { rootDirFS = prev })
|
||||
rootDirFS = fsys
|
||||
}
|
||||
|
||||
// RunCmdFunc is the signature of aghos.RunCommand function.
|
||||
type RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)
|
||||
|
||||
// substShell replaces the the aghos.RunCommand function used throughout the
|
||||
// package with rc for tests ran under t.
|
||||
func substShell(t testing.TB, rc RunCmdFunc) {
|
||||
t.Helper()
|
||||
|
||||
prev := aghosRunCommand
|
||||
t.Cleanup(func() { aghosRunCommand = prev })
|
||||
aghosRunCommand = rc
|
||||
}
|
||||
|
||||
// mapShell is a substitution of aghos.RunCommand that maps the command to it's
|
||||
// execution result. It's only needed to simplify testing.
|
||||
//
|
||||
// TODO(e.burkov): Perhaps put all the shell interactions behind an interface.
|
||||
type mapShell map[string]struct {
|
||||
err error
|
||||
out string
|
||||
code int
|
||||
}
|
||||
|
||||
// theOnlyCmd returns mapShell that only handles a single command and arguments
|
||||
// combination from cmd.
|
||||
func theOnlyCmd(cmd string, code int, out string, err error) (s mapShell) {
|
||||
return mapShell{cmd: {code: code, out: out, err: err}}
|
||||
}
|
||||
|
||||
// RunCmd is a RunCmdFunc handled by s.
|
||||
func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err error) {
|
||||
key := strings.Join(append([]string{cmd}, args...), " ")
|
||||
ret, ok := s[key]
|
||||
if !ok {
|
||||
return 0, nil, fmt.Errorf("unexpected shell command %q", key)
|
||||
}
|
||||
|
||||
return ret.code, []byte(ret.out), ret.err
|
||||
}
|
||||
|
||||
// ifaceAddrsFunc is the signature of net.InterfaceAddrs function.
|
||||
type ifaceAddrsFunc func() (ifaces []net.Addr, err error)
|
||||
|
||||
// substNetInterfaceAddrs replaces the the net.InterfaceAddrs function used
|
||||
// throughout the package with f for tests ran under t.
|
||||
func substNetInterfaceAddrs(t *testing.T, f ifaceAddrsFunc) {
|
||||
t.Helper()
|
||||
|
||||
prev := netInterfaceAddrs
|
||||
t.Cleanup(func() { netInterfaceAddrs = prev })
|
||||
netInterfaceAddrs = f
|
||||
}
|
||||
|
||||
func TestGatewayIP(t *testing.T) {
|
||||
const ifaceName = "ifaceName"
|
||||
const cmd = "ip route show dev " + ifaceName
|
||||
|
||||
testCases := []struct {
|
||||
shell mapShell
|
||||
want netip.Addr
|
||||
name string
|
||||
}{{
|
||||
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
||||
want: netip.MustParseAddr("1.2.3.4"),
|
||||
name: "success_v4",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
||||
want: netip.MustParseAddr("::ffff"),
|
||||
name: "success_v6",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
||||
want: netip.Addr{},
|
||||
name: "bad_output",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
||||
want: netip.Addr{},
|
||||
name: "err_runcmd",
|
||||
}, {
|
||||
shell: theOnlyCmd(cmd, 1, "", nil),
|
||||
want: netip.Addr{},
|
||||
name: "bad_code",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
substShell(t, tc.shell.RunCmd)
|
||||
|
||||
assert.Equal(t, tc.want, GatewayIP(ifaceName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceByIP(t *testing.T) {
|
||||
ifaces, err := GetValidNetInterfacesForWeb()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, ifaces)
|
||||
|
||||
for _, iface := range ifaces {
|
||||
t.Run(iface.Name, func(t *testing.T) {
|
||||
require.NotEmpty(t, iface.Addresses)
|
||||
|
||||
for _, ip := range iface.Addresses {
|
||||
ifaceName := InterfaceByIP(ip)
|
||||
require.Equal(t, iface.Name, ifaceName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastFromIPNet(t *testing.T) {
|
||||
known4 := netip.MustParseAddr("192.168.0.1")
|
||||
fullBroadcast4 := netip.MustParseAddr("255.255.255.255")
|
||||
|
||||
known6 := netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:f10")
|
||||
|
||||
testCases := []struct {
|
||||
pref netip.Prefix
|
||||
want netip.Addr
|
||||
name string
|
||||
}{{
|
||||
pref: netip.PrefixFrom(known4, 0),
|
||||
want: fullBroadcast4,
|
||||
name: "full",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(known4, 20),
|
||||
want: netip.MustParseAddr("192.168.15.255"),
|
||||
name: "full",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(known6, netutil.IPv6BitLen),
|
||||
want: known6,
|
||||
name: "ipv6_no_mask",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(known4, netutil.IPv4BitLen),
|
||||
want: known4,
|
||||
name: "ipv4_no_mask",
|
||||
}, {
|
||||
pref: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
|
||||
want: fullBroadcast4,
|
||||
name: "unspecified",
|
||||
}, {
|
||||
pref: netip.Prefix{},
|
||||
want: netip.Addr{},
|
||||
name: "invalid",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, BroadcastFromPref(tc.pref))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPort(t *testing.T) {
|
||||
laddr := netip.AddrPortFrom(netutil.IPv4Localhost(), 0)
|
||||
|
||||
t.Run("tcp_bound", func(t *testing.T) {
|
||||
l, err := net.Listen("tcp", laddr.String())
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||
|
||||
ipp := testutil.RequireTypeAssert[*net.TCPAddr](t, l.Addr()).AddrPort()
|
||||
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||
require.NotZero(t, ipp.Port())
|
||||
|
||||
err = CheckPort("tcp", ipp)
|
||||
target := &net.OpError{}
|
||||
require.ErrorAs(t, err, &target)
|
||||
|
||||
assert.Equal(t, "listen", target.Op)
|
||||
})
|
||||
|
||||
t.Run("udp_bound", func(t *testing.T) {
|
||||
conn, err := net.ListenPacket("udp", laddr.String())
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, conn.Close)
|
||||
|
||||
ipp := testutil.RequireTypeAssert[*net.UDPAddr](t, conn.LocalAddr()).AddrPort()
|
||||
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||
require.NotZero(t, ipp.Port())
|
||||
|
||||
err = CheckPort("udp", ipp)
|
||||
target := &net.OpError{}
|
||||
require.ErrorAs(t, err, &target)
|
||||
|
||||
assert.Equal(t, "listen", target.Op)
|
||||
})
|
||||
|
||||
t.Run("bad_network", func(t *testing.T) {
|
||||
err := CheckPort("bad_network", netip.AddrPortFrom(netip.Addr{}, 0))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("can_bind", func(t *testing.T) {
|
||||
err := CheckPort("udp", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCollectAllIfacesAddrs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
addrs []net.Addr
|
||||
wantAddrs []string
|
||||
}{{
|
||||
name: "success",
|
||||
wantErrMsg: ``,
|
||||
addrs: []net.Addr{&net.IPNet{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
|
||||
}, &net.IPNet{
|
||||
IP: net.IP{4, 3, 2, 1},
|
||||
Mask: net.CIDRMask(16, netutil.IPv4BitLen),
|
||||
}},
|
||||
wantAddrs: []string{"1.2.3.4", "4.3.2.1"},
|
||||
}, {
|
||||
name: "not_cidr",
|
||||
wantErrMsg: `parsing cidr: invalid CIDR address: 1.2.3.4`,
|
||||
addrs: []net.Addr{&net.IPAddr{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
}},
|
||||
wantAddrs: nil,
|
||||
}, {
|
||||
name: "empty",
|
||||
wantErrMsg: ``,
|
||||
addrs: []net.Addr{},
|
||||
wantAddrs: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
substNetInterfaceAddrs(t, func() ([]net.Addr, error) { return tc.addrs, nil })
|
||||
|
||||
addrs, err := CollectAllIfacesAddrs()
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.wantAddrs, addrs)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("internal_error", func(t *testing.T) {
|
||||
const errAddrs errors.Error = "can't get addresses"
|
||||
const wantErrMsg string = `getting interfaces addresses: ` + string(errAddrs)
|
||||
|
||||
substNetInterfaceAddrs(t, func() ([]net.Addr, error) { return nil, errAddrs })
|
||||
|
||||
_, err := CollectAllIfacesAddrs()
|
||||
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsAddrInUse(t *testing.T) {
|
||||
t.Run("addr_in_use", func(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "0.0.0.0:0")
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||
|
||||
_, err = net.Listen(l.Addr().Network(), l.Addr().String())
|
||||
assert.True(t, IsAddrInUse(err))
|
||||
})
|
||||
|
||||
t.Run("another", func(t *testing.T) {
|
||||
const anotherErr errors.Error = "not addr in use"
|
||||
|
||||
assert.False(t, IsAddrInUse(anotherErr))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetInterface_MarshalJSON(t *testing.T) {
|
||||
const want = `{` +
|
||||
`"hardware_address":"aa:bb:cc:dd:ee:ff",` +
|
||||
`"flags":"up|multicast",` +
|
||||
`"ip_addresses":["1.2.3.4","aaaa::1"],` +
|
||||
`"name":"iface0",` +
|
||||
`"mtu":1500` +
|
||||
`}` + "\n"
|
||||
|
||||
ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
|
||||
require.True(t, ok)
|
||||
|
||||
ip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
require.True(t, ok)
|
||||
|
||||
net4 := netip.PrefixFrom(ip4, 24)
|
||||
net6 := netip.PrefixFrom(ip6, 8)
|
||||
|
||||
iface := &NetInterface{
|
||||
Addresses: []netip.Addr{ip4, ip6},
|
||||
Subnets: []netip.Prefix{net4, net6},
|
||||
Name: "iface0",
|
||||
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
||||
Flags: net.FlagUp | net.FlagMulticast,
|
||||
MTU: 1500,
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
err := json.NewEncoder(b).Encode(iface)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, want, b.String())
|
||||
}
|
||||
|
||||
52
internal/aghrenameio/renameio.go
Normal file
52
internal/aghrenameio/renameio.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Package aghrenameio is a wrapper around package github.com/google/renameio/v2
|
||||
// that provides a similar stream-based API for both Unix and Windows systems.
|
||||
// While the Windows API is not technically atomic, it still provides a
|
||||
// consistent stream-based interface, and atomic renames of files do not seem to
|
||||
// be possible in all cases anyway.
|
||||
//
|
||||
// See https://github.com/google/renameio/issues/1.
|
||||
//
|
||||
// TODO(a.garipov): Consider moving to golibs/renameioutil once tried and
|
||||
// tested.
|
||||
package aghrenameio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// PendingFile is the interface for pending temporary files.
|
||||
type PendingFile interface {
|
||||
// Cleanup closes the file, and removes it without performing the renaming.
|
||||
// To close and rename the file, use CloseReplace.
|
||||
Cleanup() (err error)
|
||||
|
||||
// CloseReplace closes the temporary file and replaces the destination file
|
||||
// with it, possibly atomically.
|
||||
//
|
||||
// This method is not safe for concurrent use by multiple goroutines.
|
||||
CloseReplace() (err error)
|
||||
|
||||
// Write writes len(b) bytes from b to the File. It returns the number of
|
||||
// bytes written and an error, if any. Write returns a non-nil error when n
|
||||
// != len(b).
|
||||
Write(b []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// NewPendingFile is a wrapper around [renameio.NewPendingFile] on Unix systems
|
||||
// and [os.CreateTemp] on Windows.
|
||||
func NewPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {
|
||||
return newPendingFile(filePath, mode)
|
||||
}
|
||||
|
||||
// WithDeferredCleanup is a helper that performs the necessary cleanups and
|
||||
// finalizations of the temporary files based on the returned error.
|
||||
func WithDeferredCleanup(returned error, file PendingFile) (err error) {
|
||||
// Make sure that any error returned from here is marked as a deferred one.
|
||||
if returned != nil {
|
||||
return errors.WithDeferred(returned, file.Cleanup())
|
||||
}
|
||||
|
||||
return errors.WithDeferred(nil, file.CloseReplace())
|
||||
}
|
||||
101
internal/aghrenameio/renameio_test.go
Normal file
101
internal/aghrenameio/renameio_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package aghrenameio_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testPerm is the common permission mode for tests.
|
||||
const testPerm fs.FileMode = 0o644
|
||||
|
||||
// Common file data for tests.
|
||||
var (
|
||||
initialData = []byte("initial data\n")
|
||||
newData = []byte("new data\n")
|
||||
)
|
||||
|
||||
func TestPendingFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
targetPath := newInitialFile(t)
|
||||
f, err := aghrenameio.NewPendingFile(targetPath, testPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Write(newData)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = f.CloseReplace()
|
||||
require.NoError(t, err)
|
||||
|
||||
gotData, err := os.ReadFile(targetPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, newData, gotData)
|
||||
}
|
||||
|
||||
// newInitialFile is a test helper that returns the path to the file containing
|
||||
// [initialData].
|
||||
func newInitialFile(t *testing.T) (targetPath string) {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
targetPath = filepath.Join(dir, "target")
|
||||
|
||||
err := os.WriteFile(targetPath, initialData, 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return targetPath
|
||||
}
|
||||
|
||||
func TestWithDeferredCleanup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const testError errors.Error = "test error"
|
||||
|
||||
testCases := []struct {
|
||||
error error
|
||||
name string
|
||||
wantErrMsg string
|
||||
wantData []byte
|
||||
}{{
|
||||
name: "success",
|
||||
error: nil,
|
||||
wantErrMsg: "",
|
||||
wantData: newData,
|
||||
}, {
|
||||
name: "error",
|
||||
error: testError,
|
||||
wantErrMsg: testError.Error(),
|
||||
wantData: initialData,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
targetPath := newInitialFile(t)
|
||||
f, err := aghrenameio.NewPendingFile(targetPath, testPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Write(newData)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = aghrenameio.WithDeferredCleanup(tc.error, f)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
gotData, err := os.ReadFile(targetPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantData, gotData)
|
||||
})
|
||||
}
|
||||
}
|
||||
48
internal/aghrenameio/renameio_unix.go
Normal file
48
internal/aghrenameio/renameio_unix.go
Normal file
@@ -0,0 +1,48 @@
|
||||
//go:build unix
|
||||
|
||||
package aghrenameio
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/google/renameio/v2"
|
||||
)
|
||||
|
||||
// pendingFile is a wrapper around [*renameio.PendingFile] making it an
|
||||
// [io.WriteCloser].
|
||||
type pendingFile struct {
|
||||
file *renameio.PendingFile
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ PendingFile = pendingFile{}
|
||||
|
||||
// Cleanup implements the [PendingFile] interface for pendingFile.
|
||||
func (f pendingFile) Cleanup() (err error) {
|
||||
return f.file.Cleanup()
|
||||
}
|
||||
|
||||
// CloseReplace implements the [PendingFile] interface for pendingFile.
|
||||
func (f pendingFile) CloseReplace() (err error) {
|
||||
return f.file.CloseAtomicallyReplace()
|
||||
}
|
||||
|
||||
// Write implements the [PendingFile] interface for pendingFile.
|
||||
func (f pendingFile) Write(b []byte) (n int, err error) {
|
||||
return f.file.Write(b)
|
||||
}
|
||||
|
||||
// NewPendingFile is a wrapper around [renameio.NewPendingFile].
|
||||
//
|
||||
// f.Close must be called to finish the renaming.
|
||||
func newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {
|
||||
file, err := renameio.NewPendingFile(filePath, renameio.WithPermissions(mode))
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pendingFile{
|
||||
file: file,
|
||||
}, nil
|
||||
}
|
||||
74
internal/aghrenameio/renameio_windows.go
Normal file
74
internal/aghrenameio/renameio_windows.go
Normal file
@@ -0,0 +1,74 @@
|
||||
//go:build windows
|
||||
|
||||
package aghrenameio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// pendingFile is a wrapper around [*os.File] calling [os.Rename] in its Close
|
||||
// method.
|
||||
type pendingFile struct {
|
||||
file *os.File
|
||||
targetPath string
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ PendingFile = (*pendingFile)(nil)
|
||||
|
||||
// Cleanup implements the [PendingFile] interface for *pendingFile.
|
||||
func (f *pendingFile) Cleanup() (err error) {
|
||||
closeErr := f.file.Close()
|
||||
err = os.Remove(f.file.Name())
|
||||
|
||||
// Put closeErr into the deferred error because that's where it is usually
|
||||
// expected.
|
||||
return errors.WithDeferred(err, closeErr)
|
||||
}
|
||||
|
||||
// CloseReplace implements the [PendingFile] interface for *pendingFile.
|
||||
func (f *pendingFile) CloseReplace() (err error) {
|
||||
err = f.file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing: %w", err)
|
||||
}
|
||||
|
||||
err = os.Rename(f.file.Name(), f.targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write implements the [PendingFile] interface for *pendingFile.
|
||||
func (f *pendingFile) Write(b []byte) (n int, err error) {
|
||||
return f.file.Write(b)
|
||||
}
|
||||
|
||||
// NewPendingFile is a wrapper around [os.CreateTemp].
|
||||
//
|
||||
// f.Close must be called to finish the renaming.
|
||||
func newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error) {
|
||||
// Use the same directory as the file itself, because moves across
|
||||
// filesystems can be especially problematic.
|
||||
file, err := os.CreateTemp(filepath.Dir(filePath), "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening pending file: %w", err)
|
||||
}
|
||||
|
||||
err = file.Chmod(mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("preparing pending file: %w", err)
|
||||
}
|
||||
|
||||
return &pendingFile{
|
||||
file: file,
|
||||
targetPath: filePath,
|
||||
}, nil
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
package aghtest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
@@ -34,3 +36,10 @@ func ReplaceLogLevel(t testing.TB, l log.Level) {
|
||||
t.Cleanup(func() { log.SetLevel(prev) })
|
||||
log.SetLevel(l)
|
||||
}
|
||||
|
||||
// HostToIPs is a helper that generates one IPv4 and one IPv6 address from host.
|
||||
func HostToIPs(host string) (ipv4, ipv6 net.IP) {
|
||||
hash := sha256.Sum256([]byte(host))
|
||||
|
||||
return net.IP(hash[:4]), net.IP(hash[4:20])
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ package aghtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -15,94 +19,20 @@ import (
|
||||
//
|
||||
// Keep entities in this file in alphabetic order.
|
||||
|
||||
// Standard Library
|
||||
|
||||
// Package fs
|
||||
|
||||
// type check
|
||||
var _ fs.FS = &FS{}
|
||||
|
||||
// FS is a mock [fs.FS] implementation for tests.
|
||||
type FS struct {
|
||||
OnOpen func(name string) (fs.File, error)
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
// GlobFS is a mock [fs.GlobFS] implementation for tests.
|
||||
type GlobFS struct {
|
||||
// FS is embedded here to avoid implementing all it's methods.
|
||||
FS
|
||||
OnGlob func(pattern string) ([]string, error)
|
||||
}
|
||||
|
||||
// Glob implements the [fs.GlobFS] interface for *GlobFS.
|
||||
func (fsys *GlobFS) Glob(pattern string) ([]string, error) {
|
||||
return fsys.OnGlob(pattern)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ fs.StatFS = &StatFS{}
|
||||
|
||||
// StatFS is a mock [fs.StatFS] implementation for tests.
|
||||
type StatFS struct {
|
||||
// FS is embedded here to avoid implementing all it's methods.
|
||||
FS
|
||||
OnStat func(name string) (fs.FileInfo, error)
|
||||
}
|
||||
|
||||
// Stat implements the [fs.StatFS] interface for *StatFS.
|
||||
func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
|
||||
return fsys.OnStat(name)
|
||||
}
|
||||
|
||||
// Package net
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Accept implements the [net.Listener] interface for *Listener.
|
||||
func (l *Listener) Accept() (conn net.Conn, err error) {
|
||||
return l.OnAccept()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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 +50,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) {
|
||||
@@ -148,14 +78,76 @@ func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||
return s.OnConfig()
|
||||
}
|
||||
|
||||
// Package client
|
||||
|
||||
// AddressProcessor is a fake [client.AddressProcessor] implementation for
|
||||
// tests.
|
||||
type AddressProcessor struct {
|
||||
OnProcess func(ip netip.Addr)
|
||||
OnClose func() (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ client.AddressProcessor = (*AddressProcessor)(nil)
|
||||
|
||||
// Process implements the [client.AddressProcessor] interface for
|
||||
// *AddressProcessor.
|
||||
func (p *AddressProcessor) Process(ip netip.Addr) {
|
||||
p.OnProcess(ip)
|
||||
}
|
||||
|
||||
// Close implements the [client.AddressProcessor] interface for
|
||||
// *AddressProcessor.
|
||||
func (p *AddressProcessor) Close() (err error) {
|
||||
return p.OnClose()
|
||||
}
|
||||
|
||||
// AddressUpdater is a fake [client.AddressUpdater] implementation for tests.
|
||||
type AddressUpdater struct {
|
||||
OnUpdateAddress func(ip netip.Addr, host string, info *whois.Info)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ client.AddressUpdater = (*AddressUpdater)(nil)
|
||||
|
||||
// UpdateAddress implements the [client.AddressUpdater] interface for
|
||||
// *AddressUpdater.
|
||||
func (p *AddressUpdater) UpdateAddress(ip netip.Addr, host string, info *whois.Info) {
|
||||
p.OnUpdateAddress(ip, host, info)
|
||||
}
|
||||
|
||||
// Package filtering
|
||||
|
||||
// Resolver is a fake [filtering.Resolver] implementation for tests.
|
||||
type Resolver struct {
|
||||
OnLookupIP func(ctx context.Context, network, host string) (ips []net.IP, err error)
|
||||
}
|
||||
|
||||
// LookupIP implements the [filtering.Resolver] interface for *Resolver.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, network, host string) (ips []net.IP, err error) {
|
||||
return r.OnLookupIP(ctx, network, host)
|
||||
}
|
||||
|
||||
// Package rdns
|
||||
|
||||
// Exchanger is a fake [rdns.Exchanger] implementation for tests.
|
||||
type Exchanger struct {
|
||||
OnExchange func(ip netip.Addr) (host string, ttl time.Duration, err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ rdns.Exchanger = (*Exchanger)(nil)
|
||||
|
||||
// Exchange implements [rdns.Exchanger] interface for *Exchanger.
|
||||
func (e *Exchanger) Exchange(ip netip.Addr) (host string, ttl time.Duration, err error) {
|
||||
return e.OnExchange(ip)
|
||||
}
|
||||
|
||||
// Module dnsproxy
|
||||
|
||||
// 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 +157,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()
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
package aghtest_test
|
||||
|
||||
import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
)
|
||||
|
||||
// Put interface checks that cause import cycles here.
|
||||
|
||||
// type check
|
||||
var _ filtering.Resolver = (*aghtest.Resolver)(nil)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package aghtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TestResolver is a Resolver for tests.
|
||||
type TestResolver struct {
|
||||
counter int
|
||||
counterLock sync.Mutex
|
||||
}
|
||||
|
||||
// HostToIPs generates IPv4 and IPv6 from host.
|
||||
func (r *TestResolver) HostToIPs(host string) (ipv4, ipv6 net.IP) {
|
||||
hash := sha256.Sum256([]byte(host))
|
||||
|
||||
return net.IP(hash[:4]), net.IP(hash[4:20])
|
||||
}
|
||||
|
||||
// LookupIP implements Resolver interface for *testResolver. It returns the
|
||||
// slice of net.IP with IPv4 and IPv6 instances.
|
||||
func (r *TestResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) {
|
||||
ipv4, ipv6 := r.HostToIPs(host)
|
||||
addrs := []net.IP{ipv4, ipv6}
|
||||
|
||||
r.counterLock.Lock()
|
||||
defer r.counterLock.Unlock()
|
||||
r.counter++
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// LookupHost implements Resolver interface for *testResolver. It returns the
|
||||
// slice of IPv4 and IPv6 instances converted to strings.
|
||||
func (r *TestResolver) LookupHost(host string) (addrs []string, err error) {
|
||||
ipv4, ipv6 := r.HostToIPs(host)
|
||||
|
||||
r.counterLock.Lock()
|
||||
defer r.counterLock.Unlock()
|
||||
r.counter++
|
||||
|
||||
return []string{
|
||||
ipv4.String(),
|
||||
ipv6.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Counter returns the number of requests handled.
|
||||
func (r *TestResolver) Counter() int {
|
||||
r.counterLock.Lock()
|
||||
defer r.counterLock.Unlock()
|
||||
|
||||
return r.counter
|
||||
}
|
||||
294
internal/client/addrproc.go
Normal file
294
internal/client/addrproc.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
// ErrClosed is returned from [AddressProcessor.Close] if it's closed more than
|
||||
// once.
|
||||
const ErrClosed errors.Error = "use of closed address processor"
|
||||
|
||||
// AddressProcessor is the interface for types that can process clients.
|
||||
type AddressProcessor interface {
|
||||
Process(ip netip.Addr)
|
||||
Close() (err error)
|
||||
}
|
||||
|
||||
// EmptyAddrProc is an [AddressProcessor] that does nothing.
|
||||
type EmptyAddrProc struct{}
|
||||
|
||||
// type check
|
||||
var _ AddressProcessor = EmptyAddrProc{}
|
||||
|
||||
// Process implements the [AddressProcessor] interface for EmptyAddrProc.
|
||||
func (EmptyAddrProc) Process(_ netip.Addr) {}
|
||||
|
||||
// Close implements the [AddressProcessor] interface for EmptyAddrProc.
|
||||
func (EmptyAddrProc) Close() (_ error) { return nil }
|
||||
|
||||
// DefaultAddrProcConfig is the configuration structure for address processors.
|
||||
type DefaultAddrProcConfig struct {
|
||||
// DialContext is used to create TCP connections to WHOIS servers.
|
||||
// DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.
|
||||
DialContext aghnet.DialContextFunc
|
||||
|
||||
// Exchanger is used to perform rDNS queries. Exchanger must not be nil if
|
||||
// [DefaultAddrProcConfig.UseRDNS] is true.
|
||||
Exchanger rdns.Exchanger
|
||||
|
||||
// PrivateSubnets are used to determine if an incoming IP address is
|
||||
// private. It must not be nil.
|
||||
PrivateSubnets netutil.SubnetSet
|
||||
|
||||
// AddressUpdater is used to update the information about a client's IP
|
||||
// address. It must not be nil.
|
||||
AddressUpdater AddressUpdater
|
||||
|
||||
// InitialAddresses are the addresses that are queued for processing
|
||||
// immediately by [NewDefaultAddrProc].
|
||||
InitialAddresses []netip.Addr
|
||||
|
||||
// UseRDNS, if true, enables resolving of client IP addresses using reverse
|
||||
// DNS.
|
||||
UseRDNS bool
|
||||
|
||||
// UsePrivateRDNS, if true, enables resolving of private client IP addresses
|
||||
// using reverse DNS. See [DefaultAddrProcConfig.PrivateSubnets].
|
||||
UsePrivateRDNS bool
|
||||
|
||||
// UseWHOIS, if true, enables resolving of client IP addresses using WHOIS.
|
||||
UseWHOIS bool
|
||||
}
|
||||
|
||||
// AddressUpdater is the interface for storages of DNS clients that can update
|
||||
// information about them.
|
||||
//
|
||||
// TODO(a.garipov): Consider using the actual client storage once it is moved
|
||||
// into this package.
|
||||
type AddressUpdater interface {
|
||||
// UpdateAddress updates information about an IP address, setting host (if
|
||||
// not empty) and WHOIS information (if not nil).
|
||||
UpdateAddress(ip netip.Addr, host string, info *whois.Info)
|
||||
}
|
||||
|
||||
// DefaultAddrProc processes incoming client addresses with rDNS and WHOIS, if
|
||||
// configured, and updates that information in a client storage.
|
||||
type DefaultAddrProc struct {
|
||||
// clientIPsMu serializes closure of clientIPs and access to isClosed.
|
||||
clientIPsMu *sync.Mutex
|
||||
|
||||
// clientIPs is the channel queueing client processing tasks.
|
||||
clientIPs chan netip.Addr
|
||||
|
||||
// rdns is used to perform rDNS lookups of clients' IP addresses.
|
||||
rdns rdns.Interface
|
||||
|
||||
// whois is used to perform WHOIS lookups of clients' IP addresses.
|
||||
whois whois.Interface
|
||||
|
||||
// addrUpdater is used to update the information about a client's IP
|
||||
// address.
|
||||
addrUpdater AddressUpdater
|
||||
|
||||
// privateSubnets are used to determine if an incoming IP address is
|
||||
// private.
|
||||
privateSubnets netutil.SubnetSet
|
||||
|
||||
// isClosed is set to true once the address processor is closed.
|
||||
isClosed bool
|
||||
|
||||
// usePrivateRDNS, if true, enables resolving of private client IP addresses
|
||||
// using reverse DNS.
|
||||
usePrivateRDNS bool
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
// NewDefaultAddrProc returns a new running client address processor. c must
|
||||
// not be nil.
|
||||
func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
|
||||
p = &DefaultAddrProc{
|
||||
clientIPsMu: &sync.Mutex{},
|
||||
clientIPs: make(chan netip.Addr, defaultQueueSize),
|
||||
rdns: &rdns.Empty{},
|
||||
addrUpdater: c.AddressUpdater,
|
||||
whois: &whois.Empty{},
|
||||
privateSubnets: c.PrivateSubnets,
|
||||
usePrivateRDNS: c.UsePrivateRDNS,
|
||||
}
|
||||
|
||||
if c.UseRDNS {
|
||||
p.rdns = rdns.New(&rdns.Config{
|
||||
Exchanger: c.Exchanger,
|
||||
CacheSize: defaultCacheSize,
|
||||
CacheTTL: defaultIPTTL,
|
||||
})
|
||||
}
|
||||
|
||||
if c.UseWHOIS {
|
||||
p.whois = newWHOIS(c.DialContext)
|
||||
}
|
||||
|
||||
go p.process()
|
||||
|
||||
for _, ip := range c.InitialAddresses {
|
||||
p.Process(ip)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// newWHOIS returns a whois.Interface instance using the given function for
|
||||
// dialing.
|
||||
func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) {
|
||||
// TODO(s.chzhen): Consider making configurable.
|
||||
const (
|
||||
// defaultTimeout is the timeout for WHOIS requests.
|
||||
defaultTimeout = 5 * time.Second
|
||||
|
||||
// defaultMaxConnReadSize is an upper limit in bytes for reading from a
|
||||
// net.Conn.
|
||||
defaultMaxConnReadSize = 64 * 1024
|
||||
|
||||
// defaultMaxRedirects is the maximum redirects count.
|
||||
defaultMaxRedirects = 5
|
||||
|
||||
// defaultMaxInfoLen is the maximum length of whois.Info fields.
|
||||
defaultMaxInfoLen = 250
|
||||
)
|
||||
|
||||
return whois.New(&whois.Config{
|
||||
DialContext: dialFunc,
|
||||
ServerAddr: whois.DefaultServer,
|
||||
Port: whois.DefaultPort,
|
||||
Timeout: defaultTimeout,
|
||||
CacheSize: defaultCacheSize,
|
||||
MaxConnReadSize: defaultMaxConnReadSize,
|
||||
MaxRedirects: defaultMaxRedirects,
|
||||
MaxInfoLen: defaultMaxInfoLen,
|
||||
CacheTTL: defaultIPTTL,
|
||||
})
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ AddressProcessor = (*DefaultAddrProc)(nil)
|
||||
|
||||
// Process implements the [AddressProcessor] interface for *DefaultAddrProc.
|
||||
func (p *DefaultAddrProc) Process(ip netip.Addr) {
|
||||
p.clientIPsMu.Lock()
|
||||
defer p.clientIPsMu.Unlock()
|
||||
|
||||
if p.isClosed {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case p.clientIPs <- ip:
|
||||
// Go on.
|
||||
default:
|
||||
log.Debug("clients: ip channel is full; len: %d", len(p.clientIPs))
|
||||
}
|
||||
}
|
||||
|
||||
// process processes the incoming client IP-address information. It is intended
|
||||
// to be used as a goroutine. Once clientIPs is closed, process exits.
|
||||
func (p *DefaultAddrProc) process() {
|
||||
defer log.OnPanic("addrProcessor.process")
|
||||
|
||||
log.Info("clients: processing addresses")
|
||||
|
||||
for ip := range p.clientIPs {
|
||||
host := p.processRDNS(ip)
|
||||
info := p.processWHOIS(ip)
|
||||
|
||||
p.addrUpdater.UpdateAddress(ip, host, info)
|
||||
}
|
||||
|
||||
log.Info("clients: finished processing addresses")
|
||||
}
|
||||
|
||||
// processRDNS resolves the clients' IP addresses using reverse DNS. host is
|
||||
// empty if there were errors or if the information hasn't changed.
|
||||
func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
|
||||
start := time.Now()
|
||||
log.Debug("clients: processing %s with rdns", ip)
|
||||
defer func() {
|
||||
log.Debug("clients: finished processing %s with rdns in %s", ip, time.Since(start))
|
||||
}()
|
||||
|
||||
ok := p.shouldResolve(ip)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
host, changed := p.rdns.Process(ip)
|
||||
if !changed {
|
||||
host = ""
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// shouldResolve returns false if ip is a loopback address, or ip is private and
|
||||
// resolving of private addresses is disabled.
|
||||
func (p *DefaultAddrProc) shouldResolve(ip netip.Addr) (ok bool) {
|
||||
return !ip.IsLoopback() &&
|
||||
(p.usePrivateRDNS || !p.privateSubnets.Contains(ip.AsSlice()))
|
||||
}
|
||||
|
||||
// processWHOIS looks up the information about clients' IP addresses in the
|
||||
// WHOIS databases. info is nil if there were errors or if the information
|
||||
// hasn't changed.
|
||||
func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) {
|
||||
start := time.Now()
|
||||
log.Debug("clients: processing %s with whois", ip)
|
||||
defer func() {
|
||||
log.Debug("clients: finished processing %s with whois in %s", ip, time.Since(start))
|
||||
}()
|
||||
|
||||
// TODO(s.chzhen): Move the timeout logic from WHOIS configuration to the
|
||||
// context.
|
||||
info, changed := p.whois.Process(context.Background(), ip)
|
||||
if !changed {
|
||||
info = nil
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// Close implements the [AddressProcessor] interface for *DefaultAddrProc.
|
||||
func (p *DefaultAddrProc) Close() (err error) {
|
||||
p.clientIPsMu.Lock()
|
||||
defer p.clientIPsMu.Unlock()
|
||||
|
||||
if p.isClosed {
|
||||
return ErrClosed
|
||||
}
|
||||
|
||||
close(p.clientIPs)
|
||||
p.isClosed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
259
internal/client/addrproc_test.go
Normal file
259
internal/client/addrproc_test.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/testutil/fakenet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEmptyAddrProc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := client.EmptyAddrProc{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
p.Process(testIP)
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
err := p.Close()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultAddrProc_Process_rDNS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
privateIP := netip.MustParseAddr("192.168.0.1")
|
||||
|
||||
testCases := []struct {
|
||||
rdnsErr error
|
||||
ip netip.Addr
|
||||
name string
|
||||
host string
|
||||
usePrivate bool
|
||||
wantUpd bool
|
||||
}{{
|
||||
rdnsErr: nil,
|
||||
ip: testIP,
|
||||
name: "success",
|
||||
host: testHost,
|
||||
usePrivate: false,
|
||||
wantUpd: true,
|
||||
}, {
|
||||
rdnsErr: nil,
|
||||
ip: testIP,
|
||||
name: "no_host",
|
||||
host: "",
|
||||
usePrivate: false,
|
||||
wantUpd: false,
|
||||
}, {
|
||||
rdnsErr: nil,
|
||||
ip: netip.MustParseAddr("127.0.0.1"),
|
||||
name: "localhost",
|
||||
host: "",
|
||||
usePrivate: false,
|
||||
wantUpd: false,
|
||||
}, {
|
||||
rdnsErr: nil,
|
||||
ip: privateIP,
|
||||
name: "private_ignored",
|
||||
host: "",
|
||||
usePrivate: false,
|
||||
wantUpd: false,
|
||||
}, {
|
||||
rdnsErr: nil,
|
||||
ip: privateIP,
|
||||
name: "private_processed",
|
||||
host: "private.example",
|
||||
usePrivate: true,
|
||||
wantUpd: true,
|
||||
}, {
|
||||
rdnsErr: errors.Error("rdns error"),
|
||||
ip: testIP,
|
||||
name: "rdns_error",
|
||||
host: "",
|
||||
usePrivate: false,
|
||||
wantUpd: false,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
updIPCh := make(chan netip.Addr, 1)
|
||||
updHostCh := make(chan string, 1)
|
||||
updInfoCh := make(chan *whois.Info, 1)
|
||||
|
||||
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
Exchanger: &aghtest.Exchanger{
|
||||
OnExchange: func(ip netip.Addr) (host string, ttl time.Duration, err error) {
|
||||
return tc.host, 0, tc.rdnsErr
|
||||
},
|
||||
},
|
||||
PrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
AddressUpdater: &aghtest.AddressUpdater{
|
||||
OnUpdateAddress: newOnUpdateAddress(tc.wantUpd, updIPCh, updHostCh, updInfoCh),
|
||||
},
|
||||
UseRDNS: true,
|
||||
UsePrivateRDNS: tc.usePrivate,
|
||||
UseWHOIS: false,
|
||||
})
|
||||
testutil.CleanupAndRequireSuccess(t, p.Close)
|
||||
|
||||
p.Process(tc.ip)
|
||||
|
||||
if !tc.wantUpd {
|
||||
return
|
||||
}
|
||||
|
||||
gotIP, _ := testutil.RequireReceive(t, updIPCh, testTimeout)
|
||||
assert.Equal(t, tc.ip, gotIP)
|
||||
|
||||
gotHost, _ := testutil.RequireReceive(t, updHostCh, testTimeout)
|
||||
assert.Equal(t, tc.host, gotHost)
|
||||
|
||||
gotInfo, _ := testutil.RequireReceive(t, updInfoCh, testTimeout)
|
||||
assert.Nil(t, gotInfo)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// newOnUpdateAddress is a test helper that returns a new OnUpdateAddress
|
||||
// callback using the provided channels if an update is expected and panicking
|
||||
// otherwise.
|
||||
func newOnUpdateAddress(
|
||||
want bool,
|
||||
ips chan<- netip.Addr,
|
||||
hosts chan<- string,
|
||||
infos chan<- *whois.Info,
|
||||
) (f func(ip netip.Addr, host string, info *whois.Info)) {
|
||||
return func(ip netip.Addr, host string, info *whois.Info) {
|
||||
if !want {
|
||||
panic("got unexpected update")
|
||||
}
|
||||
|
||||
ips <- ip
|
||||
hosts <- host
|
||||
infos <- info
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAddrProc_Process_WHOIS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
wantInfo *whois.Info
|
||||
exchErr error
|
||||
name string
|
||||
wantUpd bool
|
||||
}{{
|
||||
wantInfo: &whois.Info{
|
||||
City: testWHOISCity,
|
||||
},
|
||||
exchErr: nil,
|
||||
name: "success",
|
||||
wantUpd: true,
|
||||
}, {
|
||||
wantInfo: nil,
|
||||
exchErr: nil,
|
||||
name: "no_info",
|
||||
wantUpd: false,
|
||||
}, {
|
||||
wantInfo: nil,
|
||||
exchErr: errors.Error("whois error"),
|
||||
name: "whois_error",
|
||||
wantUpd: false,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
whoisConn := &fakenet.Conn{
|
||||
OnClose: func() (err error) { return nil },
|
||||
OnRead: func(b []byte) (n int, err error) {
|
||||
if tc.wantInfo == nil {
|
||||
return 0, tc.exchErr
|
||||
}
|
||||
|
||||
data := "city: " + tc.wantInfo.City + "\n"
|
||||
copy(b, data)
|
||||
|
||||
return len(data), io.EOF
|
||||
},
|
||||
OnSetDeadline: func(_ time.Time) (err error) { return nil },
|
||||
OnWrite: func(b []byte) (n int, err error) { return len(b), nil },
|
||||
}
|
||||
|
||||
updIPCh := make(chan netip.Addr, 1)
|
||||
updHostCh := make(chan string, 1)
|
||||
updInfoCh := make(chan *whois.Info, 1)
|
||||
|
||||
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||
return whoisConn, nil
|
||||
},
|
||||
Exchanger: &aghtest.Exchanger{
|
||||
OnExchange: func(_ netip.Addr) (_ string, _ time.Duration, _ error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
},
|
||||
PrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
AddressUpdater: &aghtest.AddressUpdater{
|
||||
OnUpdateAddress: newOnUpdateAddress(tc.wantUpd, updIPCh, updHostCh, updInfoCh),
|
||||
},
|
||||
UseRDNS: false,
|
||||
UsePrivateRDNS: false,
|
||||
UseWHOIS: true,
|
||||
})
|
||||
testutil.CleanupAndRequireSuccess(t, p.Close)
|
||||
|
||||
p.Process(testIP)
|
||||
|
||||
if !tc.wantUpd {
|
||||
return
|
||||
}
|
||||
|
||||
gotIP, _ := testutil.RequireReceive(t, updIPCh, testTimeout)
|
||||
assert.Equal(t, testIP, gotIP)
|
||||
|
||||
gotHost, _ := testutil.RequireReceive(t, updHostCh, testTimeout)
|
||||
assert.Empty(t, gotHost)
|
||||
|
||||
gotInfo, _ := testutil.RequireReceive(t, updInfoCh, testTimeout)
|
||||
assert.Equal(t, tc.wantInfo, gotInfo)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAddrProc_Close(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{})
|
||||
|
||||
err := p.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = p.Close()
|
||||
assert.ErrorIs(t, err, client.ErrClosed)
|
||||
}
|
||||
5
internal/client/client.go
Normal file
5
internal/client/client.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// Package client contains types and logic dealing with AdGuard Home's DNS
|
||||
// clients.
|
||||
//
|
||||
// TODO(a.garipov): Expand.
|
||||
package client
|
||||
25
internal/client/client_test.go
Normal file
25
internal/client/client_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutil.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// testHost is the common hostname for tests.
|
||||
const testHost = "client.example"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testWHOISCity is the common city for tests.
|
||||
const testWHOISCity = "Brussels"
|
||||
|
||||
// testIP is the common IP address for tests.
|
||||
var testIP = netip.MustParseAddr("1.2.3.4")
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/google/renameio/maybe"
|
||||
"github.com/google/renameio/v2/maybe"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"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"
|
||||
@@ -271,7 +271,13 @@ type ServerConfig struct {
|
||||
UDPListenAddrs []*net.UDPAddr // UDP listen address
|
||||
TCPListenAddrs []*net.TCPAddr // TCP listen address
|
||||
UpstreamConfig *proxy.UpstreamConfig // Upstream DNS servers config
|
||||
OnDNSRequest func(d *proxy.DNSContext)
|
||||
|
||||
// AddrProcConf defines the configuration for the client IP processor.
|
||||
// If nil, [client.EmptyAddrProc] is used.
|
||||
//
|
||||
// TODO(a.garipov): The use of [client.EmptyAddrProc] is a crutch for tests.
|
||||
// Remove that.
|
||||
AddrProcConf *client.DefaultAddrProcConfig
|
||||
|
||||
FilteringConfig
|
||||
TLSConfig
|
||||
@@ -299,9 +305,6 @@ type ServerConfig struct {
|
||||
// DNS64Prefixes is a slice of NAT64 prefixes to be used for DNS64.
|
||||
DNS64Prefixes []netip.Prefix
|
||||
|
||||
// ResolveClients signals if the RDNS should resolve clients' addresses.
|
||||
ResolveClients bool
|
||||
|
||||
// UsePrivateRDNS defines if the PTR requests for unknown addresses from
|
||||
// locally-served networks should be resolved via private PTR resolvers.
|
||||
UsePrivateRDNS bool
|
||||
@@ -341,6 +344,7 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
||||
UpstreamConfig: srvConf.UpstreamConfig,
|
||||
BeforeRequestHandler: s.beforeRequestHandler,
|
||||
RequestHandler: s.handleDNSRequest,
|
||||
HTTPSServerName: aghhttp.UserAgent(),
|
||||
EnableEDNSClientSubnet: srvConf.EDNSClientSubnet.Enabled,
|
||||
MaxGoroutines: int(srvConf.MaxGoroutines),
|
||||
UseDNS64: srvConf.UseDNS64,
|
||||
@@ -436,102 +440,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 +448,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
|
||||
|
||||
57
internal/dnsforward/dialcontext.go
Normal file
57
internal/dnsforward/dialcontext.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames.
|
||||
func (s *Server) DialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
||||
log.Debug("dnsforward: dialing %q for network %q", addr, network)
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
// TODO(a.garipov): Consider making configurable.
|
||||
Timeout: time.Minute * 5,
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
addrs, err := s.Resolve(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving %q: %w", host, err)
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: resolving %q: %v", host, addrs)
|
||||
|
||||
if len(addrs) == 0 {
|
||||
return nil, fmt.Errorf("no addresses for host %q", host)
|
||||
}
|
||||
|
||||
var dialErrs []error
|
||||
for _, a := range addrs {
|
||||
addr = net.JoinHostPort(a.String(), port)
|
||||
conn, err = dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
dialErrs = append(dialErrs, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Use errors.Join in Go 1.20.
|
||||
return nil, errors.List(fmt.Sprintf("dialing %q", addr), dialErrs...)
|
||||
}
|
||||
@@ -14,9 +14,11 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"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"
|
||||
@@ -98,8 +100,17 @@ type Server struct {
|
||||
// must be a valid domain name plus dots on each side.
|
||||
localDomainSuffix string
|
||||
|
||||
ipset ipsetCtx
|
||||
privateNets netutil.SubnetSet
|
||||
ipset ipsetCtx
|
||||
privateNets netutil.SubnetSet
|
||||
|
||||
// addrProc, if not nil, is used to process clients' IP addresses with rDNS,
|
||||
// WHOIS, etc.
|
||||
addrProc client.AddressProcessor
|
||||
|
||||
// localResolvers is a DNS proxy instance used to resolve PTR records for
|
||||
// addresses considered private as per the [privateNets].
|
||||
//
|
||||
// TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy.
|
||||
localResolvers *proxy.Proxy
|
||||
sysResolvers aghnet.SystemResolvers
|
||||
|
||||
@@ -169,6 +180,9 @@ const (
|
||||
|
||||
// NewServer creates a new instance of the dnsforward.Server
|
||||
// Note: this function must be called only once
|
||||
//
|
||||
// TODO(a.garipov): How many constructors and initializers does this thing have?
|
||||
// Refactor!
|
||||
func NewServer(p DNSCreateParams) (s *Server, err error) {
|
||||
var localDomainSuffix string
|
||||
if p.LocalDomain == "" {
|
||||
@@ -256,14 +270,25 @@ func (s *Server) WriteDiskConfig(c *FilteringConfig) {
|
||||
c.UpstreamDNS = stringutil.CloneSlice(sc.UpstreamDNS)
|
||||
}
|
||||
|
||||
// RDNSSettings returns the copy of actual RDNS configuration.
|
||||
func (s *Server) RDNSSettings() (localPTRResolvers []string, resolveClients, resolvePTR bool) {
|
||||
// LocalPTRResolvers returns the current local PTR resolver configuration.
|
||||
func (s *Server) LocalPTRResolvers() (localPTRResolvers []string) {
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
return stringutil.CloneSlice(s.conf.LocalPTRResolvers),
|
||||
s.conf.ResolveClients,
|
||||
s.conf.UsePrivateRDNS
|
||||
return stringutil.CloneSlice(s.conf.LocalPTRResolvers)
|
||||
}
|
||||
|
||||
// AddrProcConfig returns the current address processing configuration. Only
|
||||
// fields c.UsePrivateRDNS, c.UseRDNS, and c.UseWHOIS are filled.
|
||||
func (s *Server) AddrProcConfig() (c *client.DefaultAddrProcConfig) {
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
return &client.DefaultAddrProcConfig{
|
||||
UsePrivateRDNS: s.conf.UsePrivateRDNS,
|
||||
UseRDNS: s.conf.AddrProcConf.UseRDNS,
|
||||
UseWHOIS: s.conf.AddrProcConf.UseWHOIS,
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve - get IP addresses by host name from an upstream server.
|
||||
@@ -277,17 +302,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,20 +313,16 @@ 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, ttl time.Duration, err error) {
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
if !s.conf.ResolveClients {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
arpa, err := netutil.IPToReversedAddr(ip)
|
||||
arpa, err := netutil.IPToReversedAddr(ip.AsSlice())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reversing ip: %w", err)
|
||||
return "", 0, fmt.Errorf("reversing ip: %w", err)
|
||||
}
|
||||
|
||||
arpa = dns.Fqdn(arpa)
|
||||
@@ -328,16 +338,17 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
Qclass: dns.ClassINET,
|
||||
}},
|
||||
}
|
||||
ctx := &proxy.DNSContext{
|
||||
|
||||
dctx := &proxy.DNSContext{
|
||||
Proto: "udp",
|
||||
Req: req,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
var resolver *proxy.Proxy
|
||||
if s.privateNets.Contains(ip) {
|
||||
if s.privateNets.Contains(ip.AsSlice()) {
|
||||
if !s.conf.UsePrivateRDNS {
|
||||
return "", nil
|
||||
return "", 0, nil
|
||||
}
|
||||
|
||||
resolver = s.localResolvers
|
||||
@@ -346,36 +357,48 @@ func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
||||
resolver = s.internalProxy
|
||||
}
|
||||
|
||||
if err = resolver.Resolve(ctx); err != nil {
|
||||
return "", err
|
||||
if err = resolver.Resolve(dctx); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return hostFromPTR(dctx.Res)
|
||||
}
|
||||
|
||||
// hostFromPTR returns domain name from the PTR response or error.
|
||||
func hostFromPTR(resp *dns.Msg) (host string, ttl time.Duration, 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(
|
||||
return "", 0, fmt.Errorf(
|
||||
"received %s response: %w",
|
||||
dns.RcodeToString[resp.Rcode],
|
||||
ErrRDNSFailed,
|
||||
)
|
||||
}
|
||||
|
||||
var ttlSec uint32
|
||||
|
||||
for _, ans := range resp.Answer {
|
||||
ptr, ok := ans.(*dns.PTR)
|
||||
if ok {
|
||||
return strings.TrimSuffix(ptr.Ptr, "."), nil
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if ptr.Hdr.Ttl > ttlSec {
|
||||
host = ptr.Ptr
|
||||
ttlSec = ptr.Hdr.Ttl
|
||||
}
|
||||
}
|
||||
|
||||
return "", ErrRDNSNoData
|
||||
}
|
||||
if host != "" {
|
||||
// NOTE: Don't use [aghnet.NormalizeDomain] to retain original letter
|
||||
// case.
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
ttl = time.Duration(ttlSec) * time.Second
|
||||
|
||||
// ResolvesPrivatePTR implements the RDNSExchanger interface for *Server.
|
||||
func (s *Server) ResolvesPrivatePTR() (ok bool) {
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
return host, ttl, nil
|
||||
}
|
||||
|
||||
return s.conf.UsePrivateRDNS
|
||||
return "", 0, ErrRDNSNoData
|
||||
}
|
||||
|
||||
// Start starts the DNS server.
|
||||
@@ -450,43 +473,50 @@ func (s *Server) filterOurDNSAddrs(addrs []string) (filtered []string, err error
|
||||
return stringutil.FilterOut(addrs, ourAddrsSet.Has), nil
|
||||
}
|
||||
|
||||
// setupResolvers initializes the resolvers for local addresses. For internal
|
||||
// use only.
|
||||
func (s *Server) setupResolvers(localAddrs []string) (err error) {
|
||||
// setupLocalResolvers initializes the resolvers for local addresses. For
|
||||
// internal use only.
|
||||
func (s *Server) setupLocalResolvers() (err error) {
|
||||
bootstraps := s.conf.BootstrapDNS
|
||||
if len(localAddrs) == 0 {
|
||||
localAddrs = s.sysResolvers.Get()
|
||||
resolvers := s.conf.LocalPTRResolvers
|
||||
|
||||
if len(resolvers) == 0 {
|
||||
resolvers = s.sysResolvers.Get()
|
||||
bootstraps = nil
|
||||
} else {
|
||||
resolvers = stringutil.FilterOut(resolvers, IsCommentOrEmpty)
|
||||
}
|
||||
|
||||
localAddrs, err = s.filterOurDNSAddrs(localAddrs)
|
||||
resolvers, err = s.filterOurDNSAddrs(resolvers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: upstreams to resolve ptr for local addresses: %v", localAddrs)
|
||||
log.Debug("dnsforward: upstreams to resolve ptr for local addresses: %v", resolvers)
|
||||
|
||||
var upsConfig *proxy.UpstreamConfig
|
||||
upsConfig, err = proxy.ParseUpstreamsConfig(
|
||||
localAddrs,
|
||||
&upstream.Options{
|
||||
Bootstrap: bootstraps,
|
||||
Timeout: defaultLocalTimeout,
|
||||
// TODO(e.burkov): Should we verify server's certificates?
|
||||
uc, err := s.prepareUpstreamConfig(resolvers, 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{
|
||||
Config: proxy.Config{
|
||||
UpstreamConfig: upsConfig,
|
||||
UpstreamConfig: uc,
|
||||
},
|
||||
}
|
||||
|
||||
if s.conf.UsePrivateRDNS &&
|
||||
// Only set the upstream config if there are any upstreams. It's safe
|
||||
// to put nil into [proxy.Config.PrivateRDNSUpstreamConfig].
|
||||
len(uc.Upstreams)+len(uc.DomainReservedUpstreams)+len(uc.SpecifiedDomainUpstreams) > 0 {
|
||||
s.dnsProxy.PrivateRDNSUpstreamConfig = uc
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -510,7 +540,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
|
||||
@@ -535,25 +566,48 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
|
||||
return fmt.Errorf("preparing access: %w", err)
|
||||
}
|
||||
|
||||
s.registerHandlers()
|
||||
|
||||
// Set the proxy here because [setupLocalResolvers] sets its values.
|
||||
//
|
||||
// TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy.
|
||||
err = s.setupResolvers(s.conf.LocalPTRResolvers)
|
||||
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
||||
|
||||
err = s.setupLocalResolvers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up resolvers: %w", err)
|
||||
}
|
||||
|
||||
if s.conf.UsePrivateRDNS {
|
||||
proxyConfig.PrivateRDNSUpstreamConfig = s.localResolvers.UpstreamConfig
|
||||
}
|
||||
|
||||
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
||||
|
||||
s.recDetector.clear()
|
||||
|
||||
s.setupAddrProc()
|
||||
|
||||
s.registerHandlers()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupAddrProc initializes the address processor. For internal use only.
|
||||
func (s *Server) setupAddrProc() {
|
||||
// TODO(a.garipov): This is a crutch for tests; remove.
|
||||
if s.conf.AddrProcConf == nil {
|
||||
s.conf.AddrProcConf = &client.DefaultAddrProcConfig{}
|
||||
}
|
||||
if s.conf.AddrProcConf.AddressUpdater == nil {
|
||||
s.addrProc = client.EmptyAddrProc{}
|
||||
} else {
|
||||
c := s.conf.AddrProcConf
|
||||
c.DialContext = s.DialContext
|
||||
c.PrivateSubnets = s.privateNets
|
||||
c.UsePrivateRDNS = s.conf.UsePrivateRDNS
|
||||
s.addrProc = client.NewDefaultAddrProc(s.conf.AddrProcConf)
|
||||
|
||||
// Clear the initial addresses to not resolve them again.
|
||||
//
|
||||
// TODO(a.garipov): Consider ways of removing this once more client
|
||||
// logic is moved to package client.
|
||||
c.InitialAddresses = nil
|
||||
}
|
||||
}
|
||||
|
||||
// validateBlockingMode returns an error if the blocking mode data aren't valid.
|
||||
func validateBlockingMode(mode BlockingMode, blockingIPv4, blockingIPv6 net.IP) (err error) {
|
||||
switch mode {
|
||||
@@ -692,6 +746,11 @@ func (s *Server) Reconfigure(conf *ServerConfig) error {
|
||||
// TODO(a.garipov): This whole piece of API is weird and needs to be remade.
|
||||
if conf == nil {
|
||||
conf = &s.conf
|
||||
} else {
|
||||
closeErr := s.addrProc.Close()
|
||||
if closeErr != nil {
|
||||
log.Error("dnsforward: closing address processor: %s", closeErr)
|
||||
}
|
||||
}
|
||||
|
||||
err = s.Prepare(conf)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
@@ -39,11 +40,29 @@ func TestMain(m *testing.M) {
|
||||
testutil.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
//
|
||||
// TODO(a.garipov): Use more.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testQuestionTarget is the common question target for tests.
|
||||
//
|
||||
// TODO(a.garipov): Use more.
|
||||
const testQuestionTarget = "target.example"
|
||||
|
||||
const (
|
||||
tlsServerName = "testdns.adguard.com"
|
||||
testMessagesCount = 10
|
||||
)
|
||||
|
||||
// testClientAddr is the common net.Addr for tests.
|
||||
//
|
||||
// TODO(a.garipov): Use more.
|
||||
var testClientAddr net.Addr = &net.TCPAddr{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
Port: 12345,
|
||||
}
|
||||
|
||||
func startDeferStop(t *testing.T, s *Server) {
|
||||
t.Helper()
|
||||
|
||||
@@ -53,6 +72,13 @@ func startDeferStop(t *testing.T, s *Server) {
|
||||
testutil.CleanupAndRequireSuccess(t, s.Stop)
|
||||
}
|
||||
|
||||
// packageUpstreamVariableMu is used to serialize access to the package-level
|
||||
// variables of package upstream.
|
||||
//
|
||||
// TODO(s.chzhen): Move these parameters to upstream options and remove this
|
||||
// crutch.
|
||||
var packageUpstreamVariableMu = &sync.Mutex{}
|
||||
|
||||
func createTestServer(
|
||||
t *testing.T,
|
||||
filterConf *filtering.Config,
|
||||
@@ -61,6 +87,9 @@ func createTestServer(
|
||||
) (s *Server) {
|
||||
t.Helper()
|
||||
|
||||
packageUpstreamVariableMu.Lock()
|
||||
defer packageUpstreamVariableMu.Unlock()
|
||||
|
||||
rules := `||nxdomain.example.org
|
||||
||NULL.example.org^
|
||||
127.0.0.1 host.example.org
|
||||
@@ -307,11 +336,9 @@ func TestServer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_timeout(t *testing.T) {
|
||||
const timeout time.Duration = time.Second
|
||||
|
||||
t.Run("custom", func(t *testing.T) {
|
||||
srvConf := &ServerConfig{
|
||||
UpstreamTimeout: timeout,
|
||||
UpstreamTimeout: testTimeout,
|
||||
FilteringConfig: FilteringConfig{
|
||||
BlockingMode: BlockingModeDefault,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
@@ -324,7 +351,7 @@ func TestServer_timeout(t *testing.T) {
|
||||
err = s.Prepare(srvConf)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, timeout, s.conf.UpstreamTimeout)
|
||||
assert.Equal(t, testTimeout, s.conf.UpstreamTimeout)
|
||||
})
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
@@ -441,7 +468,14 @@ func TestServerRace(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSafeSearch(t *testing.T) {
|
||||
resolver := &aghtest.TestResolver{}
|
||||
resolver := &aghtest.Resolver{
|
||||
OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
|
||||
ip4, ip6 := aghtest.HostToIPs(host)
|
||||
|
||||
return []net.IP{ip4, ip6}, nil
|
||||
},
|
||||
}
|
||||
|
||||
safeSearchConf := filtering.SafeSearchConfig{
|
||||
Enabled: true,
|
||||
Google: true,
|
||||
@@ -480,7 +514,7 @@ func TestSafeSearch(t *testing.T) {
|
||||
client := &dns.Client{}
|
||||
|
||||
yandexIP := net.IP{213, 180, 193, 56}
|
||||
googleIP, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
||||
googleIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
|
||||
|
||||
testCases := []struct {
|
||||
host string
|
||||
@@ -545,7 +579,7 @@ func TestInvalidRequest(t *testing.T) {
|
||||
|
||||
// Send a DNS request without question.
|
||||
_, _, err := (&dns.Client{
|
||||
Timeout: 500 * time.Millisecond,
|
||||
Timeout: testTimeout,
|
||||
}).Exchange(&req, addr)
|
||||
|
||||
assert.NoErrorf(t, err, "got a response to an invalid query")
|
||||
@@ -928,7 +962,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
|
||||
Upstream: aghtest.NewBlockUpstream(hostname, true),
|
||||
})
|
||||
|
||||
ans4, _ := (&aghtest.TestResolver{}).HostToIPs(hostname)
|
||||
ans4, _ := aghtest.HostToIPs(hostname)
|
||||
|
||||
filterConf := &filtering.Config{
|
||||
SafeBrowsingEnabled: true,
|
||||
@@ -1266,31 +1300,63 @@ func TestNewServer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// doubleTTL is a helper function that returns a clone of DNS PTR with appended
|
||||
// copy of first answer record with doubled TTL.
|
||||
func doubleTTL(msg *dns.Msg) (resp *dns.Msg) {
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(msg.Answer) == 0 {
|
||||
return msg
|
||||
}
|
||||
|
||||
rec := msg.Answer[0]
|
||||
ptr, ok := rec.(*dns.PTR)
|
||||
if !ok {
|
||||
return msg
|
||||
}
|
||||
|
||||
clone := *ptr
|
||||
clone.Hdr.Ttl *= 2
|
||||
msg.Answer = append(msg.Answer, &clone)
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func TestServer_Exchange(t *testing.T) {
|
||||
const (
|
||||
onesHost = "one.one.one.one"
|
||||
twosHost = "two.two.two.two"
|
||||
localDomainHost = "local.domain"
|
||||
|
||||
defaultTTL = time.Second * 60
|
||||
)
|
||||
|
||||
var (
|
||||
onesIP = net.IP{1, 1, 1, 1}
|
||||
localIP = net.IP{192, 168, 1, 1}
|
||||
onesIP = netip.MustParseAddr("1.1.1.1")
|
||||
twosIP = netip.MustParseAddr("2.2.2.2")
|
||||
localIP = netip.MustParseAddr("192.168.1.1")
|
||||
)
|
||||
|
||||
revExtIPv4, err := netutil.IPToReversedAddr(onesIP)
|
||||
onesRevExtIPv4, err := netutil.IPToReversedAddr(onesIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
twosRevExtIPv4, err := netutil.IPToReversedAddr(twosIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
extUpstream := &aghtest.UpstreamMock{
|
||||
OnAddress: func() (addr string) { return "external.upstream.example" },
|
||||
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
return aghalg.Coalesce(
|
||||
aghtest.MatchedResponse(req, dns.TypePTR, revExtIPv4, onesHost),
|
||||
aghtest.MatchedResponse(req, dns.TypePTR, onesRevExtIPv4, onesHost),
|
||||
doubleTTL(aghtest.MatchedResponse(req, dns.TypePTR, twosRevExtIPv4, twosHost)),
|
||||
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
|
||||
), nil
|
||||
},
|
||||
}
|
||||
|
||||
revLocIPv4, err := netutil.IPToReversedAddr(localIP)
|
||||
revLocIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
locUpstream := &aghtest.UpstreamMock{
|
||||
@@ -1320,53 +1386,65 @@ func TestServer_Exchange(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
srv.conf.ResolveClients = true
|
||||
srv.conf.UsePrivateRDNS = true
|
||||
|
||||
srv.privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want string
|
||||
req netip.Addr
|
||||
wantErr error
|
||||
locUpstream upstream.Upstream
|
||||
req net.IP
|
||||
name string
|
||||
want string
|
||||
wantTTL time.Duration
|
||||
}{{
|
||||
name: "external_good",
|
||||
want: onesHost,
|
||||
wantErr: nil,
|
||||
locUpstream: nil,
|
||||
req: onesIP,
|
||||
wantTTL: defaultTTL,
|
||||
}, {
|
||||
name: "local_good",
|
||||
want: localDomainHost,
|
||||
wantErr: nil,
|
||||
locUpstream: locUpstream,
|
||||
req: localIP,
|
||||
wantTTL: defaultTTL,
|
||||
}, {
|
||||
name: "upstream_error",
|
||||
want: "",
|
||||
wantErr: aghtest.ErrUpstream,
|
||||
locUpstream: errUpstream,
|
||||
req: localIP,
|
||||
wantTTL: 0,
|
||||
}, {
|
||||
name: "empty_answer_error",
|
||||
want: "",
|
||||
wantErr: ErrRDNSNoData,
|
||||
locUpstream: locUpstream,
|
||||
req: net.IP{192, 168, 1, 2},
|
||||
req: netip.MustParseAddr("192.168.1.2"),
|
||||
wantTTL: 0,
|
||||
}, {
|
||||
name: "invalid_answer",
|
||||
want: "",
|
||||
wantErr: ErrRDNSNoData,
|
||||
locUpstream: nonPtrUpstream,
|
||||
req: localIP,
|
||||
wantTTL: 0,
|
||||
}, {
|
||||
name: "refused",
|
||||
want: "",
|
||||
wantErr: ErrRDNSFailed,
|
||||
locUpstream: refusingUpstream,
|
||||
req: localIP,
|
||||
wantTTL: 0,
|
||||
}, {
|
||||
name: "longest_ttl",
|
||||
want: twosHost,
|
||||
wantErr: nil,
|
||||
locUpstream: nil,
|
||||
req: twosIP,
|
||||
wantTTL: defaultTTL * 2,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -1380,17 +1458,18 @@ func TestServer_Exchange(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
host, eerr := srv.Exchange(tc.req)
|
||||
host, ttl, eerr := srv.Exchange(tc.req)
|
||||
|
||||
require.ErrorIs(t, eerr, tc.wantErr)
|
||||
assert.Equal(t, tc.want, host)
|
||||
assert.Equal(t, tc.wantTTL, ttl)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("resolving_disabled", func(t *testing.T) {
|
||||
srv.conf.UsePrivateRDNS = false
|
||||
|
||||
host, eerr := srv.Exchange(localIP)
|
||||
host, _, eerr := srv.Exchange(localIP)
|
||||
|
||||
require.NoError(t, eerr)
|
||||
assert.Empty(t, host)
|
||||
|
||||
@@ -50,10 +50,10 @@ func (s *Server) beforeRequestHandler(
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getClientRequestFilteringSettings looks up client filtering settings using
|
||||
// the client's IP address and ID, if any, from dctx.
|
||||
func (s *Server) getClientRequestFilteringSettings(dctx *dnsContext) *filtering.Settings {
|
||||
setts := s.dnsFilter.Settings()
|
||||
// clientRequestFilteringSettings looks up client filtering settings using the
|
||||
// client's IP address and ID, if any, from dctx.
|
||||
func (s *Server) clientRequestFilteringSettings(dctx *dnsContext) (setts *filtering.Settings) {
|
||||
setts = s.dnsFilter.Settings()
|
||||
setts.ProtectionEnabled = dctx.protectionEnabled
|
||||
if s.conf.FilterHandler != nil {
|
||||
ip, _ := netutil.IPAndPortFromAddr(dctx.proxyCtx.Addr)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -124,7 +124,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||
cacheMinTTL := s.conf.CacheMinTTL
|
||||
cacheMaxTTL := s.conf.CacheMaxTTL
|
||||
cacheOptimistic := s.conf.CacheOptimistic
|
||||
resolveClients := s.conf.ResolveClients
|
||||
resolveClients := s.conf.AddrProcConf.UseRDNS
|
||||
usePrivateRDNS := s.conf.UsePrivateRDNS
|
||||
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
|
||||
|
||||
@@ -314,8 +314,6 @@ func (s *Server) setConfig(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||
setIfNotNil(&s.conf.ProtectionEnabled, dc.ProtectionEnabled)
|
||||
setIfNotNil(&s.conf.EnableDNSSEC, dc.DNSSECEnabled)
|
||||
setIfNotNil(&s.conf.AAAADisabled, dc.DisableIPv6)
|
||||
setIfNotNil(&s.conf.ResolveClients, dc.ResolveClients)
|
||||
setIfNotNil(&s.conf.UsePrivateRDNS, dc.UsePrivateRDNS)
|
||||
|
||||
return s.setConfigRestartable(dc)
|
||||
}
|
||||
@@ -335,6 +333,9 @@ func setIfNotNil[T any](currentPtr, newPtr *T) (hasSet bool) {
|
||||
// setConfigRestartable sets the parameters which trigger a restart.
|
||||
// shouldRestart is true if the server should be restarted to apply changes.
|
||||
// s.serverLock is expected to be locked.
|
||||
//
|
||||
// TODO(a.garipov): Some of these could probably be updated without a restart.
|
||||
// Inspect and consider refactoring.
|
||||
func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||
for _, hasSet := range []bool{
|
||||
setIfNotNil(&s.conf.UpstreamDNS, dc.Upstreams),
|
||||
@@ -347,6 +348,8 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||
setIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL),
|
||||
setIfNotNil(&s.conf.CacheMaxTTL, dc.CacheMaxTTL),
|
||||
setIfNotNil(&s.conf.CacheOptimistic, dc.CacheOptimistic),
|
||||
setIfNotNil(&s.conf.AddrProcConf.UseRDNS, dc.ResolveClients),
|
||||
setIfNotNil(&s.conf.UsePrivateRDNS, dc.UsePrivateRDNS),
|
||||
} {
|
||||
shouldRestart = shouldRestart || hasSet
|
||||
if shouldRestart {
|
||||
@@ -633,61 +636,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 +711,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ type dnsContext struct {
|
||||
setts *filtering.Settings
|
||||
|
||||
result *filtering.Result
|
||||
|
||||
// origResp is the response received from upstream. It is set when the
|
||||
// response is modified by filters.
|
||||
origResp *dns.Msg
|
||||
@@ -48,13 +49,13 @@ type dnsContext struct {
|
||||
// clientID is the ClientID from DoH, DoQ, or DoT, if provided.
|
||||
clientID string
|
||||
|
||||
// startTime is the time at which the processing of the request has started.
|
||||
startTime time.Time
|
||||
|
||||
// origQuestion is the question received from the client. It is set
|
||||
// when the request is modified by rewrites.
|
||||
origQuestion dns.Question
|
||||
|
||||
// startTime is the time at which the processing of the request has started.
|
||||
startTime time.Time
|
||||
|
||||
// protectionEnabled shows if the filtering is enabled, and if the
|
||||
// server's DNS filter is ready.
|
||||
protectionEnabled bool
|
||||
@@ -160,6 +161,22 @@ func (s *Server) processRecursion(dctx *dnsContext) (rc resultCode) {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// mozillaFQDN is the domain used to signal the Firefox browser to not use its
|
||||
// own DoH server.
|
||||
//
|
||||
// See https://support.mozilla.org/en-US/kb/canary-domain-use-application-dnsnet.
|
||||
const mozillaFQDN = "use-application-dns.net."
|
||||
|
||||
// healthcheckFQDN is a reserved domain-name used for healthchecking.
|
||||
//
|
||||
// [Section 6.2 of RFC 6761] states that DNS Registries/Registrars must not
|
||||
// grant requests to register test names in the normal way to any person or
|
||||
// entity, making domain names under the .test TLD free to use in internal
|
||||
// purposes.
|
||||
//
|
||||
// [Section 6.2 of RFC 6761]: https://www.rfc-editor.org/rfc/rfc6761.html#section-6.2
|
||||
const healthcheckFQDN = "healthcheck.adguardhome.test."
|
||||
|
||||
// processInitial terminates the following processing for some requests if
|
||||
// needed and enriches dctx with some client-specific information.
|
||||
//
|
||||
@@ -169,6 +186,8 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
|
||||
defer log.Debug("dnsforward: finished processing initial")
|
||||
|
||||
pctx := dctx.proxyCtx
|
||||
s.processClientIP(pctx.Addr)
|
||||
|
||||
q := pctx.Req.Question[0]
|
||||
qt := q.Qtype
|
||||
if s.conf.AAAADisabled && qt == dns.TypeAAAA {
|
||||
@@ -177,28 +196,13 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
|
||||
return resultCodeFinish
|
||||
}
|
||||
|
||||
if s.conf.OnDNSRequest != nil {
|
||||
s.conf.OnDNSRequest(pctx)
|
||||
}
|
||||
|
||||
// Disable Mozilla DoH.
|
||||
//
|
||||
// See https://support.mozilla.org/en-US/kb/canary-domain-use-application-dnsnet.
|
||||
if (qt == dns.TypeA || qt == dns.TypeAAAA) && q.Name == "use-application-dns.net." {
|
||||
if (qt == dns.TypeA || qt == dns.TypeAAAA) && q.Name == mozillaFQDN {
|
||||
pctx.Res = s.genNXDomain(pctx.Req)
|
||||
|
||||
return resultCodeFinish
|
||||
}
|
||||
|
||||
// Handle a reserved domain healthcheck.adguardhome.test.
|
||||
//
|
||||
// [Section 6.2 of RFC 6761] states that DNS Registries/Registrars must not
|
||||
// grant requests to register test names in the normal way to any person or
|
||||
// entity, making domain names under test. TLD free to use in internal
|
||||
// purposes.
|
||||
//
|
||||
// [Section 6.2 of RFC 6761]: https://www.rfc-editor.org/rfc/rfc6761.html#section-6.2
|
||||
if q.Name == "healthcheck.adguardhome.test." {
|
||||
if q.Name == healthcheckFQDN {
|
||||
// Generate a NODATA negative response to make nslookup exit with 0.
|
||||
pctx.Res = s.makeResponse(pctx.Req)
|
||||
|
||||
@@ -213,11 +217,28 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
|
||||
|
||||
// Get the client-specific filtering settings.
|
||||
dctx.protectionEnabled, _ = s.UpdatedProtectionStatus()
|
||||
dctx.setts = s.getClientRequestFilteringSettings(dctx)
|
||||
dctx.setts = s.clientRequestFilteringSettings(dctx)
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
// processClientIP sends the client IP address to s.addrProc, if needed.
|
||||
func (s *Server) processClientIP(addr net.Addr) {
|
||||
clientIP := netutil.NetAddrToAddrPort(addr).Addr()
|
||||
if clientIP == (netip.Addr{}) {
|
||||
log.Info("dnsforward: warning: bad client addr %q", addr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Do not assign s.addrProc to a local variable to then use, since this lock
|
||||
// also serializes the closure of s.addrProc.
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
s.addrProc.Process(clientIP)
|
||||
}
|
||||
|
||||
func (s *Server) setTableHostToIP(t hostToIPTable) {
|
||||
s.tableHostToIPLock.Lock()
|
||||
defer s.tableHostToIPLock.Unlock()
|
||||
@@ -698,6 +719,18 @@ func (s *Server) processLocalPTR(dctx *dnsContext) (rc resultCode) {
|
||||
if s.conf.UsePrivateRDNS {
|
||||
s.recDetector.add(*pctx.Req)
|
||||
if err := s.localResolvers.Resolve(pctx); err != nil {
|
||||
// Generate the server failure if the private upstream configuration
|
||||
// is empty.
|
||||
//
|
||||
// TODO(e.burkov): Get rid of this crutch once the local resolvers
|
||||
// logic is moved to the dnsproxy completely.
|
||||
if errors.Is(err, upstream.ErrNoUpstreams) {
|
||||
pctx.Res = s.genServerFailure(pctx.Req)
|
||||
|
||||
// Do not even put into query log.
|
||||
return resultCodeFinish
|
||||
}
|
||||
|
||||
dctx.err = err
|
||||
|
||||
return resultCodeError
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -22,6 +23,96 @@ const (
|
||||
ddrTestFQDN = ddrTestDomainName + "."
|
||||
)
|
||||
|
||||
func TestServer_ProcessInitial(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
target string
|
||||
wantRCode rules.RCode
|
||||
qType rules.RRType
|
||||
aaaaDisabled bool
|
||||
wantRC resultCode
|
||||
}{{
|
||||
name: "success",
|
||||
target: testQuestionTarget,
|
||||
wantRCode: -1,
|
||||
qType: dns.TypeA,
|
||||
aaaaDisabled: false,
|
||||
wantRC: resultCodeSuccess,
|
||||
}, {
|
||||
name: "aaaa_disabled",
|
||||
target: testQuestionTarget,
|
||||
wantRCode: dns.RcodeSuccess,
|
||||
qType: dns.TypeAAAA,
|
||||
aaaaDisabled: true,
|
||||
wantRC: resultCodeFinish,
|
||||
}, {
|
||||
name: "aaaa_disabled_a",
|
||||
target: testQuestionTarget,
|
||||
wantRCode: -1,
|
||||
qType: dns.TypeA,
|
||||
aaaaDisabled: true,
|
||||
wantRC: resultCodeSuccess,
|
||||
}, {
|
||||
name: "mozilla_canary",
|
||||
target: mozillaFQDN,
|
||||
wantRCode: dns.RcodeNameError,
|
||||
qType: dns.TypeA,
|
||||
aaaaDisabled: false,
|
||||
wantRC: resultCodeFinish,
|
||||
}, {
|
||||
name: "adguardhome_healthcheck",
|
||||
target: healthcheckFQDN,
|
||||
wantRCode: dns.RcodeSuccess,
|
||||
qType: dns.TypeA,
|
||||
aaaaDisabled: false,
|
||||
wantRC: resultCodeFinish,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := ServerConfig{
|
||||
FilteringConfig: FilteringConfig{
|
||||
AAAADisabled: tc.aaaaDisabled,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}
|
||||
|
||||
s := createTestServer(t, &filtering.Config{}, c, nil)
|
||||
|
||||
var gotAddr netip.Addr
|
||||
s.addrProc = &aghtest.AddressProcessor{
|
||||
OnProcess: func(ip netip.Addr) { gotAddr = ip },
|
||||
OnClose: func() (err error) { panic("not implemented") },
|
||||
}
|
||||
|
||||
dctx := &dnsContext{
|
||||
proxyCtx: &proxy.DNSContext{
|
||||
Req: createTestMessageWithType(tc.target, tc.qType),
|
||||
Addr: testClientAddr,
|
||||
RequestID: 1234,
|
||||
},
|
||||
}
|
||||
|
||||
gotRC := s.processInitial(dctx)
|
||||
assert.Equal(t, tc.wantRC, gotRC)
|
||||
assert.Equal(t, netutil.NetAddrToAddrPort(testClientAddr).Addr(), gotAddr)
|
||||
|
||||
if tc.wantRCode > 0 {
|
||||
gotResp := dctx.proxyCtx.Res
|
||||
require.NotNil(t, gotResp)
|
||||
|
||||
assert.Equal(t, tc.wantRCode, gotResp.Rcode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ProcessDDRQuery(t *testing.T) {
|
||||
dohSVCB := &dns.SVCB{
|
||||
Priority: 1,
|
||||
@@ -64,7 +155,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
|
||||
}{{
|
||||
name: "pass_host",
|
||||
wantRes: resultCodeSuccess,
|
||||
host: "example.net.",
|
||||
host: testQuestionTarget,
|
||||
qtype: dns.TypeSVCB,
|
||||
ddrEnabled: true,
|
||||
portDoH: 8043,
|
||||
@@ -234,33 +325,33 @@ func TestServer_ProcessDetermineLocal(t *testing.T) {
|
||||
func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
|
||||
knownIP := netip.MustParseAddr("1.2.3.4")
|
||||
testCases := []struct {
|
||||
wantIP netip.Addr
|
||||
name string
|
||||
host string
|
||||
wantIP netip.Addr
|
||||
wantRes resultCode
|
||||
isLocalCli bool
|
||||
}{{
|
||||
wantIP: knownIP,
|
||||
name: "local_client_success",
|
||||
host: "example.lan",
|
||||
wantIP: knownIP,
|
||||
wantRes: resultCodeSuccess,
|
||||
isLocalCli: true,
|
||||
}, {
|
||||
wantIP: netip.Addr{},
|
||||
name: "local_client_unknown_host",
|
||||
host: "wronghost.lan",
|
||||
wantIP: netip.Addr{},
|
||||
wantRes: resultCodeSuccess,
|
||||
isLocalCli: true,
|
||||
}, {
|
||||
wantIP: netip.Addr{},
|
||||
name: "external_client_known_host",
|
||||
host: "example.lan",
|
||||
wantIP: netip.Addr{},
|
||||
wantRes: resultCodeFinish,
|
||||
isLocalCli: false,
|
||||
}, {
|
||||
wantIP: netip.Addr{},
|
||||
name: "external_client_unknown_host",
|
||||
host: "wronghost.lan",
|
||||
wantIP: netip.Addr{},
|
||||
wantRes: resultCodeFinish,
|
||||
isLocalCli: false,
|
||||
}}
|
||||
@@ -332,52 +423,52 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
|
||||
|
||||
knownIP := netip.MustParseAddr("1.2.3.4")
|
||||
testCases := []struct {
|
||||
wantIP netip.Addr
|
||||
name string
|
||||
host string
|
||||
suffix string
|
||||
wantIP netip.Addr
|
||||
wantRes resultCode
|
||||
qtyp uint16
|
||||
}{{
|
||||
wantIP: netip.Addr{},
|
||||
name: "success_external",
|
||||
host: examplecom,
|
||||
suffix: defaultLocalDomainSuffix,
|
||||
wantIP: netip.Addr{},
|
||||
wantRes: resultCodeSuccess,
|
||||
qtyp: dns.TypeA,
|
||||
}, {
|
||||
wantIP: netip.Addr{},
|
||||
name: "success_external_non_a",
|
||||
host: examplecom,
|
||||
suffix: defaultLocalDomainSuffix,
|
||||
wantIP: netip.Addr{},
|
||||
wantRes: resultCodeSuccess,
|
||||
qtyp: dns.TypeCNAME,
|
||||
}, {
|
||||
wantIP: knownIP,
|
||||
name: "success_internal",
|
||||
host: examplelan,
|
||||
suffix: defaultLocalDomainSuffix,
|
||||
wantIP: knownIP,
|
||||
wantRes: resultCodeSuccess,
|
||||
qtyp: dns.TypeA,
|
||||
}, {
|
||||
wantIP: netip.Addr{},
|
||||
name: "success_internal_unknown",
|
||||
host: "example-new.lan",
|
||||
suffix: defaultLocalDomainSuffix,
|
||||
wantIP: netip.Addr{},
|
||||
wantRes: resultCodeSuccess,
|
||||
qtyp: dns.TypeA,
|
||||
}, {
|
||||
wantIP: netip.Addr{},
|
||||
name: "success_internal_aaaa",
|
||||
host: examplelan,
|
||||
suffix: defaultLocalDomainSuffix,
|
||||
wantIP: netip.Addr{},
|
||||
wantRes: resultCodeSuccess,
|
||||
qtyp: dns.TypeAAAA,
|
||||
}, {
|
||||
wantIP: knownIP,
|
||||
name: "success_custom_suffix",
|
||||
host: "example.custom",
|
||||
suffix: "custom",
|
||||
wantIP: knownIP,
|
||||
wantRes: resultCodeSuccess,
|
||||
qtyp: dns.TypeA,
|
||||
}}
|
||||
@@ -560,10 +651,8 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) {
|
||||
var dnsCtx *dnsContext
|
||||
setup := func(use bool) {
|
||||
proxyCtx = &proxy.DNSContext{
|
||||
Addr: &net.TCPAddr{
|
||||
IP: net.IP{127, 0, 0, 1},
|
||||
},
|
||||
Req: createTestMessageWithType(reqAddr, dns.TypePTR),
|
||||
Addr: testClientAddr,
|
||||
Req: createTestMessageWithType(reqAddr, dns.TypePTR),
|
||||
}
|
||||
dnsCtx = &dnsContext{
|
||||
proxyCtx: proxyCtx,
|
||||
@@ -2,9 +2,9 @@ package dnsforward
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||
@@ -24,7 +24,7 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
|
||||
pctx := dctx.proxyCtx
|
||||
|
||||
q := pctx.Req.Question[0]
|
||||
host := strings.ToLower(strings.TrimSuffix(q.Name, "."))
|
||||
host := aghnet.NormalizeDomain(q.Name)
|
||||
|
||||
ip, _ := netutil.IPAndPortFromAddr(pctx.Addr)
|
||||
ip = slices.Clone(ip)
|
||||
@@ -139,11 +139,10 @@ func (s *Server) updateStats(
|
||||
clientIP string,
|
||||
) {
|
||||
pctx := ctx.proxyCtx
|
||||
e := stats.Entry{}
|
||||
e.Domain = strings.ToLower(pctx.Req.Question[0].Name)
|
||||
if e.Domain != "." {
|
||||
// Remove last ".", but save the domain as is for "." queries.
|
||||
e.Domain = e.Domain[:len(e.Domain)-1]
|
||||
e := stats.Entry{
|
||||
Domain: aghnet.NormalizeDomain(pctx.Req.Question[0].Name),
|
||||
Result: stats.RNotFiltered,
|
||||
Time: uint32(elapsed / 1000),
|
||||
}
|
||||
|
||||
if clientID := ctx.clientID; clientID != "" {
|
||||
@@ -152,9 +151,6 @@ func (s *Server) updateStats(
|
||||
e.Client = clientIP
|
||||
}
|
||||
|
||||
e.Time = uint32(elapsed / 1000)
|
||||
e.Result = stats.RNotFiltered
|
||||
|
||||
switch res.Reason {
|
||||
case filtering.FilteredSafeBrowsing:
|
||||
e.Result = stats.RSafeBrowsing
|
||||
@@ -162,7 +158,8 @@ func (s *Server) updateStats(
|
||||
e.Result = stats.RParental
|
||||
case filtering.FilteredSafeSearch:
|
||||
e.Result = stats.RSafeSearch
|
||||
case filtering.FilteredBlockList,
|
||||
case
|
||||
filtering.FilteredBlockList,
|
||||
filtering.FilteredInvalid,
|
||||
filtering.FilteredBlockedService:
|
||||
e.Result = stats.RFiltered
|
||||
|
||||
313
internal/dnsforward/upstreams.go
Normal file
313
internal/dnsforward/upstreams.go
Normal file
@@ -0,0 +1,313 @@
|
||||
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) {
|
||||
// Use a customized set of RootCAs, because Go's 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].
|
||||
//
|
||||
// TODO(a.garipov): Investigate if that's true.
|
||||
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 essentially 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,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
@@ -29,9 +28,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
|
||||
@@ -85,53 +84,53 @@ func (d *DNSFilter) filterSetProperties(
|
||||
filters = d.WhitelistFilters
|
||||
}
|
||||
|
||||
i := slices.IndexFunc(filters, func(filt FilterYAML) bool { return filt.URL == listURL })
|
||||
i := slices.IndexFunc(filters, func(flt FilterYAML) bool { return flt.URL == listURL })
|
||||
if i == -1 {
|
||||
return false, errFilterNotExist
|
||||
}
|
||||
|
||||
filt := &filters[i]
|
||||
flt := &filters[i]
|
||||
log.Debug(
|
||||
"filtering: set name to %q, url to %s, enabled to %t for filter %s",
|
||||
newList.Name,
|
||||
newList.URL,
|
||||
newList.Enabled,
|
||||
filt.URL,
|
||||
flt.URL,
|
||||
)
|
||||
|
||||
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time, oldRulesCount int) {
|
||||
if err != nil {
|
||||
filt.URL = oldURL
|
||||
filt.Name = oldName
|
||||
filt.Enabled = oldEnabled
|
||||
filt.LastUpdated = oldUpdated
|
||||
filt.RulesCount = oldRulesCount
|
||||
flt.URL = oldURL
|
||||
flt.Name = oldName
|
||||
flt.Enabled = oldEnabled
|
||||
flt.LastUpdated = oldUpdated
|
||||
flt.RulesCount = oldRulesCount
|
||||
}
|
||||
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated, filt.RulesCount)
|
||||
}(flt.URL, flt.Name, flt.Enabled, flt.LastUpdated, flt.RulesCount)
|
||||
|
||||
filt.Name = newList.Name
|
||||
flt.Name = newList.Name
|
||||
|
||||
if filt.URL != newList.URL {
|
||||
if flt.URL != newList.URL {
|
||||
if d.filterExistsLocked(newList.URL) {
|
||||
return false, errFilterExists
|
||||
}
|
||||
|
||||
shouldRestart = true
|
||||
|
||||
filt.URL = newList.URL
|
||||
filt.LastUpdated = time.Time{}
|
||||
filt.unload()
|
||||
flt.URL = newList.URL
|
||||
flt.LastUpdated = time.Time{}
|
||||
flt.unload()
|
||||
}
|
||||
|
||||
if filt.Enabled != newList.Enabled {
|
||||
filt.Enabled = newList.Enabled
|
||||
if flt.Enabled != newList.Enabled {
|
||||
flt.Enabled = newList.Enabled
|
||||
shouldRestart = true
|
||||
}
|
||||
|
||||
if filt.Enabled {
|
||||
if flt.Enabled {
|
||||
if shouldRestart {
|
||||
// Download the filter contents.
|
||||
shouldRestart, err = d.update(filt)
|
||||
shouldRestart, err = d.update(flt)
|
||||
}
|
||||
} else {
|
||||
// TODO(e.burkov): The validation of the contents of the new URL is
|
||||
@@ -139,7 +138,7 @@ func (d *DNSFilter) filterSetProperties(
|
||||
// possible to set a bad rules source, but the validation should still
|
||||
// kick in when the filter is enabled. Consider changing this behavior
|
||||
// to be stricter.
|
||||
filt.unload()
|
||||
flt.unload()
|
||||
}
|
||||
|
||||
return shouldRestart, err
|
||||
@@ -213,7 +212,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,24 +251,24 @@ func assignUniqueFilterID() int64 {
|
||||
// Sets up a timer that will be checking for filters updates periodically
|
||||
func (d *DNSFilter) periodicallyRefreshFilters() {
|
||||
const maxInterval = 1 * 60 * 60
|
||||
intval := 5 // use a dynamically increasing time interval
|
||||
ivl := 5 // use a dynamically increasing time interval
|
||||
for {
|
||||
isNetErr, ok := false, false
|
||||
if d.FiltersUpdateIntervalHours != 0 {
|
||||
_, isNetErr, ok = d.tryRefreshFilters(true, true, false)
|
||||
if ok && !isNetErr {
|
||||
intval = maxInterval
|
||||
ivl = maxInterval
|
||||
}
|
||||
}
|
||||
|
||||
if isNetErr {
|
||||
intval *= 2
|
||||
if intval > maxInterval {
|
||||
intval = maxInterval
|
||||
ivl *= 2
|
||||
if ivl > maxInterval {
|
||||
ivl = maxInterval
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(intval) * time.Second)
|
||||
time.Sleep(time.Duration(ivl) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,19 +330,20 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
|
||||
return 0, nil, nil, false
|
||||
}
|
||||
|
||||
nfail := 0
|
||||
failNum := 0
|
||||
for i := range updateFilters {
|
||||
uf := &updateFilters[i]
|
||||
updated, err := d.update(uf)
|
||||
updateFlags = append(updateFlags, updated)
|
||||
if err != nil {
|
||||
nfail++
|
||||
log.Printf("Failed to update filter %s: %s\n", uf.URL, err)
|
||||
failNum++
|
||||
log.Error("filtering: updating filter from url %q: %s\n", uf.URL, err)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if nfail == len(updateFilters) {
|
||||
if failNum == len(updateFilters) {
|
||||
return 0, nil, nil, true
|
||||
}
|
||||
|
||||
@@ -367,7 +367,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 +403,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 +444,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,128 +458,123 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
return b, err
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (d *DNSFilter) finalizeUpdate(
|
||||
file *os.File,
|
||||
flt *FilterYAML,
|
||||
updated bool,
|
||||
name string,
|
||||
rnum int,
|
||||
cs uint32,
|
||||
) (err error) {
|
||||
tmpFileName := file.Name()
|
||||
|
||||
// Close the file before renaming it because it's required on Windows.
|
||||
//
|
||||
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing temporary file: %w", err)
|
||||
}
|
||||
|
||||
if !updated {
|
||||
log.Tracef("filter #%d from %s has no changes, skip", 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)
|
||||
|
||||
// Don't use renamio 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
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 tmpFile *os.File
|
||||
tmpFile, err = os.CreateTemp(filepath.Join(d.DataDir, filterDir), "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
finErr := d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs)
|
||||
if ok && finErr == nil {
|
||||
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithDeferred(err, finErr)
|
||||
}()
|
||||
var res *rulelist.ParseResult
|
||||
|
||||
// Change the default 0o600 permission to something more acceptable by end
|
||||
// users.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
|
||||
if err = tmpFile.Chmod(0o644); err != nil {
|
||||
return false, fmt.Errorf("changing file mode: %w", err)
|
||||
tmpFile, err := aghrenameio.NewPendingFile(flt.Path(d.DataDir), 0o644)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() { err = d.finalizeUpdate(tmpFile, flt, res, err, ok) }()
|
||||
|
||||
r, err := d.reader(flt.URL)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return false, err
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, r.Close()) }()
|
||||
|
||||
bufPtr := d.bufPool.Get().(*[]byte)
|
||||
defer d.bufPool.Put(bufPtr)
|
||||
|
||||
p := rulelist.NewParser()
|
||||
res, err = p.Parse(tmpFile, r, *bufPtr)
|
||||
|
||||
return res.Checksum != flt.checksum && err == nil, err
|
||||
}
|
||||
|
||||
// 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 succeeded.
|
||||
func (d *DNSFilter) finalizeUpdate(
|
||||
file aghrenameio.PendingFile,
|
||||
flt *FilterYAML,
|
||||
res *rulelist.ParseResult,
|
||||
returned error,
|
||||
updated bool,
|
||||
) (err error) {
|
||||
id := flt.ID
|
||||
if !updated {
|
||||
if returned == nil {
|
||||
log.Debug("filtering: filter %d from url %q has no changes, skipping", id, flt.URL)
|
||||
}
|
||||
|
||||
return errors.WithDeferred(returned, file.Cleanup())
|
||||
}
|
||||
|
||||
var r io.Reader
|
||||
if !filepath.IsAbs(flt.URL) {
|
||||
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: saving contents of filter %d into %q", id, flt.Path(d.DataDir))
|
||||
|
||||
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)
|
||||
|
||||
return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
r = resp.Body
|
||||
} else {
|
||||
var f *os.File
|
||||
f, err = os.Open(flt.URL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||
|
||||
r = f
|
||||
err = file.CloseReplace()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finalizing update: %w", err)
|
||||
}
|
||||
|
||||
rnum, n, cs, name, err = d.parseFilter(r, tmpFile)
|
||||
rulesCount := res.RulesCount
|
||||
log.Info("filtering: updated filter %d: %d bytes, %d rules", id, res.BytesWritten, rulesCount)
|
||||
|
||||
return cs != flt.checksum && err == nil, err
|
||||
flt.Name = aghalg.Coalesce(flt.Name, res.Title)
|
||||
flt.checksum = res.Checksum
|
||||
flt.RulesCount = rulesCount
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reader returns an io.ReadCloser reading filtering-rule list data form either
|
||||
// a file on the filesystem or the filter's HTTP URL.
|
||||
func (d *DNSFilter) reader(fltURL string) (r io.ReadCloser, err error) {
|
||||
if !filepath.IsAbs(fltURL) {
|
||||
r, err = d.readerFromURL(fltURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading from url: %w", err)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
r, err = os.Open(fltURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening file: %w", err)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// readerFromURL returns an io.ReadCloser reading filtering-rule list data form
|
||||
// the filter's URL.
|
||||
func (d *DNSFilter) readerFromURL(fltURL string) (r io.ReadCloser, err error) {
|
||||
resp, err := d.HTTPClient.Get(fltURL)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// 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 +590,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 +643,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.DefaultRuleBufSize)
|
||||
|
||||
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"
|
||||
191
internal/filtering/rulelist/parser.go
Normal file
191
internal/filtering/rulelist/parser.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package rulelist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
// Don't use [DefaultRuleBufSize] as the maximum size, since some
|
||||
// filtering-rule lists compressed by e.g. HostlistsCompiler can have very
|
||||
// large lines. The buffer optimization still works for the more common
|
||||
// case of reasonably-sized lines.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/6003.
|
||||
s.Buffer(buf, bufio.MaxScanTokenSize)
|
||||
|
||||
// Use a one-based index for lines and columns, since these errors end up in
|
||||
// the frontend, and users are more familiar with one-based line and column
|
||||
// indexes.
|
||||
lineNum := 1
|
||||
for s.Scan() {
|
||||
var n int
|
||||
n, err = p.processLine(dst, s.Bytes(), lineNum)
|
||||
p.written += n
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return p.result(), err
|
||||
}
|
||||
|
||||
lineNum++
|
||||
}
|
||||
|
||||
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, lineNum 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 %d: character %d: likely binary character %q",
|
||||
lineNum,
|
||||
badIdx+bytes.Index(line, trimmed)+1,
|
||||
trimmed[badIdx],
|
||||
)
|
||||
}
|
||||
|
||||
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. badIdx is the index of the
|
||||
// first character that may indicate that this is a binary file, or -1 if none.
|
||||
//
|
||||
// A line is considered a rule if it's not empty, not a comment, and contains
|
||||
// only printable characters.
|
||||
func parseLine(line []byte) (badIdx int, isRule bool) {
|
||||
if len(line) == 0 || line[0] == '#' || line[0] == '!' {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
badIdx = slices.IndexFunc(line, likelyBinary)
|
||||
|
||||
return badIdx, badIdx == -1
|
||||
}
|
||||
|
||||
// likelyBinary returns true if b is likely to be a byte from a binary file.
|
||||
func likelyBinary(b byte) (ok bool) {
|
||||
return (b < ' ' || b == 0x7f) && b != '\n' && b != '\r' && b != '\t'
|
||||
}
|
||||
|
||||
// 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) (badIdx int, isRule bool) {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
if line[0] != '!' {
|
||||
badIdx = slices.IndexFunc(line, likelyBinary)
|
||||
|
||||
return badIdx, badIdx == -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
|
||||
}
|
||||
276
internal/filtering/rulelist/parser_test.go
Normal file
276
internal/filtering/rulelist/parser_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package rulelist_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/testutil/fakeio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParser_Parse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
longRule := strings.Repeat("a", rulelist.DefaultRuleBufSize+1) + "\n"
|
||||
tooLongRule := strings.Repeat("a", bufio.MaxScanTokenSize+1) + "\n"
|
||||
|
||||
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: "cosmetic_with_zwnj",
|
||||
in: testRuleTextCosmetic,
|
||||
wantDst: testRuleTextCosmetic,
|
||||
wantErrMsg: "",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 1,
|
||||
wantWritten: len(testRuleTextCosmetic),
|
||||
}, {
|
||||
name: "bad_char",
|
||||
in: "! Title: Test Title \n" +
|
||||
testRuleTextBlocked +
|
||||
">>>\x7F<<<",
|
||||
wantDst: testRuleTextBlocked,
|
||||
wantErrMsg: "line 3: " +
|
||||
"character 4: " +
|
||||
"likely binary character '\\x7f'",
|
||||
wantTitle: "Test Title",
|
||||
wantRulesNum: 1,
|
||||
wantWritten: len(testRuleTextBlocked),
|
||||
}, {
|
||||
name: "too_long",
|
||||
in: tooLongRule,
|
||||
wantDst: "",
|
||||
wantErrMsg: "scanning filter contents: bufio.Scanner: token too long",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 0,
|
||||
wantWritten: 0,
|
||||
}, {
|
||||
name: "longer_than_default",
|
||||
in: longRule,
|
||||
wantDst: longRule,
|
||||
wantErrMsg: "",
|
||||
wantTitle: "",
|
||||
wantRulesNum: 1,
|
||||
wantWritten: len(longRule),
|
||||
}, {
|
||||
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.DefaultRuleBufSize)
|
||||
|
||||
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 := &fakeio.Writer{
|
||||
OnWrite: func(b []byte) (n int, err error) {
|
||||
return 1, errors.Error("test error")
|
||||
},
|
||||
}
|
||||
buf := make([]byte, rulelist.DefaultRuleBufSize)
|
||||
|
||||
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.DefaultRuleBufSize)
|
||||
|
||||
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.DefaultRuleBufSize)
|
||||
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)
|
||||
|
||||
// Most recent result, on a ThinkPad X13 with a Ryzen Pro 7 CPU:
|
||||
//
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist
|
||||
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
|
||||
// BenchmarkParser_Parse-16 100000000 128.0 ns/op 48 B/op 1 allocs/op
|
||||
}
|
||||
|
||||
func FuzzParser_Parse(f *testing.F) {
|
||||
const n = 64
|
||||
|
||||
testCases := []string{
|
||||
"",
|
||||
"# Comment",
|
||||
"! Comment",
|
||||
"! Title ",
|
||||
"! Title XXX",
|
||||
testRuleTextBadTab,
|
||||
testRuleTextBlocked,
|
||||
testRuleTextCosmetic,
|
||||
testRuleTextEtcHostsTab,
|
||||
testRuleTextHTML,
|
||||
"1.2.3.4",
|
||||
"1.2.3.4 etc-hosts.example",
|
||||
">>>\x00<<<",
|
||||
">>>\x7F<<<",
|
||||
strings.Repeat("a", rulelist.DefaultRuleBufSize+1),
|
||||
strings.Repeat("a", bufio.MaxScanTokenSize+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)
|
||||
})
|
||||
}
|
||||
9
internal/filtering/rulelist/rulelist.go
Normal file
9
internal/filtering/rulelist/rulelist.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Package rulelist contains the implementation of the standard rule-list
|
||||
// filter that wraps an urlfilter filtering-engine.
|
||||
//
|
||||
// TODO(a.garipov): Expand.
|
||||
package rulelist
|
||||
|
||||
// DefaultRuleBufSize is the default length of a buffer used to read a line with
|
||||
// a filtering rule, in bytes.
|
||||
const DefaultRuleBufSize = 1024
|
||||
19
internal/filtering/rulelist/rulelist_test.go
Normal file
19
internal/filtering/rulelist/rulelist_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package rulelist_test
|
||||
|
||||
import "time"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// Common texts for tests.
|
||||
const (
|
||||
testRuleTextBadTab = "||bad-tab-and-comment.example^\t# A comment.\n"
|
||||
testRuleTextBlocked = "||blocked.example^\n"
|
||||
testRuleTextEtcHostsTab = "0.0.0.0 tab..example^\t# A comment.\n"
|
||||
testRuleTextHTML = "<!DOCTYPE html>\n"
|
||||
|
||||
// testRuleTextCosmetic is a cosmetic rule with a zero-width non-joiner.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/6003.
|
||||
testRuleTextCosmetic = "||cosmetic.example## :has-text(/\u200c/i)\n"
|
||||
)
|
||||
@@ -89,37 +89,34 @@ func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||
assert.False(t, res.IsFiltered)
|
||||
assert.Empty(t, res.Rules)
|
||||
|
||||
resolver := &aghtest.TestResolver{}
|
||||
resolver := &aghtest.Resolver{
|
||||
OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
|
||||
ip4, ip6 := aghtest.HostToIPs(host)
|
||||
|
||||
return []net.IP{ip4, ip6}, nil
|
||||
},
|
||||
}
|
||||
|
||||
ss = newForTest(t, defaultSafeSearchConf)
|
||||
ss.resolver = resolver
|
||||
|
||||
// Lookup for safesearch domain.
|
||||
rewrite := ss.searchHost(domain, testQType)
|
||||
|
||||
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundIP net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
foundIP = ip
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
wantIP, _ := aghtest.HostToIPs(rewrite.NewCNAME)
|
||||
|
||||
res, err = ss.CheckHost(domain, testQType)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.True(t, res.Rules[0].IP.Equal(foundIP))
|
||||
assert.True(t, res.Rules[0].IP.Equal(wantIP))
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := ss.getCachedResult(domain, testQType)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
|
||||
assert.True(t, cachedValue.Rules[0].IP.Equal(wantIP))
|
||||
}
|
||||
|
||||
const googleHost = "www.google.com"
|
||||
|
||||
@@ -92,8 +92,15 @@ func TestDefault_CheckHost_yandexAAAA(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDefault_CheckHost_google(t *testing.T) {
|
||||
resolver := &aghtest.TestResolver{}
|
||||
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
||||
resolver := &aghtest.Resolver{
|
||||
OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
|
||||
ip4, ip6 := aghtest.HostToIPs(host)
|
||||
|
||||
return []net.IP{ip4, ip6}, nil
|
||||
},
|
||||
}
|
||||
|
||||
wantIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
|
||||
|
||||
conf := testConf
|
||||
conf.CustomResolver = resolver
|
||||
@@ -119,7 +126,7 @@ func TestDefault_CheckHost_google(t *testing.T) {
|
||||
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, ip, res.Rules[0].IP)
|
||||
assert.Equal(t, wantIP, res.Rules[0].IP)
|
||||
assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -1470,12 +1546,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 +1561,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,13 +1582,13 @@ var blockedServices = []blockedService{{
|
||||
"||social.linux.pizza^",
|
||||
"||social.politicaconciencia.org^",
|
||||
"||social.vivaldi.net^",
|
||||
"||sself.co^",
|
||||
"||stranger.social^",
|
||||
"||sueden.social^",
|
||||
"||tech.lgbt^",
|
||||
"||techhub.social^",
|
||||
"||theblower.au^",
|
||||
"||tkz.one^",
|
||||
"||todon.eu^",
|
||||
"||toot.aquilenet.fr^",
|
||||
"||toot.community^",
|
||||
"||toot.funami.tech^",
|
||||
@@ -1566,6 +1642,45 @@ 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^",
|
||||
"||nintendo.pt^",
|
||||
"||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 +2079,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",
|
||||
@@ -2036,6 +2161,20 @@ var blockedServices = []blockedService{{
|
||||
Rules: []string{
|
||||
"||voot.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "wargaming",
|
||||
Name: "Wargaming",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M12 1.998c-5.52 0-10 4.481-10 9.988 0 5.52 4.48 9.996 10 9.996s10-4.476 10-9.996c0-5.507-4.48-9.988-10-9.988zm0 2c4.413 0 8 3.588 8 7.988 0 3.246-1.944 6.04-4.727 7.293.54-1.861.831-3.988.807-6.226l1.414.414a23.648 23.648 0 0 0-2-4.041c-.627 1.347-1.48 2.56-2.52 3.68l1.68-.133c-1.507 2.92-3.134 3.906-5.547 4.013-.386-4.213.12-7.014 2.827-9.04l.386 1.493c.653-.974 1.36-2.12 2.373-2.947-1.506-.6-2.999-.627-4.492-.334.386.16.76.588 1.014.828-3.485 1.662-5.643 4.202-6.744 7.68A7.95 7.95 0 0 1 4 11.986c0-4.4 3.587-7.988 8-7.988z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||wargaming.com^",
|
||||
"||wargaming.net^",
|
||||
"||wgcdn.co^",
|
||||
"||wgcrowd.io^",
|
||||
"||worldoftanks.com^",
|
||||
"||worldofwarplanes.com^",
|
||||
"||worldofwarships.eu^",
|
||||
"||wotblitz.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "wechat",
|
||||
Name: "WeChat",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user