Compare commits
274 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ab44f059c | ||
|
|
ef7d71dd67 | ||
|
|
bd16eee509 | ||
|
|
729c56de20 | ||
|
|
e6b31bab77 | ||
|
|
d8f855ebfc | ||
|
|
a829adad10 | ||
|
|
2fc1e258ed | ||
|
|
bbf1ceb870 | ||
|
|
8de3734e2c | ||
|
|
474cba52f0 | ||
|
|
805de59805 | ||
|
|
effc822b85 | ||
|
|
aef00413d5 | ||
|
|
49bd72c27f | ||
|
|
2e52a2c8a0 | ||
|
|
ceb178fcd5 | ||
|
|
0d202cb544 | ||
|
|
6ce6c2c04d | ||
|
|
3f95db98d3 | ||
|
|
54f3a5f990 | ||
|
|
44cebc06ec | ||
|
|
6affa96490 | ||
|
|
2aaf8ab3c1 | ||
|
|
ee1eb80786 | ||
|
|
e8fd4b1872 | ||
|
|
8cb5781770 | ||
|
|
c7d8b9ede1 | ||
|
|
5c6bb33e3a | ||
|
|
158d4f0249 | ||
|
|
f73717ec08 | ||
|
|
1807198a9b | ||
|
|
1ccf8fe116 | ||
|
|
d22f0eefe2 | ||
|
|
344c66f7ab | ||
|
|
83be002b41 | ||
|
|
9945cd3991 | ||
|
|
667263a3a8 | ||
|
|
6318fc424b | ||
|
|
e32a37a747 | ||
|
|
7805a71332 | ||
|
|
6fb2aee210 | ||
|
|
ce9bb588ed | ||
|
|
55fb914537 | ||
|
|
6f7bfd6c9c | ||
|
|
fbc0d981ba | ||
|
|
48d1c673a9 | ||
|
|
889a0eb8b3 | ||
|
|
b01c10b73e | ||
|
|
f6ad64bf69 | ||
|
|
a5e8443735 | ||
|
|
2860929a47 | ||
|
|
ecdac56616 | ||
|
|
25918e56fa | ||
|
|
df91f016f2 | ||
|
|
f7d259f653 | ||
|
|
82ab4328d4 | ||
|
|
b21e19a223 | ||
|
|
c6aed4eb57 | ||
|
|
760d466b38 | ||
|
|
258eecc55b | ||
|
|
7b93f5d7cf | ||
|
|
3be7676970 | ||
|
|
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 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -1,8 +1,8 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.24.2'
|
||||
'NODE_VERSION': '20'
|
||||
'GO_VERSION': '1.24.1'
|
||||
'NODE_VERSION': '18'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.24.2'
|
||||
'GO_VERSION': '1.24.1'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
84
CHANGELOG.md
84
CHANGELOG.md
@@ -9,87 +9,27 @@ The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/
|
||||
<!--
|
||||
## [v0.108.0] – TBA
|
||||
|
||||
## [v0.107.62] - 2025-04-30 (APPROX.)
|
||||
## [v0.107.60] - 2025-04-01 (APPROX.)
|
||||
|
||||
See also the [v0.107.62 GitHub milestone][ms-v0.107.62].
|
||||
See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
|
||||
|
||||
[ms-v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/milestone/97?closed=1
|
||||
[ms-v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/milestone/95?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### Fixed
|
||||
|
||||
- Command line option `--update` when the `dns.serve_plain_dns` configuration property was disabled ([7801]).
|
||||
|
||||
- DNS cache not working for custom upstream configurations.
|
||||
|
||||
- Validation process for the DNS-over-TLS, DNS-over-QUIC, and HTTPS ports on the *Encryption Settings* page.
|
||||
|
||||
[#7801]: https://github.com/AdguardTeam/AdGuardHome/issues/7801
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
|
||||
## [v0.107.61] - 2025-04-22
|
||||
|
||||
See also the [v0.107.61 GitHub milestone][ms-v0.107.61].
|
||||
|
||||
### Security
|
||||
|
||||
- Any simultaneous requests that are considered duplicates will now only result in a single request to upstreams, reducing the chance of a cache poisoning attack succeeding. This is controlled by the new configuration object `pending_requests`, which has a single `enabled` property, set to `true` by default.
|
||||
|
||||
**NOTE:** We thank [Xiang Li][mr-xiang-li] for reporting this security issue. It's strongly recommended to leave it enabled, otherwise AdGuard Home will be vulnerable to untrusted clients.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Searching for persistent clients using an exact match for CIDR in the `POST /clients/search HTTP API`.
|
||||
|
||||
[mr-xiang-li]: https://lixiang521.com/
|
||||
[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1
|
||||
|
||||
## [v0.107.60] - 2025-04-14
|
||||
|
||||
See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
|
||||
|
||||
### Security
|
||||
|
||||
- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.2][go-1.24.2].
|
||||
|
||||
### Changed
|
||||
|
||||
- Alpine Linux version in `Dockerfile` has been updated to 3.21 ([#7588]).
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Node 20 support, Node 22 will be required in future releases.
|
||||
|
||||
**NOTE:** `npm` may be replaced with a different tool, such as `pnpm` or `yarn`, in a future release.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Filtering for DHCP clients ([#7734]).
|
||||
|
||||
- Incorrect label on login page ([#7729]).
|
||||
|
||||
- Validation process for the HTTPS port on the *Encryption Settings* page.
|
||||
|
||||
### Removed
|
||||
|
||||
- Node 18 support.
|
||||
|
||||
[#7588]: https://github.com/AdguardTeam/AdGuardHome/issues/7588
|
||||
[#7729]: https://github.com/AdguardTeam/AdGuardHome/issues/7729
|
||||
[#7734]: https://github.com/AdguardTeam/AdGuardHome/issues/7734
|
||||
|
||||
[go-1.24.2]: https://groups.google.com/g/golang-announce/c/Y2uBTVKjBQk
|
||||
[ms-v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/milestone/95?closed=1
|
||||
|
||||
## [v0.107.59] - 2025-03-21
|
||||
|
||||
See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
|
||||
|
||||
[ms-v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/milestone/94?closed=1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Rules with the `client` modifier not working ([#7708]).
|
||||
|
||||
- The search form not working in the query log ([#7704]).
|
||||
@@ -97,8 +37,6 @@ See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
|
||||
[#7704]: https://github.com/AdguardTeam/AdGuardHome/issues/7704
|
||||
[#7708]: https://github.com/AdguardTeam/AdGuardHome/issues/7708
|
||||
|
||||
[ms-v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/milestone/94?closed=1
|
||||
|
||||
## [v0.107.58] - 2025-03-19
|
||||
|
||||
See also the [v0.107.58 GitHub milestone][ms-v0.107.58].
|
||||
@@ -3130,13 +3068,11 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
[ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.62...HEAD
|
||||
[v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...v0.107.62
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...HEAD
|
||||
[v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...HEAD
|
||||
[v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61
|
||||
[v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...HEAD
|
||||
[v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59
|
||||
[v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58
|
||||
[v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...v0.107.57
|
||||
|
||||
6
Makefile
6
Makefile
@@ -27,7 +27,7 @@ DIST_DIR = dist
|
||||
GOAMD64 = v1
|
||||
GOPROXY = https://proxy.golang.org|direct
|
||||
GOTELEMETRY = off
|
||||
GOTOOLCHAIN = go1.24.2
|
||||
GOTOOLCHAIN = go1.24.1
|
||||
GPG_KEY = devteam@adguard.com
|
||||
GPG_KEY_PASSPHRASE = not-a-real-password
|
||||
NPM = npm
|
||||
@@ -38,6 +38,7 @@ REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
|
||||
SIGN = 1
|
||||
SIGNER_API_KEY = not-a-real-key
|
||||
VERSION = v0.0.0
|
||||
YARN = yarn
|
||||
|
||||
NEXTAPI = 0
|
||||
|
||||
@@ -138,4 +139,5 @@ txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
||||
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
|
||||
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh
|
||||
|
||||
# TODO(a.garipov): Re-add openapi-lint.
|
||||
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
||||
openapi-show: ; cd ./openapi/ && $(YARN) start
|
||||
|
||||
@@ -205,9 +205,9 @@ Run `make init` to prepare the development environment.
|
||||
|
||||
You will need this to build AdGuard Home:
|
||||
|
||||
- [Go](https://golang.org/dl/) v1.24 or later;
|
||||
- [Node.js](https://nodejs.org/en/download/) v20.19 or later;
|
||||
- [npm](https://www.npmjs.com/) v10.8 or later;
|
||||
- [Go](https://golang.org/dl/) v1.23 or later;
|
||||
- [Node.js](https://nodejs.org/en/download/) v18.18 or later;
|
||||
- [npm](https://www.npmjs.com/) v8 or later;
|
||||
|
||||
### <a href="#building" id="building" name="building">Building</a>
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||
|
||||
'stages':
|
||||
- 'Build frontend':
|
||||
@@ -50,7 +50,7 @@
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'key': 'BF'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
@@ -157,7 +157,6 @@
|
||||
|
||||
# Print Docker info.
|
||||
docker info
|
||||
docker buildx version
|
||||
|
||||
# Prepare and push the build.
|
||||
env \
|
||||
@@ -278,8 +277,8 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
@@ -294,5 +293,5 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||
'channel': 'development'
|
||||
|
||||
'stages':
|
||||
@@ -39,7 +39,7 @@
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'key': 'JSTEST'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
@@ -103,7 +103,7 @@
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'key': 'BF'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
@@ -178,7 +178,7 @@
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'key': 'E2ETEST'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
@@ -233,6 +233,6 @@
|
||||
# Set the default release channel on the release branch to beta, as we
|
||||
# may need to build a few of these.
|
||||
'variables':
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||
'channel': 'candidate'
|
||||
|
||||
1207
client/package-lock.json
generated
vendored
1207
client/package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
4
client/package.json
vendored
4
client/package.json
vendored
@@ -66,7 +66,7 @@
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^17.0.80",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-redux": "^7.1.33",
|
||||
@@ -99,7 +99,7 @@
|
||||
"stylelint": "^16.5.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"vitest": "^3.1.1",
|
||||
"vitest": "^3.0.4",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"client_settings": "Налады кліентаў",
|
||||
"example_upstream_reserved": "upstream <0>для канкрэтных даменаў</0>;",
|
||||
"example_multiple_upstreams_reserved": "некалькі сервер DNSаў <0>для канкрэтных даменаў</0>;",
|
||||
"example_multiple_upstreams_reserved": "некалькі DNS-сервераў <0>для канкрэтных даменаў</0>;",
|
||||
"example_upstream_comment": "каментар.",
|
||||
"upstream_parallel": "Ужыць адначасныя запыты да ўсіх сервераў для паскарэння апрацоўкі запыту",
|
||||
"parallel_requests": "Паралельныя запыты",
|
||||
"load_balancing": "Размеркаванне нагрузкі",
|
||||
"load_balancing_desc": "Запытвайце па адным серверы за раз. AdGuard Home будзе выкарыстоўваць выпадковы алгарытм для выбару сервера, так што самы хуткі сервер будзе выкарыстоўвацца часцей.",
|
||||
"bootstrap_dns": "Bootstrap сервер DNSы",
|
||||
"bootstrap_dns_desc": "IP-адрасы сервер DNSаў, якія выкарыстоўваюцца для вырашэння IP-адрасоў распознавальнікаў DoH/DoT, якія вы ўказваеце ў якасці перадачы. Каментары не дапускаюцца.",
|
||||
"fallback_dns_title": "Рэзервовыя сервер DNSы",
|
||||
"fallback_dns_desc": "Спіс рэзервовых сервер DNSаў, якія выкарыстоўваюцца, калі вышэйшыя сервер DNSы не адказваюць. Сінтаксіс такі ж, як і ў галоўным полі ўверх.",
|
||||
"bootstrap_dns": "Bootstrap DNS-серверы",
|
||||
"bootstrap_dns_desc": "IP-адрасы DNS-сервераў, якія выкарыстоўваюцца для вырашэння IP-адрасоў распознавальнікаў DoH/DoT, якія вы ўказваеце ў якасці перадачы. Каментары не дапускаюцца.",
|
||||
"fallback_dns_title": "Рэзервовыя DNS-серверы",
|
||||
"fallback_dns_desc": "Спіс рэзервовых DNS-сервераў, якія выкарыстоўваюцца, калі вышэйшыя DNS-серверы не адказваюць. Сінтаксіс такі ж, як і ў галоўным полі ўверх.",
|
||||
"fallback_dns_placeholder": "Увядзіце па адным рэзервовым серверы DNS у радку",
|
||||
"local_ptr_title": "Прыватныя сервер DNSы",
|
||||
"local_ptr_title": "Прыватныя DNS-серверы",
|
||||
"local_ptr_desc": "DNS-серверы, якія AdGuard Home выкарыстоўвае для лакальных PTR-запытаў. Гэтыя серверы выкарыстоўваюцца, каб атрымаць даменавыя імёны кліентаў з прыватнымі IP-адрасамі, напрыклад «192.168.12.34», з дапамогай rDNS. Калі спіс пусты, AdGuard Home выкарыстоўвае прадвызначаныя DNS-серверы вашай АС.",
|
||||
"local_ptr_default_resolver": "Па змаўчанні AdGuard Home выкарыстоўвае наступныя зваротныя DNS-рэзолверы: {{ip}}.",
|
||||
"local_ptr_no_default_resolver": "AdGuard Home не змог вызначыць прыдатныя прыватныя адваротныя DNS-рэзолверы для гэтай сістэмы.",
|
||||
"local_ptr_placeholder": "Увядзіце па адным адрасе на радок",
|
||||
"resolve_clients_title": "Уключыць запытванне даменавых імёнаў для кліентаў",
|
||||
"resolve_clients_desc": "AdGuard Home будзе спрабаваць аўтаматычна вызначыць даменавыя імёны кліентаў праз PTR-запыты да адпаведных сервераў (прыватны сервер DNS для лакальных кліентаў, upstream-серверы для кліентаў з публічным IP-адрасам).",
|
||||
"resolve_clients_desc": "AdGuard Home будзе спрабаваць аўтаматычна вызначыць даменавыя імёны кліентаў праз PTR-запыты да адпаведных сервераў (прыватны DNS-сервер для лакальных кліентаў, upstream-серверы для кліентаў з публічным IP-адрасам).",
|
||||
"use_private_ptr_resolvers_title": "Ужываць прыватныя адваротныя DNS-рэзолверы",
|
||||
"use_private_ptr_resolvers_desc": "Пасылаць адваротныя DNS-запыты для лакальна абслугоўных адрасоў на паказаныя серверы. Калі адключана, AdGuard Home будзе адказваць NXDOMAIN на ўсе падобныя PTR-запыты, апроч запытаў пра кліентаў, ужо вядомых па DHCP, /etc/hosts і гэтак далей.",
|
||||
"check_dhcp_servers": "Праверыць DHCP-серверы",
|
||||
@@ -101,13 +101,13 @@
|
||||
"compact": "Компактный",
|
||||
"nothing_found": "Нічога не знойдзена",
|
||||
"faq": "FAQ",
|
||||
"version": "Версія",
|
||||
"version": "версія",
|
||||
"address": "Адрас",
|
||||
"protocol": "Пратакол",
|
||||
"on": "УКЛ",
|
||||
"off": "Выкл",
|
||||
"copyright": "Усе правы захаваныя",
|
||||
"homepage": "Хатняя старонка",
|
||||
"homepage": "Галоўная",
|
||||
"report_an_issue": "Паведаміць пра праблему",
|
||||
"privacy_policy": "Палітыка прыватнасці",
|
||||
"enable_protection": "Уключыць абарону",
|
||||
@@ -165,8 +165,8 @@
|
||||
"custom_filtering_rules": "Карыстальніцкія правілы фільтрацыі",
|
||||
"encryption_settings": "Налады шыфравання",
|
||||
"dhcp_settings": "Налады DHCP",
|
||||
"upstream_dns": "Upstream сервер DNSы",
|
||||
"upstream_dns_help": "Увядзіце адрасы сервераў па адным у радку. <a>Даведацца больш </a> пра наладжванне сервер DNSаў.",
|
||||
"upstream_dns": "Upstream DNS-серверы",
|
||||
"upstream_dns_help": "Увядзіце адрасы сервераў па адным у радку. <a>Даведацца больш </a> пра наладжванне DNS-сервераў.",
|
||||
"upstream_dns_configured_in_file": "Наладжаны ў {{path}}",
|
||||
"test_upstream_btn": "Тэст upstream сервераў",
|
||||
"upstreams": "Upstreams",
|
||||
@@ -182,7 +182,7 @@
|
||||
"enabled_save_search_toast": "Уключаны бяспечны пошук",
|
||||
"updated_save_search_toast": "Налады бяспечнага пошуку абноўлены",
|
||||
"enabled_table_header": "УКЛ.",
|
||||
"name_table_header": "Назва",
|
||||
"name_table_header": "Імя",
|
||||
"list_url_table_header": "URL-адрас спіса",
|
||||
"rules_count_table_header": "Колькасць правілаў:",
|
||||
"last_time_updated_table_header": "Апошняе абнаўленне",
|
||||
@@ -196,7 +196,7 @@
|
||||
"no_whitelist_added": "Белыя спісы не дададзены",
|
||||
"add_blocklist": "Дадаць чорны спіс",
|
||||
"add_allowlist": "Дадаць белы спіс",
|
||||
"cancel_btn": "Скасаваць",
|
||||
"cancel_btn": "Адмена",
|
||||
"enter_name_hint": "Увядзіце імя",
|
||||
"enter_url_or_path_hint": "Увядзіце URL-адрас ці абсалютны шлях да спіса",
|
||||
"check_updates_btn": "Праверыць абнаўленні",
|
||||
@@ -219,7 +219,7 @@
|
||||
"example_meaning_host_block": "адказаць 127.0.0.1 для example.org (але не для яго паддаменаў);",
|
||||
"example_comment": "! Так можна дадаваць апісанне.",
|
||||
"example_comment_meaning": "каментар;",
|
||||
"example_comment_hash": "# Таксама каментарый.",
|
||||
"example_comment_hash": "# І вось так таксама.",
|
||||
"example_regex_meaning": "блакаваць доступ да даменаў, якія адпавядаюць зададзенаму рэгулярнаму выразу.",
|
||||
"example_upstream_regular": "звычайны DNS (наўзверх UDP);",
|
||||
"example_upstream_regular_port": "звычайны DNS (праз UDP, імя хаста);",
|
||||
@@ -233,13 +233,13 @@
|
||||
"example_upstream_tcp_port": "звычайны DNS (праз TCP, імя хаста);",
|
||||
"example_upstream_tcp_hostname": "звычайны DNS (праз TCP, імя хаста);",
|
||||
"all_lists_up_to_date_toast": "Усе спісы ўжо абноўлены",
|
||||
"updated_upstream_dns_toast": "Upstream сервер DNSы абноўлены",
|
||||
"updated_upstream_dns_toast": "Upstream DNS-серверы абноўлены",
|
||||
"dns_test_ok_toast": "Паказаныя серверы DNS працуюць карэктна",
|
||||
"dns_test_not_ok_toast": "Сервер «{{key}}»: немагчыма выкарыстоўваць, праверце слушнасць напісання",
|
||||
"dns_test_parsing_error_toast": "Раздзел {{section}}: радок {{line}}: немагчыма выкарыстоўваць, праверце слушнасць напісання",
|
||||
"dns_test_warning_toast": "Upstream «{{key}}» не адказвае на тэставыя запыты і можа не працаваць належным чынам",
|
||||
"unblock": "Адблакаваць",
|
||||
"block": "Заблакіраваць",
|
||||
"block": "Заблакаваць",
|
||||
"disallow_this_client": "Забараніць доступ гэтаму кліенту",
|
||||
"allow_this_client": "Дазволіць доступ гэтаму кліенту",
|
||||
"block_for_this_client_only": "Заблакаваць толькі для гэтага кліента",
|
||||
@@ -259,7 +259,7 @@
|
||||
"no_logs_found": "Логі не знойдзены",
|
||||
"refresh_btn": "Абнавіць",
|
||||
"previous_btn": "Назад",
|
||||
"next_btn": "Далей",
|
||||
"next_btn": "Наперад",
|
||||
"loading_table_status": "Загрузка...",
|
||||
"page_table_footer_text": "Старонка",
|
||||
"rows_table_footer_text": "радкоў",
|
||||
@@ -280,7 +280,7 @@
|
||||
"query_log_retention_confirm": "Вы ўпэўнены, што хочаце змяніць тэрмін захоўвання запытаў? Пры памяншэнні інтэрвалу, некаторыя даныя могуць быць страчаны",
|
||||
"anonymize_client_ip": "Ананімізацыя IP-адрасы кліента",
|
||||
"anonymize_client_ip_desc": "Не захоўвайце поўныя IP-адрасы гэтых удзельнікаў у часопісах або статыстыцы",
|
||||
"dns_config": "Налады сервер DNSа",
|
||||
"dns_config": "Налады DNS-сервера",
|
||||
"dns_cache_config": "Налада кэша DNS",
|
||||
"dns_cache_config_desc": "Тут можна наладзіць кэш DNS",
|
||||
"blocking_mode": "Рэжым блакавання",
|
||||
@@ -342,14 +342,14 @@
|
||||
"unknown_filter": "Невядомы фільтр {{filterId}}",
|
||||
"known_tracker": "Вядомы трэкер",
|
||||
"install_welcome_title": "Сардэчна запрашаем у AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home – гэта сервер DNS, што блакуе рэкламу і трэкінг. Яго мэта – даць вам магчымасць кантраляваць усю ваша сеціва і ўсе падлучаныя прылады. Ён не патрабуе ўсталёўкі кліенцкіх праграм.",
|
||||
"install_welcome_desc": "AdGuard Home – гэта DNS-сервер, што блакуе рэкламу і трэкінг. Яго мэта – даць вам магчымасць кантраляваць усю ваша сеціва і ўсе падлучаныя прылады. Ён не патрабуе ўсталёўкі кліенцкіх праграм.",
|
||||
"install_settings_title": "Ўэб-інтэрфейс адміністравання",
|
||||
"install_settings_listen": "Інтэрфейс сеціва",
|
||||
"install_settings_port": "Порт",
|
||||
"install_settings_interface_link": "Ваш ўэб-інтэрфейс адміністравання AdGuard Home будзе даступны па наступных адрасах:",
|
||||
"form_error_port": "Увядзіце карэктны нумар порта",
|
||||
"install_settings_dns": "DNS-сервер",
|
||||
"install_settings_dns_desc": "Вам будзе трэба наладзіць свае прылады ці роўтар на выкарыстанне сервер DNSа на адным з наступных адрасоў:",
|
||||
"install_settings_dns_desc": "Вам будзе трэба наладзіць свае прылады ці роўтар на выкарыстанне DNS-сервера на адным з наступных адрасоў:",
|
||||
"install_settings_all_interfaces": "Усе інтэрфейсы",
|
||||
"install_auth_title": "Аўтарызацыя",
|
||||
"install_auth_desc": "Настойліва рэкамендуецца наладзіць аўтэнтыфікацыю паролем для ўэб-інтэрфейсу AdGuard Home. Нават калі ён даступны толькі ў вашай лакальнай сетцы, важна абараніць яго ад неабмежаванага доступу.",
|
||||
@@ -365,17 +365,17 @@
|
||||
"install_submit_desc": "Працэдура налады завершана і вы гатовы пачаць выкарыстанне AdGuard Home.",
|
||||
"install_devices_router": "Роўтар",
|
||||
"install_devices_router_desc": "Такая наладка аўтаматычна пакрые ўсе прылады, што выкарыстоўваюць ваш хатні роўтар, і вам не трэба будзе наладжваць кожнае з іх у асобнасці.",
|
||||
"install_devices_address": "сервер DNS AdGuard Home даступны па наступных адрасах",
|
||||
"install_devices_address": "DNS-сервер AdGuard Home даступны па наступных адрасах",
|
||||
"install_devices_router_list_1": "Адкрыйце налады вашага роўтара. Звычайна вы можаце адкрыць іх у вашым браўзары, напрыклад, http://192.168.0.1/ ці http://192.168.1.1/. Вас могуць папрасіць увесці пароль. Калі вы не помніце яго, пароль часта можна скінуць, націснуўшы на кнопку на самым роўтары. Некаторыя роўтары патрабуюць адмысловага дадатку, які ў гэтым выпадку павінен быць ужо ўсталявана на ваш кампутар ці тэлефон.",
|
||||
"install_devices_router_list_2": "Знайдзіце налады DHCP ці DNS. Знайдзіце літары «DNS» поруч з тэкставым полем, у якое можна ўвесці два ці тры шэрагі лічбаў, падзеленых на 4 групы ад адной до трох лічбаў.",
|
||||
"install_devices_router_list_3": "Увядзіце туды адрас вашага AdGuard Home.",
|
||||
"install_devices_router_list_4": "Вы не можаце ўсталяваць уласны сервер DNS на некаторых тыпах маршрутызатараў. У гэтым выпадку можа дапамагчы налада AdGuard Home у якасці <a href='#dhcp'>DHCP-сервера</a>. У адваротным выпадку вам трэба звярнуцца да кіраўніцтва па наладзе сервер DNSаў для вашай пэўнай мадэлі маршрутызатара.",
|
||||
"install_devices_router_list_4": "Вы не можаце ўсталяваць уласны DNS-сервер на некаторых тыпах маршрутызатараў. У гэтым выпадку можа дапамагчы налада AdGuard Home у якасці <a href='#dhcp'>DHCP-сервера</a>. У адваротным выпадку вам трэба звярнуцца да кіраўніцтва па наладзе DNS-сервераў для вашай пэўнай мадэлі маршрутызатара.",
|
||||
"install_devices_windows_list_1": "Адкрыйце Панэль кіравання праз меню «Пуск» ці праз пошук Windows.",
|
||||
"install_devices_windows_list_2": "Перайдзіце ў «Сеціва і інтэрнэт», а потым у «Цэнтр кіравання сеціва і агульным доступам».",
|
||||
"install_devices_windows_list_3": "У левым боку экрана клікніце «Змена параметраў адаптара».",
|
||||
"install_devices_windows_list_4": "Пстрыкніце правай кнопкай мышы ваша актыўнае злучэнне і абярыце Уласцівасці.",
|
||||
"install_devices_windows_list_5": "Знайдзіце ў спісе пункт «IP версіі 4 (TCP/IPv4)», вылучыце яго і потым ізноў націсніце «Уласцівасці».",
|
||||
"install_devices_windows_list_6": "Абярыце «Выкарыстаць наступныя адрасы сервер DNSаў» і ўвядзіце адрас AdGuard Home.",
|
||||
"install_devices_windows_list_6": "Абярыце «Выкарыстаць наступныя адрасы DNS-сервераў» і ўвядзіце адрас AdGuard Home.",
|
||||
"install_devices_macos_list_1": "Клікніце па абразку Apple і перайдзіце ў Сістэмныя налады.",
|
||||
"install_devices_macos_list_2": "Клікніце па іконцы Сеціва.",
|
||||
"install_devices_macos_list_3": "Абярыце першае падлучэнне ў спісе і націсніце кнопку «Дадаткова».",
|
||||
@@ -415,7 +415,7 @@
|
||||
"encryption_key": "Прыватны ключ",
|
||||
"encryption_key_input": "Скапіюйце сюды прыватны ключ у PEM-кадоўцы.",
|
||||
"encryption_enable": "Уключыць шыфраванне (HTTPS, DNS-over-HTTPS і DNS-over-TLS)",
|
||||
"encryption_enable_desc": "Калі шыфраванне ўлучана, ўэб-інтэрфейс AdGuard Home будзе працаваць па HTTPS, а сервер DNS будзе таксама працаваць па DNS-over-HTTPS і DNS-over-TLS.",
|
||||
"encryption_enable_desc": "Калі шыфраванне ўлучана, ўэб-інтэрфейс AdGuard Home будзе працаваць па HTTPS, а DNS-сервер будзе таксама працаваць па DNS-over-HTTPS і DNS-over-TLS.",
|
||||
"encryption_chain_valid": "Ланцужок сертыфікатаў валідны",
|
||||
"encryption_chain_invalid": "Ланцужок сертыфікатаў не валідны",
|
||||
"encryption_key_valid": "Валідны {{type}} прыватны ключ",
|
||||
@@ -435,8 +435,8 @@
|
||||
"update_announcement": "AdGuard Home {{version}} ужо даступная! <0>Націсніце сюды</0>, каб даведацца больш.",
|
||||
"setup_guide": "Інструкцыя па наладзе",
|
||||
"dns_addresses": "Адрасы DNS",
|
||||
"dns_start": "сервер DNS запускаецца",
|
||||
"dns_status_error": "Памылка праверкі стану сервер DNSа",
|
||||
"dns_start": "DNS-сервер запускаецца",
|
||||
"dns_status_error": "Памылка праверкі стану DNS-сервера",
|
||||
"down": "Уніз",
|
||||
"fix": "Выправіць",
|
||||
"dns_providers": "<0>Спіс вядомых DNS-правайдараў</0> на выбар.",
|
||||
@@ -449,7 +449,7 @@
|
||||
"settings_global": "Глабальныя",
|
||||
"settings_custom": "Свае",
|
||||
"table_client": "Кліент",
|
||||
"table_name": "Назва",
|
||||
"table_name": "Імя",
|
||||
"save_btn": "Захаваць",
|
||||
"client_add": "Дадаць кліента",
|
||||
"client_new": "Новы кліент",
|
||||
@@ -475,7 +475,7 @@
|
||||
"auto_clients_title": "Кліенты (runtime)",
|
||||
"auto_clients_desc": "Інфармацыя аб IP-адрасах прылад, якія выкарыстоўваюць або могуць выкарыстоўваць AdGuard Home. Гэтая інфармацыя збіраецца з некалькіх крыніц, уключаючы файлы хостаў, зваротны DNS і г.д.",
|
||||
"access_title": "Налады доступу",
|
||||
"access_desc": "Тут вы можаце наладзіць правілы доступу да сервер DNSу AdGuard Home",
|
||||
"access_desc": "Тут вы можаце наладзіць правілы доступу да DNS-серверу AdGuard Home",
|
||||
"access_allowed_title": "Дазволеныя кліенты",
|
||||
"access_allowed_desc": "Спіс CIDR, IP-адрасоў або <a>ClientID</a>. Калі ў гэтым спісе ёсць запісы, AdGuard Home будзе прымаць запыты толькі ад гэтых кліентаў.",
|
||||
"access_disallowed_title": "Забароненыя кліенты",
|
||||
@@ -596,7 +596,7 @@
|
||||
"disable_ipv6_desc": "Ігнараваць усе запыты DNS для адрасоў IPv6 (тып AAAA) і выдаленне дадзеных IPv6 з адказаў тыпу HTTPS.",
|
||||
"fastest_addr": "Найхуткі IP-адрас",
|
||||
"fastest_addr_desc": "Апытайце ўсе DNS-серверы і вярніце самы хуткі IP-адрас сярод усіх адказаў. Гэта замарудзіць выкананне DNS-запытаў, бо нам давядзецца чакаць адказаў ад усіх DNS-сервераў, але палепшыць агульную ўзаемасувязь.",
|
||||
"autofix_warning_text": "Пры націску «Выправіць» AdGuard Home наладзіць вашу сістэму на выкарыстанне сервер DNSа AdGuard Home.",
|
||||
"autofix_warning_text": "Пры націску «Выправіць» AdGuard Home наладзіць вашу сістэму на выкарыстанне DNS-сервера AdGuard Home.",
|
||||
"autofix_warning_list": "Будуць выконвацца наступныя заданні: <0>Дэактываваць сістэмны DNSStubListener</0> <0>Усталяваць адрас сервера DNS на 127.0.0.1</0> <0>Стварыць сімвалічную спасылку /etc/resolv.conf на /run/systemd/resolve/resolv.conf</0> <0>Спыніць DNSStubListener (перазагрузіць сістэмную службу)</0>.",
|
||||
"autofix_warning_result": "У выніку ўсе DNS-запыты ад вашай сістэмы будуць па змаўчанні апрацоўвацца AdGuard Home.\n",
|
||||
"tags_title": "Тэгі",
|
||||
@@ -634,12 +634,12 @@
|
||||
"validated_with_dnssec": "Проверено с помощью DNSSEC",
|
||||
"all_queries": "Усе запыты",
|
||||
"show_blocked_responses": "Заблакавана",
|
||||
"show_whitelisted_responses": "У белым спісе",
|
||||
"show_whitelisted_responses": "Белы спіс",
|
||||
"show_processed_responses": "Апрацавана",
|
||||
"blocked_safebrowsing": "Заблакіравана згодна з базай даных Safe Browsing",
|
||||
"blocked_adult_websites": "Заблакавана Бацькоўскім кантролем",
|
||||
"blocked_threats": "Заблакавана пагроз",
|
||||
"allowed": "У белым спісе",
|
||||
"allowed": "Дазволены",
|
||||
"filtered": "Адфільтраваныя",
|
||||
"rewritten": "Перапісаныя",
|
||||
"safe_search": "Бяспечны пошук",
|
||||
@@ -738,7 +738,7 @@
|
||||
"thursday_short": "Чц.",
|
||||
"friday_short": "Пт.",
|
||||
"saturday_short": "Сб.",
|
||||
"upstream_dns_cache_configuration": "Канфігурацыя кэша upstream сервер DNSаў",
|
||||
"upstream_dns_cache_configuration": "Канфігурацыя кэша upstream DNS-сервераў",
|
||||
"enable_upstream_dns_cache": "Ўключыць кэшаванне для карыстацкай канфігурацыі upstream-сервераў гэтага кліента",
|
||||
"dns_cache_size": "Памер кэша DNS, у байтах"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"filter": "Филтър",
|
||||
"query_log": "История на заявките",
|
||||
"compact": "Compact",
|
||||
"nothing_found": "Нищо не е намерено",
|
||||
"faq": "ЧЗВ",
|
||||
"version": "версия",
|
||||
"address": "Адрес",
|
||||
@@ -66,12 +65,14 @@
|
||||
"stats_malware_phishing": "вируси/атаки",
|
||||
"stats_adult": "сайтове за възрастни",
|
||||
"stats_query_domain": "Най-отваряни страници",
|
||||
"for_last_24_hours": "за последните 24 часа",
|
||||
"no_domains_found": "Няма намерени резултати",
|
||||
"requests_count": "Сума на заявките",
|
||||
"top_blocked_domains": "Най-блокирани страници",
|
||||
"top_clients": "Най-активни IP адреси",
|
||||
"no_clients_found": "Нямa намерени адреси",
|
||||
"general_statistics": "Обща статисика",
|
||||
"number_of_dns_query_24_hours": "Сума на DNS заявки за последните 24 часа",
|
||||
"number_of_dns_query_blocked_24_hours": "Сума на блокирани DNS заявки от филтрите за реклама и местни",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Сума на блокирани DNS заявки от AdGuard свързани със сигурността",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Сума на блокирани сайтове за възрастни",
|
||||
@@ -155,7 +156,6 @@
|
||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
|
||||
"default": "По подразбиране",
|
||||
"custom_ip": "Персонализиран IP",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-пред-HTTPS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"plain_dns": "Обикновен DNS",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Zakázaný",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Velikost mezipaměti",
|
||||
"cache_size_desc": "Velikost mezipaměti DNS (v bajtech). Chcete-li ukládání do mezipaměti zakázat, nastavte 0.",
|
||||
"cache_size_desc": "Velikost mezipaměti DNS (v bajtech). Chcete-li ukládání do mezipaměti zakázat, ponechte prázdné.",
|
||||
"cache_ttl_min_override": "Přepsat minimální hodnotu TTL",
|
||||
"cache_ttl_max_override": "Přepsat maximální hodnotu TTL",
|
||||
"enter_cache_size": "Zadejte velikost mezipaměti (v bajtech)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Sortliste",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Cache-størrelse",
|
||||
"cache_size_desc": "DNS cache-størrelse (i bytes). Sæt til 0 for at deaktivere cache.",
|
||||
"cache_size_desc": "DNS cache-størrelse (i bytes). Lad stå tomt for at deaktivere cache.",
|
||||
"cache_ttl_min_override": "Tilsidesæt minimum TTL",
|
||||
"cache_ttl_max_override": "Tilsidesæt maksimal TTL",
|
||||
"enter_cache_size": "Angiv cache-størrelse (bytes)",
|
||||
|
||||
@@ -97,9 +97,9 @@
|
||||
"settings": "Einstellungen",
|
||||
"filters": "Filter",
|
||||
"filter": "Filter",
|
||||
"query_log": "Abfrageprotokoll",
|
||||
"query_log": "Anfragenprotokoll",
|
||||
"compact": "Kompakt",
|
||||
"nothing_found": "Nichts gefunden",
|
||||
"nothing_found": "Nichts gefunden\n",
|
||||
"faq": "FAQ",
|
||||
"version": "Version",
|
||||
"address": "Adresse",
|
||||
@@ -199,7 +199,7 @@
|
||||
"cancel_btn": "Abbrechen",
|
||||
"enter_name_hint": "Name eingeben",
|
||||
"enter_url_or_path_hint": "URL oder absoluten Pfad der Liste eingeben",
|
||||
"check_updates_btn": "Nach Updates suchen",
|
||||
"check_updates_btn": "Nach Aktualisierungen suchen",
|
||||
"new_blocklist": "Neue Sperrliste",
|
||||
"new_allowlist": "Neue Positivliste",
|
||||
"edit_blocklist": "Sperrliste bearbeiten",
|
||||
@@ -566,7 +566,7 @@
|
||||
"ignore_domains": "Ignorierte Domains (durch Zeilenumbruch getrennt)",
|
||||
"ignore_domains_title": "Ignorierte Domains",
|
||||
"ignore_domains_desc_stats": "Abfragen, die diesen Regeln entsprechen, werden nicht in die Statistik aufgenommen",
|
||||
"ignore_domains_desc_query": "Abfragen, die diesen Regeln entsprechen, werden nicht in das Abfrageprotokoll aufgenommen",
|
||||
"ignore_domains_desc_query": "Abfragen, die diesen Regeln entsprechen, werden nicht in das Anfragenprotokoll aufgenommen",
|
||||
"interval_hours": "{{count}} Stunde",
|
||||
"interval_hours_plural": "{{count}} Stunden",
|
||||
"filters_configuration": "Filterkonfiguration",
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Sperrliste",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Größe des Cache",
|
||||
"cache_size_desc": "Größe des DNS-Cache (in Bytes). Um das Caching zu deaktivieren, setzen Sie den Wert auf 0.",
|
||||
"cache_size_desc": "Größe des DNS-Zwischenspeichers (in Bytes)",
|
||||
"cache_ttl_min_override": "TTL-Minimalwert überschreiben",
|
||||
"cache_ttl_max_override": "TTL-Höchstwert überschreiben",
|
||||
"enter_cache_size": "Größe des Cache (Bytes) eingeben",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Blocklist",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Cache size",
|
||||
"cache_size_desc": "DNS cache size (in bytes). To disable caching, set to 0.",
|
||||
"cache_size_desc": "DNS cache size (in bytes). To disable caching, leave empty.",
|
||||
"cache_ttl_min_override": "Override minimum TTL",
|
||||
"cache_ttl_max_override": "Override maximum TTL",
|
||||
"enter_cache_size": "Enter cache size (bytes)",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"client_settings": "Configuración de clientes",
|
||||
"example_upstream_reserved": "un proveedor DNS <0>para un dominio específico</0>.",
|
||||
"example_multiple_upstreams_reserved": "múltiples proveedores DNS <0>para dominios específicos</0>.",
|
||||
"example_upstream_reserved": "un DNS de subida <0>para un dominio específico</0>.",
|
||||
"example_multiple_upstreams_reserved": "múltiples upstreams <0>para dominios específicos</0>;",
|
||||
"example_upstream_comment": "un comentario.",
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los proveedores DNS.",
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los servidores DNS de subida.",
|
||||
"parallel_requests": "Consultas paralelas",
|
||||
"load_balancing": "Balanceo de carga",
|
||||
"load_balancing_desc": "Consulta un proveedor DNS a la vez.<br/>AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo promedio de búsqueda.",
|
||||
"load_balancing_desc": "Consulta un servidor Dns upstream a la vez.<br/>AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo medio de búsqueda.",
|
||||
"bootstrap_dns": "Servidores DNS de arranque",
|
||||
"bootstrap_dns_desc": "Direcciones IP de los servidores DNS utilizados para resolver las direcciones IP de los resolutores DoH/DoT que especifiques como proveedores DNS. No se permiten comentarios.",
|
||||
"bootstrap_dns_desc": "Direcciones IP de servidores DNS utilizadas para resolver direcciones IP de los solucionadores DoH/DoT que especifiques como ascendentes. No se permiten comentarios.",
|
||||
"fallback_dns_title": "Servidores DNS alternativos",
|
||||
"fallback_dns_desc": "Lista de servidores DNS alternativos utilizados cuando los proveedores DNS no responden. La sintaxis es la misma que en el campo de los principales proveedores DNS anterior.",
|
||||
"fallback_dns_desc": "Lista de servidores DNS alternativos utilizados cuando los servidores DNS de subida no responden. La sintaxis es la misma que en el campo de los principales DNS de subida anterior.",
|
||||
"fallback_dns_placeholder": "Ingresa un servidor DNS alternativo por línea",
|
||||
"local_ptr_title": "Servidores DNS inversos y privados",
|
||||
"local_ptr_desc": "Los servidores DNS que AdGuard Home utiliza para consultas PTR, SOA y NS privadas. La petición se considera privada si solicita un dominio ARPA que contiene una subred dentro de rangos IP privados, por ejemplo \"192.168.12.34\", y procede de un cliente con dirección privada. Si no se configura, AdGuard Home utiliza las direcciones de los resolvedores DNS predeterminados de tu sistema operativo, excepto las direcciones del propio AdGuard Home.",
|
||||
@@ -18,9 +18,9 @@
|
||||
"local_ptr_no_default_resolver": "AdGuard Home no pudo determinar los resolutores DNS inversos y privados adecuados para este sistema.",
|
||||
"local_ptr_placeholder": "Ingresa una dirección IP por línea",
|
||||
"resolve_clients_title": "Habilitar la resolución inversa de las direcciones IP de clientes",
|
||||
"resolve_clients_desc": "Resuelve de manera inversa las direcciones IP de los clientes a sus nombres de hosts enviando consultas PTR a los resolutores correspondientes (servidores DNS privados para clientes locales, proveedores DNS para clientes con direcciones IP públicas).",
|
||||
"resolve_clients_desc": "Resolve de manera inversa las direcciones IP de los clientes a sus nombres de hosts enviando consultas PTR a los resolutores correspondientes (servidores DNS privados para clientes locales, servidores DNS de subida para clientes con direcciones IP públicas).",
|
||||
"use_private_ptr_resolvers_title": "Usar resolutores DNS inversos y privados",
|
||||
"use_private_ptr_resolvers_desc": "Resuelve peticiones PTR, SOA y NS para dominios ARPA que contienen direcciones IP privadas a través de proveedores DNS privados, DHCP, /etc/hosts, etc. Si se deshabilita, AdGuard Home responderá a todas estas peticiones con NXDOMAIN.",
|
||||
"use_private_ptr_resolvers_desc": "Resolver las peticiones PTR, SOA y NS para dominios ARPA que contienen direcciones privadas utilizando servidores upstream privados, DHCP, /etc/hosts, etc. Si se desactiva, AdGuard Home responde a todas estas consultas con NXDOMAIN.",
|
||||
"check_dhcp_servers": "Comprobar si hay servidores DHCP",
|
||||
"save_config": "Guardar configuración",
|
||||
"enabled_dhcp": "Servidor DHCP habilitado",
|
||||
@@ -132,8 +132,8 @@
|
||||
"top_clients": "Clientes más frecuentes",
|
||||
"no_clients_found": "No se han encontrado clientes",
|
||||
"general_statistics": "Estadísticas generales",
|
||||
"top_upstreams": "Proveedores DNS más frecuentes",
|
||||
"no_upstreams_data_found": "No se han encontrado datos de proveedores DNS",
|
||||
"top_upstreams": "DNS de subida más frecuentes",
|
||||
"no_upstreams_data_found": "No se han encontrado datos de DNS de subida",
|
||||
"number_of_dns_query_days": "Número de consultas DNS procesadas durante el último {{count}} día",
|
||||
"number_of_dns_query_days_plural": "Número de consultas DNS procesadas durante los últimos {{count}} días",
|
||||
"number_of_dns_query_hours": "Número de consultas DNS procesadas durante la última {{count}} hora",
|
||||
@@ -144,7 +144,7 @@
|
||||
"enforced_save_search": "Búsquedas seguras forzadas",
|
||||
"number_of_dns_query_to_safe_search": "Número de peticiones DNS a los motores de búsqueda para los que se aplicó la búsqueda segura forzada",
|
||||
"average_processing_time": "Tiempo promedio de procesamiento",
|
||||
"average_upstream_response_time": "Tiempo promedio de respuesta del proveedor DNS",
|
||||
"average_upstream_response_time": "Tiempo promedio de respuesta upstream",
|
||||
"response_time": "Tiempo de respuesta",
|
||||
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una petición DNS",
|
||||
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
|
||||
@@ -157,7 +157,7 @@
|
||||
"enforce_save_search_hint": "AdGuard Home reforzará la búsqueda segura en los siguientes motores de búsqueda: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex y Pixabay.",
|
||||
"no_servers_specified": "No hay servidores especificados",
|
||||
"general_settings": "Configuración general",
|
||||
"dns_settings": "Configuración DNS",
|
||||
"dns_settings": "Configuración del DNS",
|
||||
"dns_blocklists": "Listas de bloqueo DNS",
|
||||
"dns_allowlists": "Listas de permitido DNS",
|
||||
"dns_blocklists_desc": "AdGuard Home bloqueará los dominios que coincidan con las listas de bloqueo.",
|
||||
@@ -165,12 +165,12 @@
|
||||
"custom_filtering_rules": "Reglas de filtrado personalizado",
|
||||
"encryption_settings": "Configuración de cifrado",
|
||||
"dhcp_settings": "Configuración DHCP",
|
||||
"upstream_dns": "Proveedores DNS",
|
||||
"upstream_dns_help": "Ingresa una dirección de servidor por línea. <a>Más información</a> sobre la configuración de los proveedores DNS.",
|
||||
"upstream_dns": "Servidores DNS de subida",
|
||||
"upstream_dns_help": "Ingresa una dirección de servidor por línea. <a>Más información</a> sobre la configuración de los servidores DNS de subida.",
|
||||
"upstream_dns_configured_in_file": "Configurado en {{path}}",
|
||||
"test_upstream_btn": "Probar proveedores DNS",
|
||||
"upstreams": "Proveedores DNS",
|
||||
"upstream": "Proveedor DNS",
|
||||
"test_upstream_btn": "Probar DNS de subida",
|
||||
"upstreams": "DNS de subida",
|
||||
"upstream": "DNS de subida",
|
||||
"apply_btn": "Aplicar",
|
||||
"disabled_filtering_toast": "Filtrado deshabilitado",
|
||||
"enabled_filtering_toast": "Filtrado habilitado",
|
||||
@@ -233,11 +233,11 @@
|
||||
"example_upstream_tcp_port": "DNS regular (mediante TCP, con puerto).",
|
||||
"example_upstream_tcp_hostname": "DNS regular (mediante TCP, nombre del host).",
|
||||
"all_lists_up_to_date_toast": "Todas las listas ya están actualizadas",
|
||||
"updated_upstream_dns_toast": "Proveedores DNS guardados correctamente",
|
||||
"updated_upstream_dns_toast": "Servidores DNS de subida guardados correctamente",
|
||||
"dns_test_ok_toast": "Los servidores DNS especificados funcionan correctamente",
|
||||
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no se puede utilizar, por favor revisa si lo has escrito correctamente",
|
||||
"dns_test_parsing_error_toast": "No se pudo utilizar la sección {{section}}: línea {{line}}:, verifica si la escribiste correctamente",
|
||||
"dns_test_warning_toast": "Proveedor DNS \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
|
||||
"dns_test_warning_toast": "DNS de subida \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
|
||||
"unblock": "Desbloquear",
|
||||
"block": "Bloquear",
|
||||
"disallow_this_client": "No permitir a este cliente",
|
||||
@@ -294,9 +294,9 @@
|
||||
"blocked_response_ttl": "Respuesta TTL bloqueada",
|
||||
"blocked_response_ttl_desc": "Especifica durante cuántos segundos los clientes deben almacenar en cache una respuesta filtrada",
|
||||
"form_enter_blocked_response_ttl": "Ingresa el TTL de respuesta bloqueada (segundos)",
|
||||
"upstream_timeout": "Tiempo de espera del proveedor DNS",
|
||||
"upstream_timeout_desc": "Especifica el número de segundos que se debe esperar para recibir una respuesta del proveedor DNS",
|
||||
"form_enter_upstream_timeout": "Ingresa la duración de tiempo de espera del proveedor DNS en segundos",
|
||||
"upstream_timeout": "Tiempo de espera del upstream",
|
||||
"upstream_timeout_desc": "Especifica el número de segundos que se debe esperar para recibir una respuesta del servidor upstream",
|
||||
"form_enter_upstream_timeout": "Ingresa la duración del tiempo de espera del servidor DNS upstream en segundos",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS mediante HTTPS",
|
||||
"dns_over_tls": "DNS mediante TLS",
|
||||
@@ -311,7 +311,7 @@
|
||||
"form_enter_rate_limit": "Ingresa el límite de cantidad",
|
||||
"rate_limit": "Límite de cantidad",
|
||||
"edns_enable": "Habilitar subred de cliente EDNS",
|
||||
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del proveedor DNS y registra los valores enviados por los clientes en el registro de consultas.",
|
||||
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del DNS de subida y registra los valores enviados por los clientes en el registro de consultas.",
|
||||
"edns_use_custom_ip": "Usar IP personalizada para EDNS",
|
||||
"edns_use_custom_ip_desc": "Permitir el uso de IP personalizadas para EDNS",
|
||||
"rate_limit_desc": "Número de peticiones por segundo permitidas por cliente. Establecerlo en 0 significa que no hay límite.",
|
||||
@@ -335,7 +335,7 @@
|
||||
"theme_auto": "Auto",
|
||||
"theme_light": "Claro",
|
||||
"theme_dark": "Oscuro",
|
||||
"upstream_dns_client_desc": "Si se mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración DNS</0>.",
|
||||
"upstream_dns_client_desc": "Si se mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración del DNS</0>.",
|
||||
"tracker_source": "Fuente del rastreador",
|
||||
"source_label": "Fuente",
|
||||
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
|
||||
@@ -596,12 +596,12 @@
|
||||
"example_rewrite_wildcard": "reescribe las respuestas para todos los subdominios de <0>ejemplo.org</0>.",
|
||||
"rewrite_ip_address": "Dirección IP: utiliza esta IP en una respuesta A o AAAA",
|
||||
"rewrite_domain_name": "Nombre de dominio: añade un registro CNAME",
|
||||
"rewrite_A": "<0>A</0>: valor especial, mantiene registros <0>A</0> del proveedor DNS",
|
||||
"rewrite_AAAA": "<0>AAAA</0>: valor especial, mantiene registros <0>AAAA</0> del proveedor DNS",
|
||||
"rewrite_A": "<0>A</0>: valor especial, mantiene registros <0>A</0> del DNS de subida",
|
||||
"rewrite_AAAA": "<0>AAAA</0>: valor especial, mantiene registros <0>AAAA</0> del DNS de subida",
|
||||
"disable_ipv6": "Deshabilitar resolución de direcciones IPv6",
|
||||
"disable_ipv6_desc": "Descarta todas las consultas de DNS para direcciones IPv6 (tipo AAAA) y elimina las sugerencias de IPv6 de las respuestas HTTPS.",
|
||||
"fastest_addr": "Dirección IP más rápida",
|
||||
"fastest_addr_desc": "Espera respuestas de <b>todos</b> los servidores DNS, mide la velocidad de conexión TCP de cada servidor y devuelve la dirección IP del servidor con la velocidad de conexión más rápida.<br/>Este modo puede ralentizar significativamente las consultas DNS, si uno o más proveedores DNS no responden. Asegúrate de que tus proveedores DNS sean estables y de que el tiempo de espera tu proveedor DNS sea bajo.",
|
||||
"fastest_addr_desc": "Espera a que respondan <b>todos</b> los servidores DNS, mide la velocidad de conexión TCP de cada servidor y devuelve la Dirección IP del servidor con la velocidad de conexión más rápida.<br/>Este modo puede ralentizar significativamente las consultas DNS, si uno o más servidores DNS de upstream no están respondiendo. Asegúrate de que tus servidores DNS upstream sean estables y tu tiempo de espera de upstream sea bajo.",
|
||||
"autofix_warning_text": "Si haces clic en \"Corregir\", AdGuard Home configurará tu sistema para utilizar el servidor DNS de AdGuard Home.",
|
||||
"autofix_warning_list": "Realizará estas tareas: <0>Deshabilitar el sistema DNSStubListener</0> <0>Establecer la dirección del servidor DNS en 127.0.0.1</0> <0>Reemplazar el destino del enlace simbólico de /etc/resolv.conf por /run/systemd/resolve/resolv.conf</0> <0>Detener DNSStubListener (recargar el servicio systemd-resolved)</0>",
|
||||
"autofix_warning_result": "Como resultado, todas las peticiones DNS de tu sistema serán procesadas por AdGuard Home de manera predeterminada.",
|
||||
@@ -656,13 +656,13 @@
|
||||
"blocklist": "Lista de bloqueo",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Tamaño de la caché",
|
||||
"cache_size_desc": "Tamaño de la caché DNS (en bytes). Para desactivar el almacenamiento en caché, configúralo en 0.",
|
||||
"cache_size_desc": "Tamaño de la caché DNS (en bytes). Para deshabilitar el almacenamiento en caché, déjalo vacío.",
|
||||
"cache_ttl_min_override": "Anular TTL mínimo",
|
||||
"cache_ttl_max_override": "Anular TTL máximo",
|
||||
"enter_cache_size": "Ingresa el tamaño de la caché (bytes)",
|
||||
"enter_cache_ttl_min_override": "Ingresa el TTL mínimo (en segundos)",
|
||||
"enter_cache_ttl_max_override": "Ingresa el TTL máximo (en segundos)",
|
||||
"cache_ttl_min_override_desc": "Amplía el corto tiempo de vida (segundos) de los valores recibidos del proveedor DNS al almacenar en caché las respuestas DNS.",
|
||||
"cache_ttl_min_override_desc": "Amplía el corto tiempo de vida (segundos) de los valores recibidos del servidor DNS de subida al almacenar en caché las respuestas DNS.",
|
||||
"cache_ttl_max_override_desc": "Establece un valor de tiempo de vida (segundos) máximo para las entradas en la caché DNS.",
|
||||
"ttl_cache_validation": "La anulación TTL mínimo de la caché debe ser menor o igual al máximo",
|
||||
"cache_optimistic": "Caché optimista",
|
||||
@@ -748,7 +748,7 @@
|
||||
"thursday_short": "Jue.",
|
||||
"friday_short": "Vie.",
|
||||
"saturday_short": "Sáb.",
|
||||
"upstream_dns_cache_configuration": "Configuración de la caché del proveedor DNS",
|
||||
"enable_upstream_dns_cache": "Habilitar el almacenamiento en caché del DNS para la configuración personalizada de este cliente",
|
||||
"upstream_dns_cache_configuration": "Configuración de la caché DNS upstream",
|
||||
"enable_upstream_dns_cache": "Habilitar el almacenamiento en caché de DNS para la configuración personalizada de este cliente",
|
||||
"dns_cache_size": "Tamaño de la caché DNS, en bytes"
|
||||
}
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Liste de blocage",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Taille du cache",
|
||||
"cache_size_desc": "Taille du cache DNS (en octets). Pour désactiver la mise en cache, mettez la valeur sur 0.",
|
||||
"cache_size_desc": "Taille du cache DNS (en octets). Pour désactiver la mise en cache, laissez vide.",
|
||||
"cache_ttl_min_override": "Remplacer le TTL minimum",
|
||||
"cache_ttl_max_override": "Remplacer le TTL maximum",
|
||||
"enter_cache_size": "Entrer la taille du cache (octets)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Lista nera",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Dimensioni cache",
|
||||
"cache_size_desc": "Dimensione della cache DNS (in byte). Per disabilitare la cache, impostare su 0.",
|
||||
"cache_size_desc": "Dimensione della cache DNS (in byte). Per disabilitare la memorizzazione nella cache, lascia vuoto.",
|
||||
"cache_ttl_min_override": "Sovrascrivi TTL minimo",
|
||||
"cache_ttl_max_override": "Sovrascrivi TTL massimo",
|
||||
"enter_cache_size": "Immetti dimensioni cache (in byte)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "ブロックリスト",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "キャッシュサイズ",
|
||||
"cache_size_desc": "DNSキャッシュサイズ(バイト単位)※キャッシュを無効化するには、「0」(ゼロ)にしてください。",
|
||||
"cache_size_desc": "DNSキャッシュサイズ(バイト単位)。※キャッシュを無効化するには、この欄を空してください。",
|
||||
"cache_ttl_min_override": "最小TTLの上書き(秒単位)",
|
||||
"cache_ttl_max_override": "最大TTLの上書き(秒単位)",
|
||||
"enter_cache_size": "キャッシュサイズ(バイト単位)を入力してください",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "차단 목록",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "캐시 크기",
|
||||
"cache_size_desc": "DNS 캐시 크기(바이트). 캐싱을 사용하지 않으려면 0으로 설정합니다.",
|
||||
"cache_size_desc": "DNS 캐시 크기(바이트). 캐싱을 비활성화하려면 비워 둡니다.",
|
||||
"cache_ttl_min_override": "최소 TTL (초) 무시",
|
||||
"cache_ttl_max_override": "최대 TTL (초) 무시",
|
||||
"enter_cache_size": "캐시 크기를 입력하세요",
|
||||
|
||||
@@ -110,9 +110,9 @@
|
||||
"homepage": "Startpagina",
|
||||
"report_an_issue": "Rapporteer een probleem",
|
||||
"privacy_policy": "Privacybeleid",
|
||||
"enable_protection": "Bescherming inschakelen",
|
||||
"enable_protection": "Schakel bescherming in",
|
||||
"enabled_protection": "Bescherming ingeschakeld",
|
||||
"disable_protection": "Bescherming uitschakelen",
|
||||
"disable_protection": "Schakel bescherming uit",
|
||||
"disabled_protection": "Bescherming uitgeschakeld",
|
||||
"refresh_statics": "Ververs statistieken",
|
||||
"dns_query": "DNS-queries",
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Blokkeerlijst",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Cache grootte",
|
||||
"cache_size_desc": "DNS-cachegrootte (in bytes). Om caching uit te schakelen, stel deze in op 0.",
|
||||
"cache_size_desc": "DNS-cachegrootte (in bytes). Leeg laten om caching uit te schakelen.",
|
||||
"cache_ttl_min_override": "Minimale TTL overschrijven",
|
||||
"cache_ttl_max_override": "Maximale TTL overschrijven",
|
||||
"enter_cache_size": "Cache grootte invoeren (bytes)",
|
||||
@@ -702,13 +702,13 @@
|
||||
"disable_for_hours": "Voor {{count}} uur",
|
||||
"disable_for_hours_plural": "Voor {{count}} uren",
|
||||
"disable_until_tomorrow": "Tot morgen",
|
||||
"disable_notify_for_seconds": "Bescherming uitschakelen voor {{count}} seconde",
|
||||
"disable_notify_for_seconds_plural": "Bescherming uitschakelen voor {{count}} seconden",
|
||||
"disable_notify_for_minutes": "Bescherming uitschakelen voor {{count}} minuut",
|
||||
"disable_notify_for_minutes_plural": "Bescherming uitschakelen voor {{count}} minuten",
|
||||
"disable_notify_for_hours": "Bescherming uitschakelen voor {{count}} uur",
|
||||
"disable_notify_for_hours_plural": "Bescherming uitschakelen voor {{count}} uren",
|
||||
"disable_notify_until_tomorrow": "Bescherming uitschakelen tot morgen",
|
||||
"disable_notify_for_seconds": "Beveiliging uitschakelen voor {{count}} seconde",
|
||||
"disable_notify_for_seconds_plural": "Beveiliging uitschakelen voor {{count}} seconden",
|
||||
"disable_notify_for_minutes": "Beveiliging uitschakelen voor {{count}} minuut",
|
||||
"disable_notify_for_minutes_plural": "Beveiliging uitschakelen voor {{count}} minuten",
|
||||
"disable_notify_for_hours": "Beveiliging uitschakelen voor {{count}} uur",
|
||||
"disable_notify_for_hours_plural": "Beveiliging uitschakelen voor {{count}} uren",
|
||||
"disable_notify_until_tomorrow": "Beveiliging uitschakelen tot morgen",
|
||||
"enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}",
|
||||
"custom_retention_input": "Voer retentie in uren in",
|
||||
"custom_rotation_input": "Voer rotatie in uren in",
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"stats_malware_phishing": "Blokkert skadevare/phishing",
|
||||
"stats_adult": "Blokkerte voksennettsteder",
|
||||
"stats_query_domain": "Mest forespurte domener",
|
||||
"for_last_24_hours": "de siste 24 timene",
|
||||
"for_last_days": "for den siste {{count}} dagen",
|
||||
"for_last_days_plural": "de siste {{count}} dagene",
|
||||
"stats_disabled": "Statistikkene har blitt skrudd av. Du kan skru den på fra <0>innstillingssiden</0>.",
|
||||
@@ -120,6 +121,7 @@
|
||||
"no_upstreams_data_found": "Ingen oppstrøms servere data funnet",
|
||||
"number_of_dns_query_days": "Antall DNS-spørringer behandlet for de siste {{count}} dagene",
|
||||
"number_of_dns_query_days_plural": "Antall DNS-forespørsler som ble behandlet de siste {{count}} dagene",
|
||||
"number_of_dns_query_24_hours": "Antall DNS-forespørsler som ble behandlet de siste 24 timene",
|
||||
"number_of_dns_query_blocked_24_hours": "Antall DNS-forespørsler som ble blokkert av adblock-filtre, hosts-lister, og domene-lister",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Antall DNS-forespørsler som ble blokkert av AdGuard sin nettlesersikkerhetsmodul",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Antall voksennettsteder som ble blokkert",
|
||||
@@ -264,7 +266,6 @@
|
||||
"custom_ip": "Tilpasset IP",
|
||||
"blocking_ipv4": "IPv4-blokkering",
|
||||
"blocking_ipv6": "IPv6-blokkering",
|
||||
"blocked_response_ttl": "Blokkerte svars TTL",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
@@ -626,6 +627,7 @@
|
||||
"use_saved_key": "Bruk den tidligere lagrede nøkkelen",
|
||||
"parental_control": "Foreldrekontroll",
|
||||
"safe_browsing": "Sikker surfing",
|
||||
"served_from_cache": "{{value}} <i>(formidlet fra mellomlageret)</i>",
|
||||
"theme_dark_desc": "Mørkt tema",
|
||||
"theme_light_desc": "Lyst tema",
|
||||
"disable_notify_until_tomorrow": "Deaktiver beskyttelsen til i morgen",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Lista de bloqueio",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Tamanho do cache",
|
||||
"cache_size_desc": "Tamanho do cache do DNS (em bytes). Para desativar o cache, defina como 0.",
|
||||
"cache_size_desc": "Tamanho do cache do DNS (em bytes). Para desativar o cache, deixe em branco.",
|
||||
"cache_ttl_min_override": "Sobrepor o TTL mínimo",
|
||||
"cache_ttl_max_override": "Sobrepor o TTL máximo",
|
||||
"enter_cache_size": "Digite o tamanho do cache (bytes)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Lista de bloqueio",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Tamanho do cache",
|
||||
"cache_size_desc": "Tamanho do cache DNS (em bytes). Para desativar o cache, defina como 0.",
|
||||
"cache_size_desc": "Tamanho do cache DNS (em bytes). Para desativar o cache, deixar o campo vazio.",
|
||||
"cache_ttl_min_override": "Sobrepor o TTL mínimo",
|
||||
"cache_ttl_max_override": "Sobrepor o TTL máximo",
|
||||
"enter_cache_size": "Digite o tamanho do cache (bytes)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Чёрный список",
|
||||
"milliseconds_abbreviation": "мс",
|
||||
"cache_size": "Размер кеша",
|
||||
"cache_size_desc": "Размер кеша DNS (в байтах). Чтобы отключить кеширование, установите значение 0.",
|
||||
"cache_size_desc": "Размера кеша DNS (в байтах). Чтобы отключить кэширование, оставьте поле пустым.",
|
||||
"cache_ttl_min_override": "Переопределить минимальный TTL",
|
||||
"cache_ttl_max_override": "Переопределить максимальный TTL",
|
||||
"enter_cache_size": "Введите размер кеша (в байтах)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Zoznam blokovaní",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Veľkosť cache",
|
||||
"cache_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete vypnúť ukladanie do vyrovnávacej pamäte, nastavte hodnotu 0.",
|
||||
"cache_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete zakázať ukladanie do vyrovnávacej pamäte, ponechajte pole prázdne.",
|
||||
"cache_ttl_min_override": "Prepísať minimálne TTL",
|
||||
"cache_ttl_max_override": "Prepísať maximálne TTL",
|
||||
"enter_cache_size": "Zadať veľkosť cache (v bajtoch)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "Engel listesi",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Önbellek boyutu",
|
||||
"cache_size_desc": "DNS önbellek boyutu (bayt cinsinden). Önbelleği devre dışı bırakmak için 0 olarak ayarlayın.",
|
||||
"cache_size_desc": "DNS önbellek boyutu (bayt cinsinden). Önbelleğe almayı devre dışı bırakmak için boş bırakın.",
|
||||
"cache_ttl_min_override": "Minimum kullanım süresini geçersiz kıl",
|
||||
"cache_ttl_max_override": "Maksimum kullanım süresini geçersiz kıl",
|
||||
"enter_cache_size": "Önbellek boyutunu girin (bayt)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "黑名单",
|
||||
"milliseconds_abbreviation": "毫秒",
|
||||
"cache_size": "缓存大小",
|
||||
"cache_size_desc": "DNS 缓存大小(单位:字节)。若要禁用缓存,请设置为 0。",
|
||||
"cache_size_desc": "DNS 缓存大小(单位:字节)。若要关闭缓存,请留空。",
|
||||
"cache_ttl_min_override": "覆盖最小 TTL 值",
|
||||
"cache_ttl_max_override": "覆盖最大 TTL 值",
|
||||
"enter_cache_size": "输入缓存大小(字节)",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"client_settings": "用戶端設定",
|
||||
"example_upstream_reserved": "<0>特定網域</0>的上游;",
|
||||
"example_upstream_reserved": "<0>供特定的網域</0>之上游;",
|
||||
"example_multiple_upstreams_reserved": "<0>特定網域</0>的多個上游伺服器;",
|
||||
"example_upstream_comment": "註解。",
|
||||
"upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析。",
|
||||
@@ -20,7 +20,7 @@
|
||||
"resolve_clients_title": "啟用用戶端的 IP 位址之反向的解析",
|
||||
"resolve_clients_desc": "透過傳送指標(PTR)查詢到對應的解析器(私人 DNS 伺服器供區域的用戶端,上游的伺服器供有公共 IP 位址的用戶端),反向地解析用戶端的 IP 位址變為它們的主機名稱。",
|
||||
"use_private_ptr_resolvers_title": "使用私人反向的 DNS 解析器",
|
||||
"use_private_ptr_resolvers_desc": "透過私有上游伺服器、DHCP 或 /etc/hosts 等管道,解析含有私有 IP 位址的 ARPA 網域的 PTR、SOA 與 NS 請求。若停用此功能,AdGuard Home 將以 NXDOMAIN 回應所有相關請求。",
|
||||
"use_private_ptr_resolvers_desc": "使用私人上游伺服器、DHCP、/etc/hosts 等方式解析包含私人 IP 位址的 ARPA 網域的 PTR、SOA 和 NS 請求。如果停用,AdGuard Home 將對所有此類請求以 NXDOMAIN 回應。",
|
||||
"check_dhcp_servers": "檢查動態主機設定協定(DHCP)伺服器",
|
||||
"save_config": "儲存配置",
|
||||
"enabled_dhcp": "動態主機設定協定(DHCP)伺服器被啟用",
|
||||
@@ -112,8 +112,8 @@
|
||||
"privacy_policy": "隱私政策",
|
||||
"enable_protection": "啟用防護",
|
||||
"enabled_protection": "已啟用防護",
|
||||
"disable_protection": "停用防護",
|
||||
"disabled_protection": "已停用防護",
|
||||
"disable_protection": "禁用防護",
|
||||
"disabled_protection": "已禁用防護",
|
||||
"refresh_statics": "重新整理統計資料",
|
||||
"dns_query": "DNS 查詢",
|
||||
"blocked_by": "<0>被過濾器封鎖</0>",
|
||||
@@ -124,8 +124,8 @@
|
||||
"for_last_hours_plural": "在過去的 {{count}} 小時內",
|
||||
"for_last_days": "在最近的 {{count}} 日內",
|
||||
"for_last_days_plural": "在最近的 {{count}} 日內",
|
||||
"stats_disabled": "統計功能目前停用中,請至<0>設定頁面</0>重新開啟。",
|
||||
"stats_disabled_short": "該統計資料已停用",
|
||||
"stats_disabled": "該統計資料已被禁用。您可從<0>設定頁面</0>中打開它。",
|
||||
"stats_disabled_short": "該統計資料已被禁用",
|
||||
"no_domains_found": "無已發現之網域",
|
||||
"requests_count": "請求總數",
|
||||
"top_blocked_domains": "熱門已封鎖的網域",
|
||||
@@ -172,13 +172,13 @@
|
||||
"upstreams": "上游",
|
||||
"upstream": "上游伺服器",
|
||||
"apply_btn": "套用",
|
||||
"disabled_filtering_toast": "已停用過濾",
|
||||
"disabled_filtering_toast": "已禁用過濾",
|
||||
"enabled_filtering_toast": "已啟用過濾",
|
||||
"disabled_safe_browsing_toast": "已停用安全瀏覽",
|
||||
"disabled_safe_browsing_toast": "已禁用安全瀏覽",
|
||||
"enabled_safe_browsing_toast": "已啟用安全瀏覽",
|
||||
"disabled_parental_toast": "已停用家長控制",
|
||||
"disabled_parental_toast": "已禁用家長控制",
|
||||
"enabled_parental_toast": "已啟用家長控制",
|
||||
"disabled_safe_search_toast": "已停用安全搜尋",
|
||||
"disabled_safe_search_toast": "已禁用安全搜尋",
|
||||
"enabled_save_search_toast": "已啟用安全搜尋",
|
||||
"updated_save_search_toast": "安全搜尋設定更新成功",
|
||||
"enabled_table_header": "已啟用",
|
||||
@@ -275,7 +275,7 @@
|
||||
"query_log_retention": "查詢記錄保留時間",
|
||||
"query_log_enable": "啟用記錄",
|
||||
"query_log_configuration": "記錄配置",
|
||||
"query_log_disabled": "查詢記錄功能已停用,請至「<0>設定</0>」調整",
|
||||
"query_log_disabled": "查詢記錄被禁用並可在<0>設定</0>中被配置",
|
||||
"query_log_strict_search": "使用雙引號於嚴謹的搜尋",
|
||||
"query_log_retention_confirm": "您確定要更改記錄檔保存期限嗎?如果您縮短期限部分資料可能將會遺失",
|
||||
"anonymize_client_ip": "將用戶端 IP 匿名",
|
||||
@@ -401,7 +401,7 @@
|
||||
"encryption_config_saved": "加密配置被儲存",
|
||||
"encryption_server": "伺服器名稱",
|
||||
"encryption_server_enter": "輸入您的域名",
|
||||
"encryption_server_desc": "如果設定,AdGuard Home 會偵測 ClientID、回應 DDR 查詢,並執行其他連線驗證。如果未設定,則會停用這些功能。必須符合憑證中的一個 DNS 名稱。",
|
||||
"encryption_server_desc": "如果被設定,AdGuard Home 檢測用戶端 IDs,回覆 DDR 查詢,並執行額外的連線驗證。如果未被設定,這些功能被禁用。必須與在該憑證裡的 DNS 名稱其中之一相符。",
|
||||
"encryption_redirect": "自動地重新導向到 HTTPS",
|
||||
"encryption_redirect_desc": "如果被勾選,AdGuard Home 將自動地重新導向您從 HTTP 到 HTTPS 位址。",
|
||||
"encryption_https": "HTTPS 連接埠",
|
||||
@@ -429,8 +429,8 @@
|
||||
"encryption_reset": "您確定您想要重置加密設定嗎?",
|
||||
"encryption_warning": "警告",
|
||||
"encryption_plain_dns_enable": "啟用一般的 DNS",
|
||||
"encryption_plain_dns_desc": "預設啟用一般 DNS。您可以停用它以強制所有裝置使用加密 DNS。若要這樣做,您必須啟用至少一個加密 DNS 通訊協定",
|
||||
"encryption_plain_dns_error": "若要停用一般 DNS,請啟用至少一個加密 DNS 通訊協定",
|
||||
"encryption_plain_dns_desc": "預設情況下啟用一般的 DNS。使用者可以禁用它,強制所有裝置使用一般的 DNS。為此,必須至少啟用一個一般的 DNS 協定。",
|
||||
"encryption_plain_dns_error": "要禁用一般的 DNS,請至少啟用一個一般的 DNS 協定",
|
||||
"topline_expiring_certificate": "您的安全通訊端層(SSL)憑證即將到期。更新<0>加密設定</0>。",
|
||||
"topline_expired_certificate": "您的安全通訊端層(SSL)憑證為已到期的。更新<0>加密設定</0>。",
|
||||
"form_error_port_range": "輸入在 80-65535 之範圍內的連接埠號碼",
|
||||
@@ -572,7 +572,7 @@
|
||||
"filters_configuration": "過濾器配置",
|
||||
"filters_enable": "啟用過濾器",
|
||||
"filters_interval": "過濾器更新間隔",
|
||||
"disabled": "已停用",
|
||||
"disabled": "已禁用",
|
||||
"username_label": "使用者名稱",
|
||||
"username_placeholder": "輸入使用者名稱",
|
||||
"password_label": "密碼",
|
||||
@@ -598,7 +598,7 @@
|
||||
"rewrite_domain_name": "域名:新增一筆正規名稱(CNAME)記錄",
|
||||
"rewrite_A": "<0>A</0>:特殊的數值,阻止 <0>A</0> 記錄免於該上游",
|
||||
"rewrite_AAAA": "<0>AAAA</0>:特殊的數值,阻止 <0>AAAA</0> 記錄免於該上游",
|
||||
"disable_ipv6": "停用 IPv6 位址解析",
|
||||
"disable_ipv6": "禁用 IPv6 位址之解析",
|
||||
"disable_ipv6_desc": "停止所有對於 IPv6 位址(類型 AAAA)的 DNS 查詢,並從 HTTPS 回應中移除 IPv6 的提示。",
|
||||
"fastest_addr": "最快的 IP 位址",
|
||||
"fastest_addr_desc": "等待<b>所有</b> DNS 伺服器的回應,測量每個伺服器的 TCP 連線速度,並返回連線速度最快的伺服器的 IP 位址。<br/>如果一個或多個上游伺服器沒有回應,此模式會顯著減慢 DNS 查詢速度。確保您的上游伺服器穩定且上游超時時間短。",
|
||||
@@ -656,7 +656,7 @@
|
||||
"blocklist": "封鎖清單",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "快取大小",
|
||||
"cache_size_desc": "DNS 快取大小(位元組)。若要停用快取,請設為 0。",
|
||||
"cache_size_desc": "DNS 快取大小(以位元組)。要禁用快取,留空。",
|
||||
"cache_ttl_min_override": "覆寫最小的存活時間(TTL)",
|
||||
"cache_ttl_max_override": "覆寫最大的存活時間(TTL)",
|
||||
"enter_cache_size": "輸入快取大小(位元組)",
|
||||
@@ -681,13 +681,13 @@
|
||||
"port_53_faq_link": "連接埠 53 常被 \"DNSStubListener\" 或 \"systemd-resolved\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明</0>。",
|
||||
"adg_will_drop_dns_queries": "AdGuard Home 將持續排除來自此用戶端之所有的 DNS 查詢。",
|
||||
"filter_allowlist": "警告:此動作也將把 \"{{disallowed_rule}}\" 規則排除在被允許的用戶端的清單之外。",
|
||||
"last_rule_in_allowlist": "無法禁止此用戶端,因為排除規則 \"{{disallowed_rule}}\" 會停用「允許的用戶端」清單。",
|
||||
"last_rule_in_allowlist": "因為排除 \"{{disallowed_rule}}\" 規則將禁用\"被允許的用戶端\"清單,無法不允許此用戶端。",
|
||||
"use_saved_key": "使用該先前已儲存的金鑰",
|
||||
"parental_control": "家長控制",
|
||||
"safe_browsing": "安全瀏覽",
|
||||
"served_from_cache_label": "從快取中",
|
||||
"form_error_password_length": "密碼長度必須為 {{min}} 到 {{max}} 個字符",
|
||||
"anonymizer_notification": "<0>注意:</0>IP 匿名功能已開啟。您可在<1>一般設定</1>中關閉。",
|
||||
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。",
|
||||
"confirm_dns_cache_clear": "您確定您想要清除 DNS 快取嗎?",
|
||||
"cache_cleared": "DNS 快取被成功地清除",
|
||||
"clear_cache": "清除快取",
|
||||
@@ -702,14 +702,14 @@
|
||||
"disable_for_hours": "{{count}} 小時",
|
||||
"disable_for_hours_plural": "{{count}} 小時",
|
||||
"disable_until_tomorrow": "直到明天",
|
||||
"disable_notify_for_seconds": "計 {{count}} 秒停用防護",
|
||||
"disable_notify_for_seconds_plural": "計 {{count}} 秒停用防護",
|
||||
"disable_notify_for_minutes": "計 {{count}} 分鐘停用防護",
|
||||
"disable_notify_for_minutes_plural": "計 {{count}} 分鐘停用防護",
|
||||
"disable_notify_for_hours": "計 {{count}} 小時停用防護",
|
||||
"disable_notify_for_hours_plural": "計 {{count}} 小時停用防護",
|
||||
"disable_notify_until_tomorrow": "停用防護直到明天",
|
||||
"enable_protection_timer": "防護將於 {{time}} 啟用",
|
||||
"disable_notify_for_seconds": "計 {{count}} 秒禁用防護",
|
||||
"disable_notify_for_seconds_plural": "計 {{count}} 秒禁用防護",
|
||||
"disable_notify_for_minutes": "計 {{count}} 分鐘禁用防護",
|
||||
"disable_notify_for_minutes_plural": "計 {{count}} 分鐘禁用防護",
|
||||
"disable_notify_for_hours": "計 {{count}} 小時禁用防護",
|
||||
"disable_notify_for_hours_plural": "計 {{count}} 小時禁用防護",
|
||||
"disable_notify_until_tomorrow": "禁用防護直到明天",
|
||||
"enable_protection_timer": "防護將於 {{time}} 被啟用",
|
||||
"custom_retention_input": "輸入保留時間(小時)",
|
||||
"custom_rotation_input": "輸入旋轉時間(小時)",
|
||||
"protection_section_label": "防護",
|
||||
|
||||
@@ -78,7 +78,6 @@ class CustomRules extends Component<CustomRulesProps> {
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="text-edit-container mb-4">
|
||||
<textarea
|
||||
data-testid="custom_rule_textarea"
|
||||
className="form-control font-monospace text-input"
|
||||
value={userRules}
|
||||
onChange={this.handleChange}
|
||||
@@ -92,7 +91,6 @@ class CustomRules extends Component<CustomRulesProps> {
|
||||
|
||||
<div className="card-actions">
|
||||
<button
|
||||
data-testid="apply_custom_rule"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}>
|
||||
|
||||
@@ -59,7 +59,7 @@ const Header = () => {
|
||||
<div className="header__column">
|
||||
<div className="header__right">
|
||||
{!processingProfile && name && (
|
||||
<a href="control/logout" className="btn btn-sm btn-outline-secondary" data-testid="sign_out">
|
||||
<a href="control/logout" className="btn btn-sm btn-outline-secondary">
|
||||
{t('sign_out')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -288,7 +288,7 @@ const Row = memo(
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={style} className={className} onClick={onClick} role="row" data-testid="querylog_cell">
|
||||
<div style={style} className={className} onClick={onClick} role="row">
|
||||
<DateCell {...rowProps} />
|
||||
|
||||
<DomainCell {...rowProps} />
|
||||
|
||||
@@ -84,7 +84,6 @@ export const Form = ({ className, setIsLoading }: Props) => {
|
||||
}}>
|
||||
<div className="field__search">
|
||||
<SearchField
|
||||
data-testid="querylog_search"
|
||||
value={searchValue}
|
||||
handleChange={(val) => setValue('search', val)}
|
||||
onKeyDown={onEnterPress}
|
||||
|
||||
@@ -27,14 +27,12 @@ const SETTINGS = {
|
||||
enabled: false,
|
||||
title: i18next.t('use_adguard_browsing_sec'),
|
||||
subtitle: i18next.t('use_adguard_browsing_sec_hint'),
|
||||
testId: 'safebrowsing',
|
||||
[ORDER_KEY]: 0,
|
||||
},
|
||||
parental: {
|
||||
enabled: false,
|
||||
title: i18next.t('use_adguard_parental'),
|
||||
subtitle: i18next.t('use_adguard_parental_hint'),
|
||||
testId: 'parental',
|
||||
[ORDER_KEY]: 1,
|
||||
},
|
||||
};
|
||||
@@ -92,12 +90,11 @@ class Settings extends Component<SettingsProps> {
|
||||
renderSettings = (settings: any) =>
|
||||
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
|
||||
const setting = settings[key];
|
||||
const { enabled, title, subtitle, testId } = setting;
|
||||
const { enabled, title, subtitle } = setting;
|
||||
|
||||
return (
|
||||
<div key={key} className="form__group form__group--checkbox">
|
||||
<Checkbox
|
||||
data-testid={testId}
|
||||
value={enabled}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
@@ -121,7 +118,6 @@ class Settings extends Component<SettingsProps> {
|
||||
<>
|
||||
<div className="form__group form__group--checkbox">
|
||||
<Checkbox
|
||||
data-testid="safesearch"
|
||||
value={enabled}
|
||||
title={i18next.t('enforce_safe_search')}
|
||||
subtitle={i18next.t('enforce_save_search_hint')}
|
||||
|
||||
@@ -94,17 +94,14 @@ const Footer = () => {
|
||||
auto: {
|
||||
desc: t('theme_auto_desc'),
|
||||
icon: '#auto',
|
||||
testId: 'theme_auto',
|
||||
},
|
||||
dark: {
|
||||
desc: t('theme_dark_desc'),
|
||||
icon: '#dark',
|
||||
testId: 'theme_dark',
|
||||
},
|
||||
light: {
|
||||
desc: t('theme_light_desc'),
|
||||
icon: '#light',
|
||||
testId: 'theme_light',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,9 +113,7 @@ const Footer = () => {
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary footer__theme-button"
|
||||
onClick={() => onThemeChange(theme)}
|
||||
title={content[theme].desc}
|
||||
data-testid={content[theme].testId}
|
||||
>
|
||||
title={content[theme].desc}>
|
||||
<svg className={cn('footer__theme-icon', { 'footer__theme-icon--active': currentValue === theme })}>
|
||||
<use xlinkHref={content[theme].icon} />
|
||||
</svg>
|
||||
|
||||
@@ -28,12 +28,6 @@ export default {
|
||||
"homepage": "https://badmojr.github.io/1Hosts/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
|
||||
},
|
||||
"1hosts_pro": {
|
||||
"name": "1Hosts (Pro)",
|
||||
"categoryId": "general",
|
||||
"homepage": "https://badmojr.github.io/1Hosts/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_64.txt"
|
||||
},
|
||||
"CHN_adrules": {
|
||||
"name": "CHN: AdRules DNS List",
|
||||
"categoryId": "regional",
|
||||
|
||||
@@ -62,7 +62,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
||||
{...field}
|
||||
data-testid="password"
|
||||
type="password"
|
||||
label={t('password_label')}
|
||||
label={t('username_label')}
|
||||
placeholder={t('password_placeholder')}
|
||||
error={fieldState.error?.message}
|
||||
autoComplete="current-password"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
|
||||
|
||||
test.describe('Control Panel', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login.html');
|
||||
await page.getByTestId('username').click();
|
||||
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||
await page.getByTestId('password').click();
|
||||
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByTestId('sign_in').click();
|
||||
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
|
||||
});
|
||||
|
||||
test('should sign out successfully', async ({ page }) => {
|
||||
await page.getByTestId('sign_out').click();
|
||||
|
||||
await page.waitForURL((url) => url.href.endsWith('/login.html'));
|
||||
|
||||
await expect(page.getByTestId('sign_in')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should change theme to dark and then light', async ({ page }) => {
|
||||
await page.getByTestId('theme_dark').click();
|
||||
|
||||
await expect(page.locator('body[data-theme="dark"]')).toBeVisible();
|
||||
|
||||
|
||||
await page.getByTestId('theme_light').click();
|
||||
|
||||
await expect(page.locator('body:not([data-theme="dark"])')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
|
||||
|
||||
test.describe('DNS Settings', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Login before each test
|
||||
await page.goto('/login.html');
|
||||
await page.getByTestId('username').click();
|
||||
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||
await page.getByTestId('password').click();
|
||||
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByTestId('sign_in').click();
|
||||
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
|
||||
});
|
||||
|
||||
const runDNSSettingsTest = async (page: Page, address: string) => {
|
||||
await page.goto('/#dns');
|
||||
|
||||
const currentDns = await page.getByTestId('upstream_dns').inputValue();
|
||||
|
||||
await page.getByTestId('upstream_dns').fill(address);
|
||||
await page.getByTestId('dns_upstream_test').click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page.getByTestId('upstream_dns')).toHaveValue(address);
|
||||
|
||||
await page.getByTestId('upstream_dns').fill(currentDns);
|
||||
await page.getByTestId('dns_upstream_save').click({ force: true });
|
||||
};
|
||||
|
||||
test('test for Default DNS', async ({ page }) => {
|
||||
await runDNSSettingsTest(page, 'https://dns10.quad9.net/dns-query');
|
||||
});
|
||||
|
||||
test('test for Plain DNS', async ({ page }) => {
|
||||
await runDNSSettingsTest(page, '94.140.14.140');
|
||||
});
|
||||
|
||||
test('test for DNS-over-HTTPS', async ({ page }) => {
|
||||
await runDNSSettingsTest(page, 'https://unfiltered.adguard-dns.com/dns-query');
|
||||
});
|
||||
|
||||
test('test for DNS-over-TLS', async ({ page }) => {
|
||||
await runDNSSettingsTest(page, 'tls://unfiltered.adguard-dns.com');
|
||||
});
|
||||
|
||||
test('test for DNS-over-QUIC', async ({ page }) => {
|
||||
await runDNSSettingsTest(page, 'quic://unfiltered.adguard-dns.com');
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { execSync } from 'child_process';
|
||||
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
|
||||
|
||||
test.describe('Filtering', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Login before each test
|
||||
await page.goto('/login.html');
|
||||
await page.getByTestId('username').click();
|
||||
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||
await page.getByTestId('password').click();
|
||||
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByTestId('sign_in').click();
|
||||
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
|
||||
});
|
||||
|
||||
const runTerminalCommand = (command: string) => {
|
||||
try {
|
||||
console.info(`Executing command: ${command}`);
|
||||
|
||||
const output = execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
||||
|
||||
console.info('Command executed successfully.');
|
||||
console.debug(`Command output:\n${output}`);
|
||||
|
||||
return output;
|
||||
} catch (error: any) {
|
||||
console.error(`Command execution failed with error:\n${error.message}`);
|
||||
throw new Error(`Failed to execute command: ${command}\nError: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const runCustomRuleTest = async (page: Page, domain_to_block: string) => {
|
||||
await page.goto('/#custom_rules');
|
||||
|
||||
await page.getByTestId('custom_rule_textarea').fill(domain_to_block);
|
||||
await page.getByTestId('apply_custom_rule').click();
|
||||
|
||||
const nslookupBlockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
|
||||
|
||||
console.info(`nslookup blocked CNAME result: '${nslookupBlockedResult}'`);
|
||||
|
||||
const currentRules = await page.getByTestId('custom_rule_textarea').inputValue();
|
||||
console.debug(`Current rules before removal:\n${currentRules}`);
|
||||
|
||||
if (currentRules.includes(domain_to_block)) {
|
||||
const updatedRules = currentRules
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() !== domain_to_block.trim())
|
||||
.join('\n');
|
||||
|
||||
await page.getByTestId('custom_rule_textarea').fill(updatedRules);
|
||||
console.info(`Rule '${domain_to_block}' removed successfully.`);
|
||||
|
||||
console.info('Applying the updated filtering rules after removal.');
|
||||
await page.getByTestId('apply_custom_rule').click();
|
||||
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
console.info(`Filtering rules successfully updated after removing '${domain_to_block}'.`);
|
||||
} else {
|
||||
console.warn(`Rule '${domain_to_block}' not found. No changes were made.`);
|
||||
}
|
||||
|
||||
const nslookupUnblockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
|
||||
console.info(`nslookup unblocked CNAME result: '${nslookupUnblockedResult}'`);
|
||||
};
|
||||
|
||||
test('Test blocking rule for apple.com', async ({ page }) => {
|
||||
await runCustomRuleTest(page, 'apple.com');
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { execSync } from 'child_process';
|
||||
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
|
||||
|
||||
test.describe('General Settings', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login.html');
|
||||
await page.getByTestId('username').click();
|
||||
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||
await page.getByTestId('password').click();
|
||||
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByTestId('sign_in').click();
|
||||
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
|
||||
});
|
||||
|
||||
test('should toggle browsing security feature and verify DNS changes', async ({ page }) => {
|
||||
await page.goto('/#settings');
|
||||
|
||||
const browsingSecurity = await page.getByTestId('safebrowsing');
|
||||
const browsingSecurityLabel = await browsingSecurity.locator('xpath=following-sibling::*[1]');
|
||||
|
||||
const initialState = await browsingSecurity.isChecked();
|
||||
|
||||
if (!initialState) {
|
||||
await browsingSecurityLabel.click();
|
||||
await expect(browsingSecurity).toBeChecked();
|
||||
}
|
||||
|
||||
const resultEnabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
|
||||
|
||||
await browsingSecurityLabel.click();
|
||||
await expect(browsingSecurity).not.toBeChecked();
|
||||
|
||||
const resultDisabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
|
||||
|
||||
expect(resultEnabled).not.toEqual(resultDisabled);
|
||||
|
||||
if (initialState) {
|
||||
await browsingSecurityLabel.click();
|
||||
await expect(browsingSecurity).toBeChecked();
|
||||
}
|
||||
});
|
||||
|
||||
test('should toggle parental control feature and verify DNS changes', async ({ page }) => {
|
||||
await page.goto('/#settings');
|
||||
|
||||
const parentalControl = page.getByTestId('parental');
|
||||
const parentalControlLabel = await parentalControl.locator('xpath=following-sibling::*[1]');
|
||||
|
||||
const initialState = await parentalControl.isChecked();
|
||||
|
||||
if (!initialState) {
|
||||
await parentalControlLabel.click();
|
||||
await expect(parentalControl).toBeChecked();
|
||||
}
|
||||
|
||||
const resultEnabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
|
||||
|
||||
await parentalControlLabel.click();
|
||||
await expect(parentalControl).not.toBeChecked();
|
||||
|
||||
const resultDisabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
|
||||
|
||||
expect(resultEnabled).not.toEqual(resultDisabled);
|
||||
|
||||
if (initialState) {
|
||||
await parentalControlLabel.click();
|
||||
await expect(parentalControl).toBeChecked();
|
||||
}
|
||||
});
|
||||
|
||||
test('should toggle safe search feature', async ({ page }) => {
|
||||
await page.goto('/#settings');
|
||||
|
||||
const safeSearch = page.getByTestId('safesearch');
|
||||
const safeSearchLabel = await safeSearch.locator('xpath=following-sibling::*[1]');
|
||||
|
||||
const initialState = await safeSearch.isChecked();
|
||||
|
||||
await safeSearchLabel.click();
|
||||
|
||||
await expect(safeSearch).not.toBeChecked({ checked: initialState });
|
||||
|
||||
await safeSearchLabel.click();
|
||||
|
||||
await expect(safeSearch).toBeChecked({ checked: initialState });
|
||||
});
|
||||
});
|
||||
@@ -1,124 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
|
||||
|
||||
test.describe('QueryLog', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login.html');
|
||||
await page.getByTestId('username').click();
|
||||
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||
await page.getByTestId('password').click();
|
||||
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByTestId('sign_in').click();
|
||||
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
|
||||
});
|
||||
|
||||
test('Search of queryLog should work correctly', async ({ page }) => {
|
||||
await page.route('/control/querylog', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"answer": [
|
||||
{
|
||||
"type": "A",
|
||||
"value": "77.88.44.242",
|
||||
"ttl": 294
|
||||
},
|
||||
{
|
||||
"type": "A",
|
||||
"value": "5.255.255.242",
|
||||
"ttl": 294
|
||||
},
|
||||
{
|
||||
"type": "A",
|
||||
"value": "77.88.55.242",
|
||||
"ttl": 294
|
||||
}
|
||||
],
|
||||
"answer_dnssec": false,
|
||||
"cached": false,
|
||||
"client": "127.0.0.1",
|
||||
"client_info": {
|
||||
"whois": {},
|
||||
"name": "localhost",
|
||||
"disallowed_rule": "127.0.0.1",
|
||||
"disallowed": false
|
||||
},
|
||||
"client_proto": "",
|
||||
"elapsedMs": "78.163167",
|
||||
"question": {
|
||||
"class": "IN",
|
||||
"name": "ya.ru",
|
||||
"type": "A"
|
||||
},
|
||||
"reason": "NotFilteredNotFound",
|
||||
"rules": [],
|
||||
"status": "NOERROR",
|
||||
"time": "2024-07-17T16:02:37.500662+02:00",
|
||||
"upstream": "https://dns10.quad9.net:443/dns-query"
|
||||
},
|
||||
{
|
||||
"answer": [
|
||||
{
|
||||
"type": "A",
|
||||
"value": "77.88.55.242",
|
||||
"ttl": 351
|
||||
},
|
||||
{
|
||||
"type": "A",
|
||||
"value": "77.88.44.242",
|
||||
"ttl": 351
|
||||
},
|
||||
{
|
||||
"type": "A",
|
||||
"value": "5.255.255.242",
|
||||
"ttl": 351
|
||||
}
|
||||
],
|
||||
"answer_dnssec": false,
|
||||
"cached": false,
|
||||
"client": "127.0.0.1",
|
||||
"client_info": {
|
||||
"whois": {},
|
||||
"name": "localhost",
|
||||
"disallowed_rule": "127.0.0.1",
|
||||
"disallowed": false
|
||||
},
|
||||
"client_proto": "",
|
||||
"elapsedMs": "5051.070708",
|
||||
"question": {
|
||||
"class": "IN",
|
||||
"name": "ya.ru",
|
||||
"type": "A"
|
||||
},
|
||||
"reason": "NotFilteredNotFound",
|
||||
"rules": [],
|
||||
"status": "NOERROR",
|
||||
"time": "2024-07-17T16:02:37.4983+02:00",
|
||||
"upstream": "https://dns10.quad9.net:443/dns-query"
|
||||
}
|
||||
],
|
||||
"oldest": "2024-07-17T16:02:37.4983+02:00"
|
||||
}
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/#logs');
|
||||
|
||||
await page.getByTestId('querylog_search').fill('127.0.0.1');
|
||||
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest((req) => req.url().includes('/control/querylog')),
|
||||
]);
|
||||
|
||||
if (request) {
|
||||
expect(request.url()).toContain('search=127.0.0.1');
|
||||
expect(await page.getByTestId('querylog_cell').first().isVisible()).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
# A docker file for scripts/make/build-docker.sh.
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.18
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
ARG VCS_REF
|
||||
|
||||
LABEL \
|
||||
LABEL\
|
||||
maintainer="AdGuard Team <devteam@adguard.com>" \
|
||||
org.opencontainers.image.authors="AdGuard Team <devteam@adguard.com>" \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
@@ -30,8 +30,8 @@ ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
ARG TARGETVARIANT
|
||||
|
||||
COPY --chown=nobody:nogroup \
|
||||
./${DIST_DIR}/docker/AdGuardHome_${TARGETOS}_${TARGETARCH}_${TARGETVARIANT} \
|
||||
COPY --chown=nobody:nogroup\
|
||||
./${DIST_DIR}/docker/AdGuardHome_${TARGETOS}_${TARGETARCH}_${TARGETVARIANT}\
|
||||
/opt/adguardhome/AdGuardHome
|
||||
|
||||
RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
|
||||
@@ -45,15 +45,8 @@ RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
|
||||
# 3000 : TCP, UDP : HTTP(S) (alt, incl. HTTP/3)
|
||||
# 5443 : TCP, UDP : DNSCrypt (alt)
|
||||
# 6060 : TCP : HTTP (pprof)
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
73
go.mod
73
go.mod
@@ -1,17 +1,17 @@
|
||||
module github.com/AdguardTeam/AdGuardHome
|
||||
|
||||
go 1.24.2
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.75.5
|
||||
github.com/AdguardTeam/golibs v0.32.9
|
||||
github.com/AdguardTeam/dnsproxy v0.75.1
|
||||
github.com/AdguardTeam/golibs v0.32.5
|
||||
github.com/AdguardTeam/urlfilter v0.20.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/ameshkov/dnscrypt/v2 v2.4.0
|
||||
github.com/ameshkov/dnscrypt/v2 v2.3.0
|
||||
github.com/bluele/gcache v0.0.2
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
||||
github.com/digineo/go-ipset/v2 v2.2.1
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
// TODO(e.burkov): This package is deprecated; find a new one or use our
|
||||
// own code for that. Perhaps, use gopacket.
|
||||
github.com/go-ping/ping v1.2.0
|
||||
@@ -28,31 +28,33 @@ require (
|
||||
// TODO(a.garipov): This package is deprecated; find a new one or use our
|
||||
// own code for that. Perhaps, use gopacket.
|
||||
github.com/mdlayher/raw v0.1.0
|
||||
github.com/miekg/dns v1.1.65
|
||||
github.com/quic-go/quic-go v0.50.1
|
||||
github.com/miekg/dns v1.1.63
|
||||
github.com/quic-go/quic-go v0.49.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/ti-mo/netfilter v0.5.2
|
||||
go.etcd.io/bbolt v1.4.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
|
||||
golang.org/x/net v0.37.0
|
||||
golang.org/x/sys v0.31.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.120.1 // indirect
|
||||
cloud.google.com/go/ai v0.10.2 // indirect
|
||||
cloud.google.com/go/auth v0.16.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go v0.119.0 // indirect
|
||||
cloud.google.com/go/ai v0.10.1 // indirect
|
||||
cloud.google.com/go/auth v0.15.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.6 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
@@ -61,7 +63,7 @@ require (
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/golangci/misspell v0.6.0 // indirect
|
||||
github.com/google/generative-ai-go v0.19.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
@@ -70,7 +72,7 @@ require (
|
||||
github.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect
|
||||
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
@@ -78,7 +80,7 @@ require (
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/securego/gosec/v2 v2.22.3 // indirect
|
||||
github.com/securego/gosec/v2 v2.22.2 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/uudashr/gocognit v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
@@ -88,27 +90,26 @@ require (
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/vuln v1.1.4 // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
google.golang.org/api v0.229.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/grpc v1.71.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gonum.org/v1/gonum v0.15.1 // indirect
|
||||
google.golang.org/api v0.227.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
mvdan.cc/editorconfig v0.3.0 // indirect
|
||||
mvdan.cc/gofumpt v0.8.0 // indirect
|
||||
mvdan.cc/gofumpt v0.7.0 // indirect
|
||||
mvdan.cc/sh/v3 v3.11.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect
|
||||
)
|
||||
|
||||
148
go.sum
148
go.sum
@@ -1,27 +1,31 @@
|
||||
cloud.google.com/go v0.120.1 h1:Z+5V7yd383+9617XDCyszmK5E4wJRJL+tquMfDj9hLM=
|
||||
cloud.google.com/go v0.120.1/go.mod h1:56Vs7sf/i2jYM6ZL9NYlC82r04PThNcPS5YgFmb0rp8=
|
||||
cloud.google.com/go/ai v0.10.2 h1:5NHzmZlRs+3kvlsVdjT0cTnLrjQdROJ/8VOljVfs+8o=
|
||||
cloud.google.com/go/ai v0.10.2/go.mod h1:xZuZuE9d3RgsR132meCnPadiU9XV0qXjpLr+P4J46eE=
|
||||
cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
|
||||
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go v0.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE=
|
||||
cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08=
|
||||
cloud.google.com/go/ai v0.10.1 h1:EU93KqYmMeOKgaBXAz2DshH2C/BzAT1P+iJORksLIic=
|
||||
cloud.google.com/go/ai v0.10.1/go.mod h1:sWWHZvmJ83BjuxAQtYEiA0SFTpijtbH+SXWFO14ri5A=
|
||||
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
|
||||
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
github.com/AdguardTeam/dnsproxy v0.75.5 h1:/P7+Ku4bjl+sVC/FW3PbT7pabgCjKTcrAOHqsZe2e60=
|
||||
github.com/AdguardTeam/dnsproxy v0.75.5/go.mod h1:fdwtHhrDkTueDagDCasYKZbXdppkkBXW7RGPBNH+pis=
|
||||
github.com/AdguardTeam/golibs v0.32.9 h1:/6luT0aMOn05/s9eh1yA4lbcHgl0d1iEEvEBbIMMUk0=
|
||||
github.com/AdguardTeam/golibs v0.32.9/go.mod h1:McV1QFFlKLElKa306V4OL/T2kr7564PhsayfvTWYBVs=
|
||||
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
|
||||
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
|
||||
github.com/AdguardTeam/dnsproxy v0.75.1 h1:ux2sQfF/9+WRo6a32g9NtfaAPU19gJhqkEu2OZflxJg=
|
||||
github.com/AdguardTeam/dnsproxy v0.75.1/go.mod h1:HKBI/IO2/ACOjfTV6qIzB5ZDDxfjgHHvQ3hIbGg9wvc=
|
||||
github.com/AdguardTeam/golibs v0.32.5 h1:4Rkv2xBnyJe6l/EM2MFgoY1S4pweYwDgLTYg2MDArEA=
|
||||
github.com/AdguardTeam/golibs v0.32.5/go.mod h1:agsvz8Iyv0uV9NU56hpCoFLAtSPkiBf9nPVhDvdUIb0=
|
||||
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=
|
||||
github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=
|
||||
github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
|
||||
github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP2zNLs=
|
||||
github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE=
|
||||
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
|
||||
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
|
||||
@@ -30,8 +34,8 @@ github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -39,8 +43,8 @@ github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1M
|
||||
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -72,8 +76,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg=
|
||||
github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
|
||||
@@ -124,12 +128,12 @@ github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5
|
||||
github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
@@ -141,18 +145,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
|
||||
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
|
||||
github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94=
|
||||
github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/securego/gosec/v2 v2.22.3 h1:mRrCNmRF2NgZp4RJ8oJ6yPJ7G4x6OCiAXHd8x4trLRc=
|
||||
github.com/securego/gosec/v2 v2.22.3/go.mod h1:42M9Xs0v1WseinaB/BmNGO8AVqG8vRfhC2686ACY48k=
|
||||
github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g=
|
||||
github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
@@ -197,18 +199,16 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0 h1:oMe07YcizemJ09rs2kRkFYAp0pt4e1lYLwPWiEGMpXE=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 h1:VI4qDpTkfFaCXEPrbojidLgVQhj2x4nzTccG0hjaLlU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
|
||||
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=
|
||||
@@ -221,14 +221,14 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -241,43 +241,43 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 h1:RXY2+rSHXvxO2Y+gKrPjYVaEoGOqh3VEXFhnWAt1Irg=
|
||||
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3/go.mod h1:RoaXAWDwS90j6FxVKwJdBV+0HCU+llrKUGgJaxiKl6M=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 h1:UY+gQAskx5vohcvUlJDKkJPt9lALCgtZs3rs8msRatU=
|
||||
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314/go.mod h1:16eI1RtbPZAEm3u7hpIh7JM/w5AbmlDtnrdKYaREic8=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
|
||||
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
|
||||
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
|
||||
gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
|
||||
google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc=
|
||||
google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -292,8 +292,8 @@ howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=
|
||||
mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=
|
||||
mvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k=
|
||||
mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg=
|
||||
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
|
||||
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
|
||||
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
|
||||
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
|
||||
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8=
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package aghuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Login is the type for web user logins.
|
||||
type Login string
|
||||
|
||||
// NewLogin returns a web user login. The length of s must not be greater than
|
||||
// [math.MaxUint16].
|
||||
//
|
||||
// TODO(s.chzhen): Add more constraints as needed.
|
||||
func NewLogin(s string) (l Login, err error) {
|
||||
if s == "" {
|
||||
return "", errors.ErrEmptyValue
|
||||
}
|
||||
|
||||
return Login(s), nil
|
||||
}
|
||||
|
||||
// Password is an interface that defines methods for handling web user
|
||||
// passwords.
|
||||
type Password interface {
|
||||
// Authenticate returns true if the provided password is allowed.
|
||||
Authenticate(ctx context.Context, password string) (ok bool)
|
||||
|
||||
// Hash returns a hashed representation of the web user password.
|
||||
Hash() (b []byte)
|
||||
}
|
||||
|
||||
// DefaultPassword is the default bcrypt implementation of the [Password]
|
||||
// interface.
|
||||
type DefaultPassword struct {
|
||||
hash []byte
|
||||
}
|
||||
|
||||
// NewDefaultPassword returns the new properly initialized *DefaultPassword.
|
||||
func NewDefaultPassword(hash string) (p *DefaultPassword) {
|
||||
return &DefaultPassword{
|
||||
hash: []byte(hash),
|
||||
}
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Password = (*DefaultPassword)(nil)
|
||||
|
||||
// Authenticate implements the [Password] interface for *DefaultPassword.
|
||||
func (p *DefaultPassword) Authenticate(ctx context.Context, passwd string) (ok bool) {
|
||||
return bcrypt.CompareHashAndPassword([]byte(p.hash), []byte(passwd)) == nil
|
||||
}
|
||||
|
||||
// Hash implements the [Password] interface for *DefaultPassword.
|
||||
func (p *DefaultPassword) Hash() (b []byte) {
|
||||
return p.hash
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package aghuser_test
|
||||
|
||||
import "time"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
@@ -1,149 +0,0 @@
|
||||
package aghuser
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// DB is an interface that defines methods for interacting with user
|
||||
// information. All methods must be safe for concurrent use.
|
||||
//
|
||||
// TODO(s.chzhen): Use this.
|
||||
//
|
||||
// TODO(s.chzhen): Consider updating methods to return a clone.
|
||||
type DB interface {
|
||||
// All retrieves all users from the database, sorted by login.
|
||||
//
|
||||
// TODO(s.chzhen): Consider function signature change to reflect the
|
||||
// in-memory implementation, as it currently always returns nil for error.
|
||||
All(ctx context.Context) (users []*User, err error)
|
||||
|
||||
// ByLogin retrieves a user by their login. u must not be modified.
|
||||
//
|
||||
// TODO(s.chzhen): Remove this once user sessions support [UserID].
|
||||
ByLogin(ctx context.Context, login Login) (u *User, err error)
|
||||
|
||||
// ByUUID retrieves a user by their unique identifier. u must not be
|
||||
// modified.
|
||||
//
|
||||
// TODO(s.chzhen): Use this.
|
||||
ByUUID(ctx context.Context, id UserID) (u *User, err error)
|
||||
|
||||
// Create adds a new user to the database. If the credentials already
|
||||
// exist, it returns the [errors.ErrDuplicated] error. It also can return
|
||||
// an error from the cryptographic randomness reader. u must not be
|
||||
// modified.
|
||||
Create(ctx context.Context, u *User) (err error)
|
||||
}
|
||||
|
||||
// DefaultDB is the default in-memory implementation of the [DB] interface.
|
||||
type DefaultDB struct {
|
||||
// mu protects all properties below.
|
||||
mu *sync.Mutex
|
||||
|
||||
// loginToUserID maps a web user login to their UserID. The values must not
|
||||
// be empty.
|
||||
//
|
||||
// TODO(s.chzhen): Remove this once user sessions support [UserID].
|
||||
loginToUserID map[Login]UserID
|
||||
|
||||
// userIDToUser maps a UserID to a web user. The values must not be nil.
|
||||
// It must be synchronized with loginToUserID, meaning all UserIDs stored in
|
||||
// loginToUserID must also be stored in this map.
|
||||
userIDToUser map[UserID]*User
|
||||
}
|
||||
|
||||
// NewDefaultDB returns the new properly initialized *DefaultDB.
|
||||
func NewDefaultDB() (db *DefaultDB) {
|
||||
return &DefaultDB{
|
||||
mu: &sync.Mutex{},
|
||||
loginToUserID: map[Login]UserID{},
|
||||
userIDToUser: map[UserID]*User{},
|
||||
}
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ DB = (*DefaultDB)(nil)
|
||||
|
||||
// All implements the [DB] interface for *DefaultDB.
|
||||
func (db *DefaultDB) All(ctx context.Context) (users []*User, err error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
if len(db.userIDToUser) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
users = slices.SortedStableFunc(
|
||||
maps.Values(db.userIDToUser),
|
||||
func(a, b *User) (res int) {
|
||||
// TODO(s.chzhen): Consider adding a custom comparer.
|
||||
return cmp.Compare(a.Login, b.Login)
|
||||
},
|
||||
)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ByLogin implements the [DB] interface for *DefaultDB.
|
||||
func (db *DefaultDB) ByLogin(ctx context.Context, login Login) (u *User, err error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
id, ok := db.loginToUserID[login]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
u, ok = db.userIDToUser[id]
|
||||
if !ok {
|
||||
// Should not happen.
|
||||
panic(fmt.Errorf("no web user present with login %q", login))
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ByUUID implements the [DB] interface for *DefaultDB.
|
||||
func (db *DefaultDB) ByUUID(ctx context.Context, id UserID) (u *User, err error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
u, ok := db.userIDToUser[id]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Create implements the [DB] interface for *DefaultDB.
|
||||
func (db *DefaultDB) Create(ctx context.Context, u *User) (err error) {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
if u.ID == (UserID{}) {
|
||||
return fmt.Errorf("userid: %w", errors.ErrEmptyValue)
|
||||
}
|
||||
|
||||
_, ok := db.userIDToUser[u.ID]
|
||||
if ok {
|
||||
return fmt.Errorf("userid: %w", errors.ErrDuplicated)
|
||||
}
|
||||
|
||||
_, ok = db.loginToUserID[u.Login]
|
||||
if ok {
|
||||
return fmt.Errorf("login: %w", errors.ErrDuplicated)
|
||||
}
|
||||
|
||||
db.userIDToUser[u.ID] = u
|
||||
db.loginToUserID[u.Login] = u.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package aghuser_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghuser"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestDB(t *testing.T) {
|
||||
db := aghuser.NewDefaultDB()
|
||||
|
||||
const (
|
||||
userWithIDPassRaw = "user_with_id_password"
|
||||
userSecondPassRaw = "user_second_password"
|
||||
)
|
||||
|
||||
userWithIDPassHash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte(userWithIDPassRaw),
|
||||
bcrypt.DefaultCost,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
userSecondPassHash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte(userSecondPassRaw),
|
||||
bcrypt.DefaultCost,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
userWithIDPass := aghuser.NewDefaultPassword(string(userWithIDPassHash))
|
||||
userSecondPass := aghuser.NewDefaultPassword(string(userSecondPassHash))
|
||||
|
||||
var (
|
||||
userWithID = &aghuser.User{
|
||||
ID: aghuser.MustNewUserID(),
|
||||
Login: "user_with_id",
|
||||
Password: userWithIDPass,
|
||||
}
|
||||
userSecond = &aghuser.User{
|
||||
ID: aghuser.MustNewUserID(),
|
||||
Login: "user_second",
|
||||
Password: userSecondPass,
|
||||
}
|
||||
userDuplicateLogin = &aghuser.User{
|
||||
ID: aghuser.MustNewUserID(),
|
||||
Login: userWithID.Login,
|
||||
Password: userWithIDPass,
|
||||
}
|
||||
)
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
err = db.Create(ctx, userWithID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.Create(ctx, userSecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.Create(ctx, userDuplicateLogin)
|
||||
assert.ErrorIs(t, err, errors.ErrDuplicated)
|
||||
|
||||
got, err := db.ByUUID(ctx, userWithID.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, userWithID, got)
|
||||
assert.True(t, got.Password.Authenticate(ctx, userWithIDPassRaw))
|
||||
|
||||
got, err = db.ByLogin(ctx, userSecond.Login)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, userSecond, got)
|
||||
assert.True(t, got.Password.Authenticate(ctx, userSecondPassRaw))
|
||||
|
||||
users, err := db.All(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, users, 2)
|
||||
assert.Equal(t, []*aghuser.User{userSecond, userWithID}, users)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package aghuser
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionToken is the type for the web user session token.
|
||||
type SessionToken [16]byte
|
||||
|
||||
// NewSessionToken returns a cryptographically secure randomly generated web
|
||||
// user session token. If an error occurs during random generation, it will
|
||||
// cause the program to crash.
|
||||
func NewSessionToken() (t SessionToken) {
|
||||
_, _ = rand.Read(t[:])
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Session represents a web user session.
|
||||
type Session struct {
|
||||
// Expire indicates when the session will expire.
|
||||
Expire time.Time
|
||||
|
||||
// UserLogin is the login of the web user associated with the session.
|
||||
//
|
||||
// TODO(s.chzhen): Remove this field and associate the user by UserID.
|
||||
UserLogin Login
|
||||
|
||||
// Token is the session token.
|
||||
Token SessionToken
|
||||
|
||||
// UserID is the identifier of the web user associated with the session.
|
||||
UserID UserID
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
package aghuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"go.etcd.io/bbolt"
|
||||
berrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
// SessionStorage is an interface that defines methods for handling web user
|
||||
// sessions. All methods must be safe for concurrent use.
|
||||
//
|
||||
// TODO(s.chzhen): Add DeleteAll method.
|
||||
type SessionStorage interface {
|
||||
// New creates a new session for the web user.
|
||||
New(ctx context.Context, u *User) (s *Session, err error)
|
||||
|
||||
// FindByToken returns the stored session for the web user based on the session
|
||||
// token.
|
||||
//
|
||||
// TODO(s.chzhen): Consider function signature change to reflect the
|
||||
// in-memory implementation, as it currently always returns nil for error.
|
||||
FindByToken(ctx context.Context, t SessionToken) (s *Session, err error)
|
||||
|
||||
// DeleteByToken removes a stored web user session by the provided token.
|
||||
DeleteByToken(ctx context.Context, t SessionToken) (err error)
|
||||
|
||||
// Close releases the web user sessions database resources.
|
||||
Close() (err error)
|
||||
}
|
||||
|
||||
// DefaultSessionStorageConfig represents the web user session storage
|
||||
// configuration structure.
|
||||
type DefaultSessionStorageConfig struct {
|
||||
// Logger is used for logging the operation of the session storage. It must
|
||||
// not be nil.
|
||||
Logger *slog.Logger
|
||||
|
||||
// Clock is used to get the current time. It must not be nil.
|
||||
Clock timeutil.Clock
|
||||
|
||||
// UserDB contains the web user information such as ID, login, and password.
|
||||
// It must not be nil.
|
||||
UserDB DB
|
||||
|
||||
// DBPath is the path to the database file where session data is stored. It
|
||||
// must not be empty.
|
||||
DBPath string
|
||||
|
||||
// SessionTTL is the default Time-To-Live duration for web user sessions.
|
||||
// It specifies how long a session should last and is a required field.
|
||||
SessionTTL time.Duration
|
||||
}
|
||||
|
||||
// DefaultSessionStorage is the default bbolt database implementation of the
|
||||
// [SessionStorage] interface.
|
||||
type DefaultSessionStorage struct {
|
||||
// db is an instance of the bbolt database where web user sessions are
|
||||
// stored by [SessionToken] in the [bucketNameSessions] bucket.
|
||||
db *bbolt.DB
|
||||
|
||||
// logger is used for logging the operation of the session storage.
|
||||
logger *slog.Logger
|
||||
|
||||
// mu protects sessions.
|
||||
mu *sync.Mutex
|
||||
|
||||
// clock is used to get the current time.
|
||||
clock timeutil.Clock
|
||||
|
||||
// userDB contains the web user information such as ID, login, and password.
|
||||
userDB DB
|
||||
|
||||
// sessions maps a session token to a web user session.
|
||||
sessions map[SessionToken]*Session
|
||||
|
||||
// sessionTTL is the default Time-To-Live value for web user sessions.
|
||||
sessionTTL time.Duration
|
||||
}
|
||||
|
||||
// NewDefaultSessionStorage returns the new properly initialized
|
||||
// *DefaultSessionStorage.
|
||||
func NewDefaultSessionStorage(
|
||||
ctx context.Context,
|
||||
conf *DefaultSessionStorageConfig,
|
||||
) (ds *DefaultSessionStorage, err error) {
|
||||
ds = &DefaultSessionStorage{
|
||||
clock: conf.Clock,
|
||||
userDB: conf.UserDB,
|
||||
logger: conf.Logger,
|
||||
mu: &sync.Mutex{},
|
||||
sessions: map[SessionToken]*Session{},
|
||||
sessionTTL: conf.SessionTTL,
|
||||
}
|
||||
|
||||
dbFilename := conf.DBPath
|
||||
// TODO(s.chzhen): Pass logger with options.
|
||||
ds.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, nil)
|
||||
if err != nil {
|
||||
ds.logger.ErrorContext(ctx, "opening db %q: %w", dbFilename, err)
|
||||
if errors.Is(err, berrors.ErrInvalid) {
|
||||
const s = "AdGuard Home cannot be initialized due to an incompatible file system.\n" +
|
||||
"Please read the explanation here: https://adguard-dns.io/kb/adguard-home/getting-started/#limitations"
|
||||
slogutil.PrintLines(ctx, ds.logger, slog.LevelError, "", s)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ds.loadSessions(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading sessions: %w", err)
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// loadSessions loads web user sessions from the bbolt database.
|
||||
func (ds *DefaultSessionStorage) loadSessions(ctx context.Context) (err error) {
|
||||
tx, err := ds.db.Begin(true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting transaction: %w", err)
|
||||
}
|
||||
|
||||
needRollback := true
|
||||
defer func() {
|
||||
if needRollback {
|
||||
err = errors.WithDeferred(err, tx.Rollback())
|
||||
}
|
||||
}()
|
||||
|
||||
bkt := tx.Bucket([]byte(bboltBucketSessions))
|
||||
if bkt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
removed, err := ds.processSessions(ctx, bkt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("processing sessions: %w", err)
|
||||
}
|
||||
|
||||
if removed == 0 {
|
||||
ds.logger.DebugContext(ctx, "loading sessions from db", "stored", len(ds.sessions))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
needRollback = false
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("committing transaction: %w", err)
|
||||
}
|
||||
|
||||
ds.logger.DebugContext(
|
||||
ctx,
|
||||
"loading sessions from db",
|
||||
"stored", len(ds.sessions),
|
||||
"removed", removed,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSessions iterates over the sessions bucket and loads or removes
|
||||
// sessions as needed.
|
||||
func (ds *DefaultSessionStorage) processSessions(
|
||||
ctx context.Context,
|
||||
bkt *bbolt.Bucket,
|
||||
) (removed int, err error) {
|
||||
invalidSessions := [][]byte{}
|
||||
|
||||
err = bkt.ForEach(ds.bboltSessionHandler(ctx, &invalidSessions))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("iterating over sessions: %w", err)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, s := range invalidSessions {
|
||||
if err = bkt.Delete(s); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = errors.Join(errs...); err != nil {
|
||||
return 0, fmt.Errorf("deleting sessions: %w", err)
|
||||
}
|
||||
|
||||
return len(invalidSessions), nil
|
||||
}
|
||||
|
||||
// bboltSessionHandler returns a function for [bbolt.Bucket.ForEach] that
|
||||
// iterates over stored sessions, deserializes them, and logs any errors
|
||||
// encountered. The returned error is always nil, as these errors are
|
||||
// considered non-critical to stop the iteration process.
|
||||
func (ds *DefaultSessionStorage) bboltSessionHandler(
|
||||
ctx context.Context,
|
||||
invalidSessions *[][]byte,
|
||||
) (fn func(k, v []byte) (err error)) {
|
||||
now := ds.clock.Now()
|
||||
|
||||
return func(k, v []byte) (err error) {
|
||||
s, err := bboltDecode(v)
|
||||
if err != nil {
|
||||
*invalidSessions = append(*invalidSessions, k)
|
||||
ds.logger.DebugContext(ctx, "deserializing session", slogutil.KeyError, err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if now.After(s.Expire) {
|
||||
*invalidSessions = append(*invalidSessions, k)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := ds.userDB.ByLogin(ctx, s.UserLogin)
|
||||
if err != nil {
|
||||
// Should not happen, as it currently always returns nil for error.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
*invalidSessions = append(*invalidSessions, k)
|
||||
ds.logger.DebugContext(ctx, "no saved user by name", "name", s.UserLogin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
t := SessionToken(k)
|
||||
s.Token = t
|
||||
s.UserID = u.ID
|
||||
ds.sessions[t] = s
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// bboltBucketSessions is the name of the bucket storing web user sessions in
|
||||
// the bbolt database.
|
||||
const bboltBucketSessions = "sessions-2"
|
||||
|
||||
const (
|
||||
// bboltSessionExpireLen is the length of the expire field in the binary
|
||||
// entry stored in bbolt.
|
||||
bboltSessionExpireLen = 4
|
||||
|
||||
// bboltSessionNameLen is the length of the name field in the binary entry
|
||||
// stored in bbolt.
|
||||
bboltSessionNameLen = 2
|
||||
)
|
||||
|
||||
// bboltDecode deserializes decodes a binary data into a session.
|
||||
func bboltDecode(data []byte) (s *Session, err error) {
|
||||
if len(data) < bboltSessionExpireLen+bboltSessionNameLen {
|
||||
return nil, fmt.Errorf("length of the data is less than expected: got %d", len(data))
|
||||
}
|
||||
|
||||
expireData := data[:bboltSessionExpireLen]
|
||||
nameLenData := data[bboltSessionExpireLen : bboltSessionExpireLen+bboltSessionNameLen]
|
||||
nameData := data[bboltSessionExpireLen+bboltSessionNameLen:]
|
||||
|
||||
nameLen := binary.BigEndian.Uint16(nameLenData)
|
||||
if len(nameData) != int(nameLen) {
|
||||
return nil, fmt.Errorf("login: expected length %d, got %d", nameLen, len(nameData))
|
||||
}
|
||||
|
||||
expire := binary.BigEndian.Uint32(expireData)
|
||||
|
||||
return &Session{
|
||||
Expire: time.Unix(int64(expire), 0),
|
||||
UserLogin: Login(nameData),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// bboltEncode serializes a session properties into a binary data.
|
||||
func bboltEncode(s *Session) (data []byte) {
|
||||
data = make([]byte, bboltSessionExpireLen+bboltSessionNameLen+len(s.UserLogin))
|
||||
|
||||
expireData := data[:bboltSessionExpireLen]
|
||||
nameLenData := data[bboltSessionExpireLen : bboltSessionExpireLen+bboltSessionNameLen]
|
||||
nameData := data[bboltSessionExpireLen+bboltSessionNameLen:]
|
||||
|
||||
expire := uint32(s.Expire.Unix())
|
||||
binary.BigEndian.PutUint32(expireData, expire)
|
||||
binary.BigEndian.PutUint16(nameLenData, uint16(len(s.UserLogin)))
|
||||
copy(nameData, []byte(s.UserLogin))
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ SessionStorage = (*DefaultSessionStorage)(nil)
|
||||
|
||||
// New implements the [SessionStorage] interface for *DefaultSessionStorage.
|
||||
func (ds *DefaultSessionStorage) New(ctx context.Context, u *User) (s *Session, err error) {
|
||||
s = &Session{
|
||||
Token: NewSessionToken(),
|
||||
UserID: u.ID,
|
||||
UserLogin: u.Login,
|
||||
Expire: ds.clock.Now().Add(ds.sessionTTL),
|
||||
}
|
||||
|
||||
err = ds.store(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storing session: %w", err)
|
||||
}
|
||||
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
ds.sessions[s.Token] = s
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// store saves a web user session in the bbolt database.
|
||||
func (ds *DefaultSessionStorage) store(s *Session) (err error) {
|
||||
tx, err := ds.db.Begin(true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting transaction: %w", err)
|
||||
}
|
||||
|
||||
needRollback := true
|
||||
defer func() {
|
||||
if needRollback {
|
||||
err = errors.WithDeferred(err, tx.Rollback())
|
||||
}
|
||||
}()
|
||||
|
||||
bkt, err := tx.CreateBucketIfNotExists([]byte(bboltBucketSessions))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating bucket: %w", err)
|
||||
}
|
||||
|
||||
err = bkt.Put(s.Token[:], bboltEncode(s))
|
||||
if err != nil {
|
||||
return fmt.Errorf("putting data: %w", err)
|
||||
}
|
||||
|
||||
needRollback = false
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("committing transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByToken implements the [SessionStorage] interface for *DefaultSessionStorage.
|
||||
func (ds *DefaultSessionStorage) FindByToken(ctx context.Context, t SessionToken) (s *Session, err error) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
s, ok := ds.sessions[t]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
now := ds.clock.Now()
|
||||
if now.After(s.Expire) {
|
||||
err = ds.deleteByToken(ctx, t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("expired session: %w", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// DeleteByToken implements the [SessionStorage] interface for
|
||||
// *DefaultSessionStorage.
|
||||
func (ds *DefaultSessionStorage) DeleteByToken(ctx context.Context, t SessionToken) (err error) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
// Don't wrap the error because it's informative enough as is.
|
||||
return ds.deleteByToken(ctx, t)
|
||||
}
|
||||
|
||||
// deleteByToken removes stored session by token. ds.mu is expected to be
|
||||
// locked.
|
||||
func (ds *DefaultSessionStorage) deleteByToken(ctx context.Context, t SessionToken) (err error) {
|
||||
err = ds.remove(ctx, t)
|
||||
if err != nil {
|
||||
ds.logger.ErrorContext(ctx, "deleting session", slogutil.KeyError, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
delete(ds.sessions, t)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove deletes a web user session from the bbolt database.
|
||||
func (ds *DefaultSessionStorage) remove(ctx context.Context, t SessionToken) (err error) {
|
||||
tx, err := ds.db.Begin(true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting transaction: %w", err)
|
||||
}
|
||||
|
||||
needRollback := true
|
||||
defer func() {
|
||||
if needRollback {
|
||||
err = errors.WithDeferred(err, tx.Rollback())
|
||||
}
|
||||
}()
|
||||
|
||||
bkt := tx.Bucket([]byte(bboltBucketSessions))
|
||||
if bkt == nil {
|
||||
return errors.Error("no bucket")
|
||||
}
|
||||
|
||||
err = bkt.Delete(t[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing data: %w", err)
|
||||
}
|
||||
|
||||
needRollback = false
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("committing transaction: %w", err)
|
||||
}
|
||||
|
||||
ds.logger.DebugContext(ctx, "removed session from db")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Close implements the [SessionStorage] interface for *DefaultSessionStorage.
|
||||
func (ds *DefaultSessionStorage) Close() (err error) {
|
||||
err = ds.db.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing db: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package aghuser_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghuser"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/testutil/faketime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// addSession is a helper function that saves and returns a session for a newly
|
||||
// generated [aghuser.User] by login.
|
||||
func addSession(
|
||||
tb testing.TB,
|
||||
ctx context.Context,
|
||||
ds aghuser.SessionStorage,
|
||||
login aghuser.Login,
|
||||
) (s *aghuser.Session) {
|
||||
tb.Helper()
|
||||
|
||||
s, err := ds.New(ctx, &aghuser.User{
|
||||
ID: aghuser.MustNewUserID(),
|
||||
Login: login,
|
||||
})
|
||||
require.NoError(tb, err)
|
||||
require.NotNil(tb, s)
|
||||
|
||||
var got *aghuser.Session
|
||||
got, err = ds.FindByToken(ctx, s.Token)
|
||||
require.NoError(tb, err)
|
||||
require.NotNil(tb, got)
|
||||
|
||||
assert.Equal(tb, login, got.UserLogin)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func TestDefaultSessionStorage(t *testing.T) {
|
||||
const (
|
||||
userLoginFirst aghuser.Login = "user_one"
|
||||
userLoginSecond aghuser.Login = "user_two"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = testutil.ContextWithTimeout(t, testTimeout)
|
||||
logger = slogutil.NewDiscardLogger()
|
||||
)
|
||||
|
||||
const (
|
||||
sessionTTL = time.Minute
|
||||
timeStep = time.Second
|
||||
)
|
||||
|
||||
// Set up a mock clock to test expired sessions. Each call to [clock.Now]
|
||||
// will return the [date] incremented by [timeStep].
|
||||
date := time.Now()
|
||||
clock := &faketime.Clock{
|
||||
OnNow: func() (now time.Time) {
|
||||
date = date.Add(timeStep)
|
||||
|
||||
return date
|
||||
},
|
||||
}
|
||||
|
||||
dbFile, err := os.CreateTemp(t.TempDir(), "sessions.db")
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, dbFile.Close)
|
||||
|
||||
userDB := aghuser.NewDefaultDB()
|
||||
|
||||
err = userDB.Create(ctx, &aghuser.User{
|
||||
Login: userLoginFirst,
|
||||
ID: aghuser.MustNewUserID(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = userDB.Create(ctx, &aghuser.User{
|
||||
Login: userLoginSecond,
|
||||
ID: aghuser.MustNewUserID(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var (
|
||||
ds *aghuser.DefaultSessionStorage
|
||||
|
||||
sessionFirst *aghuser.Session
|
||||
sessionSecond *aghuser.Session
|
||||
)
|
||||
|
||||
require.True(t, t.Run("prepare_session_storage", func(t *testing.T) {
|
||||
ds, err = aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{
|
||||
Clock: clock,
|
||||
UserDB: userDB,
|
||||
Logger: logger,
|
||||
DBPath: dbFile.Name(),
|
||||
SessionTTL: sessionTTL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionFirst = addSession(t, ctx, ds, userLoginFirst)
|
||||
|
||||
// Advance time to ensure the first session expires before creating the
|
||||
// second session.
|
||||
date = date.Add(time.Hour)
|
||||
|
||||
sessionSecond = addSession(t, ctx, ds, userLoginSecond)
|
||||
|
||||
err = ds.Close()
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
require.True(t, t.Run("load_sessions", func(t *testing.T) {
|
||||
ds, err = aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{
|
||||
Clock: clock,
|
||||
UserDB: userDB,
|
||||
Logger: logger,
|
||||
DBPath: dbFile.Name(),
|
||||
SessionTTL: sessionTTL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var got *aghuser.Session
|
||||
got, err = ds.FindByToken(ctx, sessionFirst.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Nil(t, got)
|
||||
|
||||
got, err = ds.FindByToken(ctx, sessionSecond.Token)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
|
||||
assert.Equal(t, userLoginSecond, got.UserLogin)
|
||||
|
||||
err = ds.DeleteByToken(ctx, sessionSecond.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err = ds.FindByToken(ctx, sessionSecond.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Nil(t, got)
|
||||
}))
|
||||
|
||||
require.True(t, t.Run("expired_session", func(t *testing.T) {
|
||||
testutil.CleanupAndRequireSuccess(t, ds.Close)
|
||||
|
||||
sessionFirst = addSession(t, ctx, ds, userLoginFirst)
|
||||
|
||||
date = date.Add(time.Hour)
|
||||
|
||||
var got *aghuser.Session
|
||||
got, err = ds.FindByToken(ctx, sessionFirst.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Nil(t, got)
|
||||
}))
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Package aghuser contains types and logic for dealing with AdGuard Home's web
|
||||
// users.
|
||||
package aghuser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserID is the type for the unique IDs of web users.
|
||||
type UserID uuid.UUID
|
||||
|
||||
// NewUserID returns a new web user unique identifier. Any error returned is an
|
||||
// error from the cryptographic randomness reader.
|
||||
func NewUserID() (uid UserID, err error) {
|
||||
uuidv7, err := uuid.NewV7()
|
||||
|
||||
return UserID(uuidv7), err
|
||||
}
|
||||
|
||||
// MustNewUserID is a wrapper around [NewUserID] that panics if there is an
|
||||
// error. It is currently only used in tests.
|
||||
func MustNewUserID() (uid UserID) {
|
||||
uid, err := NewUserID()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unexpected uuidv7 error: %w", err))
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
// User represents a web user.
|
||||
type User struct {
|
||||
// Password stores the password information for the web user. It must not
|
||||
// be nil.
|
||||
Password Password
|
||||
|
||||
// Login is the login name of the web user. It must not be empty.
|
||||
Login Login
|
||||
|
||||
// ID is the unique identifier for the web user. It must not be empty.
|
||||
ID UserID
|
||||
}
|
||||
@@ -11,34 +11,8 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
// ClientID is a unique identifier for a persistent client used in
|
||||
// DNS-over-HTTPS, DNS-over-TLS, and DNS-over-QUIC queries.
|
||||
//
|
||||
// TODO(s.chzhen): Use everywhere.
|
||||
type ClientID string
|
||||
|
||||
// ValidateClientID returns an error if id is not a valid ClientID.
|
||||
//
|
||||
// TODO(s.chzhen): Consider implementing [validate.Interface] for ClientID.
|
||||
func ValidateClientID(id string) (err error) {
|
||||
err = netutil.ValidateHostnameLabel(id)
|
||||
if err != nil {
|
||||
// Replace the domain name label wrapper with our own.
|
||||
return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidClientID returns false if id is not a valid ClientID.
|
||||
func isValidClientID(id string) (ok bool) {
|
||||
return netutil.IsValidHostnameLabel(id)
|
||||
}
|
||||
|
||||
// Source represents the source from which the information about the client has
|
||||
// been obtained.
|
||||
type Source uint8
|
||||
|
||||
@@ -35,7 +35,7 @@ type index struct {
|
||||
nameToUID map[string]UID
|
||||
|
||||
// clientIDToUID maps ClientID to UID.
|
||||
clientIDToUID map[ClientID]UID
|
||||
clientIDToUID map[string]UID
|
||||
|
||||
// ipToUID maps IP address to UID.
|
||||
ipToUID map[netip.Addr]UID
|
||||
@@ -54,7 +54,7 @@ type index struct {
|
||||
func newIndex() (ci *index) {
|
||||
return &index{
|
||||
nameToUID: map[string]UID{},
|
||||
clientIDToUID: map[ClientID]UID{},
|
||||
clientIDToUID: map[string]UID{},
|
||||
ipToUID: map[netip.Addr]UID{},
|
||||
subnetToUID: aghalg.NewSortedMap[netip.Prefix, UID](subnetCompare),
|
||||
macToUID: map[macKey]UID{},
|
||||
@@ -207,7 +207,7 @@ func (ci *index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr)
|
||||
// find finds persistent client by string representation of the ClientID, IP
|
||||
// address, or MAC.
|
||||
func (ci *index) find(id string) (c *Persistent, ok bool) {
|
||||
c, ok = ci.findByClientID(ClientID(id))
|
||||
c, ok = ci.findByClientID(id)
|
||||
if ok {
|
||||
return c, true
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func (ci *index) find(id string) (c *Persistent, ok bool) {
|
||||
}
|
||||
|
||||
// findByClientID finds persistent client by ClientID.
|
||||
func (ci *index) findByClientID(clientID ClientID) (c *Persistent, ok bool) {
|
||||
func (ci *index) findByClientID(clientID string) (c *Persistent, ok bool) {
|
||||
uid, ok := ci.clientIDToUID[clientID]
|
||||
if ok {
|
||||
return ci.uidToClient[uid], true
|
||||
@@ -275,26 +275,6 @@ func (ci *index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// findByCIDR searches for a persistent client with the provided subnet as an
|
||||
// identifier. Note that this function looks for an exact match of subnets,
|
||||
// rather than checking if one subnet contains another.
|
||||
func (ci *index) findByCIDR(subnet netip.Prefix) (c *Persistent, ok bool) {
|
||||
var uid UID
|
||||
for pref, id := range ci.subnetToUID.Range {
|
||||
if subnet == pref {
|
||||
uid, ok = id, true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
return ci.uidToClient[uid], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// findByMAC finds persistent client by MAC.
|
||||
func (ci *index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
|
||||
k := macToKey(mac)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -59,12 +58,12 @@ func TestClientIndex_Find(t *testing.T) {
|
||||
|
||||
clientWithMAC = &Persistent{
|
||||
Name: "client_with_mac",
|
||||
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
|
||||
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
|
||||
}
|
||||
|
||||
clientWithID = &Persistent{
|
||||
Name: "client_with_id",
|
||||
ClientIDs: []ClientID{cliID},
|
||||
ClientIDs: []string{cliID},
|
||||
}
|
||||
|
||||
clientLinkLocal = &Persistent{
|
||||
@@ -142,10 +141,10 @@ func TestClientIndex_Clashes(t *testing.T) {
|
||||
Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
|
||||
}, {
|
||||
Name: "client_with_mac",
|
||||
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
|
||||
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
|
||||
}, {
|
||||
Name: "client_with_id",
|
||||
ClientIDs: []ClientID{cliID},
|
||||
ClientIDs: []string{cliID},
|
||||
}}
|
||||
|
||||
ci := newIDIndex(clients)
|
||||
@@ -182,6 +181,17 @@ func TestClientIndex_Clashes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
|
||||
// error.
|
||||
func mustParseMAC(s string) (mac net.HardwareAddr) {
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return mac
|
||||
}
|
||||
|
||||
func TestMACToKey(t *testing.T) {
|
||||
testCases := []struct {
|
||||
want any
|
||||
@@ -190,44 +200,44 @@ func TestMACToKey(t *testing.T) {
|
||||
}{{
|
||||
name: "column6",
|
||||
in: "00:00:5e:00:53:01",
|
||||
want: [6]byte(errors.Must(net.ParseMAC("00:00:5e:00:53:01"))),
|
||||
want: [6]byte(mustParseMAC("00:00:5e:00:53:01")),
|
||||
}, {
|
||||
name: "column8",
|
||||
in: "02:00:5e:10:00:00:00:01",
|
||||
want: [8]byte(errors.Must(net.ParseMAC("02:00:5e:10:00:00:00:01"))),
|
||||
want: [8]byte(mustParseMAC("02:00:5e:10:00:00:00:01")),
|
||||
}, {
|
||||
name: "column20",
|
||||
in: "00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01",
|
||||
want: [20]byte(errors.Must(net.ParseMAC("00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01"))),
|
||||
want: [20]byte(mustParseMAC("00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01")),
|
||||
}, {
|
||||
name: "hyphen6",
|
||||
in: "00-00-5e-00-53-01",
|
||||
want: [6]byte(errors.Must(net.ParseMAC("00-00-5e-00-53-01"))),
|
||||
want: [6]byte(mustParseMAC("00-00-5e-00-53-01")),
|
||||
}, {
|
||||
name: "hyphen8",
|
||||
in: "02-00-5e-10-00-00-00-01",
|
||||
want: [8]byte(errors.Must(net.ParseMAC("02-00-5e-10-00-00-00-01"))),
|
||||
want: [8]byte(mustParseMAC("02-00-5e-10-00-00-00-01")),
|
||||
}, {
|
||||
name: "hyphen20",
|
||||
in: "00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01",
|
||||
want: [20]byte(errors.Must(net.ParseMAC("00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01"))),
|
||||
want: [20]byte(mustParseMAC("00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01")),
|
||||
}, {
|
||||
name: "dot6",
|
||||
in: "0000.5e00.5301",
|
||||
want: [6]byte(errors.Must(net.ParseMAC("0000.5e00.5301"))),
|
||||
want: [6]byte(mustParseMAC("0000.5e00.5301")),
|
||||
}, {
|
||||
name: "dot8",
|
||||
in: "0200.5e10.0000.0001",
|
||||
want: [8]byte(errors.Must(net.ParseMAC("0200.5e10.0000.0001"))),
|
||||
want: [8]byte(mustParseMAC("0200.5e10.0000.0001")),
|
||||
}, {
|
||||
name: "dot20",
|
||||
in: "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001",
|
||||
want: [20]byte(errors.Must(net.ParseMAC("0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001"))),
|
||||
want: [20]byte(mustParseMAC("0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001")),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mac := errors.Must(net.ParseMAC(tc.in))
|
||||
mac := mustParseMAC(tc.in)
|
||||
|
||||
key := macToKey(mac)
|
||||
assert.Equal(t, tc.want, key)
|
||||
@@ -292,19 +302,19 @@ func TestIndex_FindByIPWithoutZone(t *testing.T) {
|
||||
func TestClientIndex_RangeByName(t *testing.T) {
|
||||
sortedClients := []*Persistent{{
|
||||
Name: "clientA",
|
||||
ClientIDs: []ClientID{"A"},
|
||||
ClientIDs: []string{"A"},
|
||||
}, {
|
||||
Name: "clientB",
|
||||
ClientIDs: []ClientID{"B"},
|
||||
ClientIDs: []string{"B"},
|
||||
}, {
|
||||
Name: "clientC",
|
||||
ClientIDs: []ClientID{"C"},
|
||||
ClientIDs: []string{"C"},
|
||||
}, {
|
||||
Name: "clientD",
|
||||
ClientIDs: []ClientID{"D"},
|
||||
ClientIDs: []string{"D"},
|
||||
}, {
|
||||
Name: "clientE",
|
||||
ClientIDs: []ClientID{"E"},
|
||||
ClientIDs: []string{"E"},
|
||||
}}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -339,115 +349,3 @@ func TestClientIndex_RangeByName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndex_FindByName(t *testing.T) {
|
||||
const (
|
||||
clientExistingName = "client_existing"
|
||||
clientAnotherExistingName = "client_another_existing"
|
||||
nonExistingClientName = "client_non_existing"
|
||||
)
|
||||
|
||||
var (
|
||||
clientExisting = &Persistent{
|
||||
Name: clientExistingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr("192.0.2.1")},
|
||||
}
|
||||
|
||||
clientAnotherExisting = &Persistent{
|
||||
Name: clientAnotherExistingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*Persistent{
|
||||
clientExisting,
|
||||
clientAnotherExisting,
|
||||
}
|
||||
ci := newIDIndex(clients)
|
||||
|
||||
testCases := []struct {
|
||||
want *Persistent
|
||||
found assert.BoolAssertionFunc
|
||||
name string
|
||||
clientName string
|
||||
}{{
|
||||
want: clientExisting,
|
||||
found: assert.True,
|
||||
name: "existing",
|
||||
clientName: clientExistingName,
|
||||
}, {
|
||||
want: clientAnotherExisting,
|
||||
found: assert.True,
|
||||
name: "another_existing",
|
||||
clientName: clientAnotherExistingName,
|
||||
}, {
|
||||
want: nil,
|
||||
found: assert.False,
|
||||
name: "non_existing",
|
||||
clientName: nonExistingClientName,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, ok := ci.findByName(tc.clientName)
|
||||
assert.Equal(t, tc.want, c)
|
||||
tc.found(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndex_FindByMAC(t *testing.T) {
|
||||
var (
|
||||
cliMAC = errors.Must(net.ParseMAC("11:11:11:11:11:11"))
|
||||
cliAnotherMAC = errors.Must(net.ParseMAC("22:22:22:22:22:22"))
|
||||
nonExistingClientMAC = errors.Must(net.ParseMAC("33:33:33:33:33:33"))
|
||||
)
|
||||
|
||||
var (
|
||||
clientExisting = &Persistent{
|
||||
Name: "client",
|
||||
MACs: []net.HardwareAddr{cliMAC},
|
||||
}
|
||||
|
||||
clientAnotherExisting = &Persistent{
|
||||
Name: "another_client",
|
||||
MACs: []net.HardwareAddr{cliAnotherMAC},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*Persistent{
|
||||
clientExisting,
|
||||
clientAnotherExisting,
|
||||
}
|
||||
ci := newIDIndex(clients)
|
||||
|
||||
testCases := []struct {
|
||||
want *Persistent
|
||||
found assert.BoolAssertionFunc
|
||||
name string
|
||||
clientMAC net.HardwareAddr
|
||||
}{{
|
||||
want: clientExisting,
|
||||
found: assert.True,
|
||||
name: "existing",
|
||||
clientMAC: cliMAC,
|
||||
}, {
|
||||
want: clientAnotherExisting,
|
||||
found: assert.True,
|
||||
name: "another_existing",
|
||||
clientMAC: cliAnotherMAC,
|
||||
}, {
|
||||
want: nil,
|
||||
found: assert.False,
|
||||
name: "non_existing",
|
||||
clientMAC: nonExistingClientMAC,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, ok := ci.findByMAC(tc.clientMAC)
|
||||
assert.Equal(t, tc.want, c)
|
||||
tc.found(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -70,9 +71,7 @@ type Persistent struct {
|
||||
// Tags is a list of client tags that categorize the client.
|
||||
Tags []string
|
||||
|
||||
// Upstreams is a list of custom upstream DNS servers for the client. If
|
||||
// it's empty, the custom upstream cache is disabled, regardless of the
|
||||
// value of UpstreamsCacheEnabled.
|
||||
// Upstreams is a list of custom upstream DNS servers for the client.
|
||||
Upstreams []string
|
||||
|
||||
// IPs is a list of IP addresses that identify the client. The client must
|
||||
@@ -91,16 +90,15 @@ type Persistent struct {
|
||||
|
||||
// ClientIDs identifying the client. The client must have at least one ID
|
||||
// (IP, subnet, MAC, or ClientID).
|
||||
ClientIDs []ClientID
|
||||
ClientIDs []string
|
||||
|
||||
// UID is the unique identifier of the persistent client.
|
||||
UID UID
|
||||
|
||||
// UpstreamsCacheSize defines the size of the custom upstream cache.
|
||||
// UpstreamsCacheSize is the cache size for custom upstreams.
|
||||
UpstreamsCacheSize uint32
|
||||
|
||||
// UpstreamsCacheEnabled specifies whether the custom upstream cache is
|
||||
// used. If true, the list of Upstreams should not be empty.
|
||||
// UpstreamsCacheEnabled specifies whether custom upstreams are used.
|
||||
UpstreamsCacheEnabled bool
|
||||
|
||||
// UseOwnSettings specifies whether custom filtering settings are used.
|
||||
@@ -136,7 +134,7 @@ func (c *Persistent) validate(ctx context.Context, l *slog.Logger, allTags []str
|
||||
switch {
|
||||
case c.Name == "":
|
||||
return errors.Error("empty name")
|
||||
case c.idendifiersLen() == 0:
|
||||
case c.IDsLen() == 0:
|
||||
return errors.Error("id required")
|
||||
case c.UID == UID{}:
|
||||
return errors.Error("uid required")
|
||||
@@ -239,15 +237,28 @@ func (c *Persistent) setID(id string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
c.ClientIDs = append(c.ClientIDs, ClientID(strings.ToLower(id)))
|
||||
c.ClientIDs = append(c.ClientIDs, strings.ToLower(id))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Identifiers returns a list of client identifiers containing at least one
|
||||
// element.
|
||||
func (c *Persistent) Identifiers() (ids []string) {
|
||||
ids = make([]string, 0, c.idendifiersLen())
|
||||
// ValidateClientID returns an error if id is not a valid ClientID.
|
||||
//
|
||||
// TODO(s.chzhen): It's an exact copy of the [dnsforward.ValidateClientID] to
|
||||
// avoid the import cycle. Remove it.
|
||||
func ValidateClientID(id string) (err error) {
|
||||
err = netutil.ValidateHostnameLabel(id)
|
||||
if err != nil {
|
||||
// Replace the domain name label wrapper with our own.
|
||||
return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDs returns a list of ClientIDs containing at least one element.
|
||||
func (c *Persistent) IDs() (ids []string) {
|
||||
ids = make([]string, 0, c.IDsLen())
|
||||
|
||||
for _, ip := range c.IPs {
|
||||
ids = append(ids, ip.String())
|
||||
@@ -261,15 +272,11 @@ func (c *Persistent) Identifiers() (ids []string) {
|
||||
ids = append(ids, mac.String())
|
||||
}
|
||||
|
||||
for _, cid := range c.ClientIDs {
|
||||
ids = append(ids, string(cid))
|
||||
}
|
||||
|
||||
return ids
|
||||
return append(ids, c.ClientIDs...)
|
||||
}
|
||||
|
||||
// identifiersLen returns the number of client identifiers.
|
||||
func (c *Persistent) idendifiersLen() (n int) {
|
||||
// IDsLen returns a length of ClientIDs.
|
||||
func (c *Persistent) IDsLen() (n int) {
|
||||
return len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/hostsfile"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
)
|
||||
|
||||
@@ -434,138 +433,48 @@ func (s *Storage) Add(ctx context.Context, p *Persistent) (err error) {
|
||||
ctx,
|
||||
"client added",
|
||||
"name", p.Name,
|
||||
"ids", p.Identifiers(),
|
||||
"ids", p.IDs(),
|
||||
"clients_count", s.index.size(),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindParams represents the parameters for searching a client. At least one
|
||||
// field must be non-empty.
|
||||
type FindParams struct {
|
||||
// ClientID is a unique identifier for the client used in DoH, DoT, and DoQ
|
||||
// DNS queries.
|
||||
ClientID ClientID
|
||||
|
||||
// RemoteIP is the IP address used as a client search parameter.
|
||||
RemoteIP netip.Addr
|
||||
|
||||
// Subnet is the CIDR used as a client search parameter.
|
||||
Subnet netip.Prefix
|
||||
|
||||
// MAC is the physical hardware address used as a client search parameter.
|
||||
MAC net.HardwareAddr
|
||||
|
||||
// UID is the unique ID of persistent client used as a search parameter.
|
||||
//
|
||||
// TODO(s.chzhen): Use this.
|
||||
UID UID
|
||||
}
|
||||
|
||||
// ErrBadIdentifier is returned by [FindParams.Set] when it cannot parse the
|
||||
// provided client identifier.
|
||||
const ErrBadIdentifier errors.Error = "bad client identifier"
|
||||
|
||||
// Set clears the stored search parameters and parses the string representation
|
||||
// of the search parameter into typed parameter, storing it. In some cases, it
|
||||
// may result in storing both an IP address and a MAC address because they might
|
||||
// have identical string representations. It returns [ErrBadIdentifier] if id
|
||||
// cannot be parsed.
|
||||
//
|
||||
// TODO(s.chzhen): Add support for UID.
|
||||
func (p *FindParams) Set(id string) (err error) {
|
||||
*p = FindParams{}
|
||||
|
||||
isFound := false
|
||||
|
||||
if netutil.IsValidIPString(id) {
|
||||
// It is safe to use [netip.MustParseAddr] because it has already been
|
||||
// validated that id contains the string representation of the IP
|
||||
// address.
|
||||
p.RemoteIP = netip.MustParseAddr(id)
|
||||
|
||||
// Even if id can be parsed as an IP address, it may be a MAC address.
|
||||
// So do not return prematurely, continue parsing.
|
||||
isFound = true
|
||||
}
|
||||
|
||||
if netutil.IsValidMACString(id) {
|
||||
p.MAC, err = net.ParseMAC(id)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parsing mac from %q: %w", id, err))
|
||||
}
|
||||
|
||||
isFound = true
|
||||
}
|
||||
|
||||
if isFound {
|
||||
return nil
|
||||
}
|
||||
|
||||
if netutil.IsValidIPPrefixString(id) {
|
||||
// It is safe to use [netip.MustParsePrefix] because it has already been
|
||||
// validated that id contains the string representation of IP prefix.
|
||||
p.Subnet = netip.MustParsePrefix(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isValidClientID(id) {
|
||||
return ErrBadIdentifier
|
||||
}
|
||||
|
||||
p.ClientID = ClientID(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find represents the parameters for searching a client. params must not be
|
||||
// nil and must have at least one non-empty field.
|
||||
func (s *Storage) Find(params *FindParams) (p *Persistent, ok bool) {
|
||||
// FindByName finds persistent client by name. And returns its shallow copy.
|
||||
func (s *Storage) FindByName(name string) (p *Persistent, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
isClientID := params.ClientID != ""
|
||||
isRemoteIP := params.RemoteIP != (netip.Addr{})
|
||||
isSubnet := params.Subnet != (netip.Prefix{})
|
||||
isMAC := params.MAC != nil
|
||||
|
||||
for {
|
||||
switch {
|
||||
case isClientID:
|
||||
isClientID = false
|
||||
p, ok = s.index.findByClientID(params.ClientID)
|
||||
case isRemoteIP:
|
||||
isRemoteIP = false
|
||||
p, ok = s.findByIP(params.RemoteIP)
|
||||
case isSubnet:
|
||||
isSubnet = false
|
||||
p, ok = s.index.findByCIDR(params.Subnet)
|
||||
case isMAC:
|
||||
isMAC = false
|
||||
p, ok = s.index.findByMAC(params.MAC)
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if ok {
|
||||
return p.ShallowClone(), true
|
||||
}
|
||||
p, ok = s.index.findByName(name)
|
||||
if ok {
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// findByIP finds persistent client by IP address. s.mu is expected to be
|
||||
// locked.
|
||||
func (s *Storage) findByIP(addr netip.Addr) (p *Persistent, ok bool) {
|
||||
p, ok = s.index.findByIP(addr)
|
||||
// Find finds persistent client by string representation of the ClientID, IP
|
||||
// address, or MAC. And returns its shallow copy.
|
||||
//
|
||||
// TODO(s.chzhen): Accept ClientIDData structure instead, which will contain
|
||||
// the parsed IP address, if any.
|
||||
func (s *Storage) Find(id string) (p *Persistent, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok = s.index.find(id)
|
||||
if ok {
|
||||
return p, true
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
foundMAC := s.dhcp.MACByIP(addr)
|
||||
ip, err := netip.ParseAddr(id)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
foundMAC := s.dhcp.MACByIP(ip)
|
||||
if foundMAC != nil {
|
||||
return s.index.findByMAC(foundMAC)
|
||||
return s.FindByMAC(foundMAC)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
@@ -578,8 +487,6 @@ func (s *Storage) findByIP(addr netip.Addr) (p *Persistent, ok bool) {
|
||||
//
|
||||
// Note that multiple clients can have the same IP address with different zones.
|
||||
// Therefore, the result of this method is indeterminate.
|
||||
//
|
||||
// TODO(s.chzhen): Consider accepting [FindParams].
|
||||
func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -589,11 +496,6 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
foundMAC := s.dhcp.MACByIP(ip)
|
||||
if foundMAC != nil {
|
||||
return s.index.findByMAC(foundMAC)
|
||||
}
|
||||
|
||||
p = s.index.findByIPWithoutZone(ip)
|
||||
if p != nil {
|
||||
return p.ShallowClone(), true
|
||||
@@ -602,6 +504,17 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindByMAC finds persistent client by MAC and returns its shallow copy. s.mu
|
||||
// is expected to be locked.
|
||||
func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) {
|
||||
p, ok = s.index.findByMAC(mac)
|
||||
if ok {
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// RemoveByName removes persistent client information. ok is false if no such
|
||||
// client exists by that name.
|
||||
func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) {
|
||||
@@ -730,9 +643,9 @@ func (s *Storage) CustomUpstreamConfig(
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
c, ok := s.index.findByClientID(ClientID(id))
|
||||
c, ok := s.index.findByClientID(id)
|
||||
if !ok {
|
||||
c, ok = s.findByIP(addr)
|
||||
c, ok = s.index.findByIP(addr)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -764,18 +677,11 @@ func (s *Storage) ClearUpstreamCache() {
|
||||
// ClientID or client IP address, and applies it to the filtering settings.
|
||||
// setts must not be nil.
|
||||
func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filtering.Settings) {
|
||||
c, ok := s.index.findByClientID(ClientID(id))
|
||||
c, ok := s.index.findByClientID(id)
|
||||
if !ok {
|
||||
c, ok = s.index.findByIP(addr)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
foundMAC := s.dhcp.MACByIP(addr)
|
||||
if foundMAC != nil {
|
||||
c, ok = s.index.findByMAC(foundMAC)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
s.logger.Debug("no client filtering settings found", "clientid", id, "addr", addr)
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/hostsfile"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
@@ -351,15 +350,15 @@ func TestClientsDHCP(t *testing.T) {
|
||||
cliName1 = "one.dhcp"
|
||||
|
||||
cliIP2 = netip.MustParseAddr("2.2.2.2")
|
||||
cliMAC2 = errors.Must(net.ParseMAC("22:22:22:22:22:22"))
|
||||
cliMAC2 = mustParseMAC("22:22:22:22:22:22")
|
||||
cliName2 = "two.dhcp"
|
||||
|
||||
cliIP3 = netip.MustParseAddr("3.3.3.3")
|
||||
cliMAC3 = errors.Must(net.ParseMAC("33:33:33:33:33:33"))
|
||||
cliMAC3 = mustParseMAC("33:33:33:33:33:33")
|
||||
cliName3 = "three.dhcp"
|
||||
|
||||
prsCliIP = netip.MustParseAddr("4.3.2.1")
|
||||
prsCliMAC = errors.Must(net.ParseMAC("AA:AA:AA:AA:AA:AA"))
|
||||
prsCliMAC = mustParseMAC("AA:AA:AA:AA:AA:AA")
|
||||
prsCliName = "persistent.dhcp"
|
||||
|
||||
otherARPCliName = "other.arp"
|
||||
@@ -520,11 +519,7 @@ func TestClientsDHCP(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
params := &client.FindParams{}
|
||||
err = params.Set(prsCliIP.String())
|
||||
require.NoError(t, err)
|
||||
|
||||
prsCli, ok := storage.Find(params)
|
||||
prsCli, ok := storage.Find(prsCliIP.String())
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, prsCliName, prsCli.Name)
|
||||
@@ -668,6 +663,17 @@ func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
|
||||
return s
|
||||
}
|
||||
|
||||
// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
|
||||
// error.
|
||||
func mustParseMAC(s string) (mac net.HardwareAddr) {
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return mac
|
||||
}
|
||||
|
||||
func TestStorage_Add(t *testing.T) {
|
||||
const (
|
||||
existingName = "existing_name"
|
||||
@@ -687,7 +693,7 @@ func TestStorage_Add(t *testing.T) {
|
||||
Name: existingName,
|
||||
IPs: []netip.Addr{existingIP},
|
||||
Subnets: []netip.Prefix{existingSubnet},
|
||||
ClientIDs: []client.ClientID{existingClientID},
|
||||
ClientIDs: []string{existingClientID},
|
||||
UID: existingClientUID,
|
||||
}
|
||||
|
||||
@@ -755,7 +761,7 @@ func TestStorage_Add(t *testing.T) {
|
||||
name: "duplicate_client_id",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_client_id",
|
||||
ClientIDs: []client.ClientID{existingClientID},
|
||||
ClientIDs: []string{existingClientID},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `adding client: another client "existing_name" ` +
|
||||
@@ -892,12 +898,12 @@ func TestStorage_Find(t *testing.T) {
|
||||
|
||||
clientWithMAC = &client.Persistent{
|
||||
Name: "client_with_mac",
|
||||
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
|
||||
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
|
||||
}
|
||||
|
||||
clientWithID = &client.Persistent{
|
||||
Name: "client_with_id",
|
||||
ClientIDs: []client.ClientID{cliID},
|
||||
ClientIDs: []string{cliID},
|
||||
}
|
||||
|
||||
clientLinkLocal = &client.Persistent{
|
||||
@@ -944,11 +950,7 @@ func TestStorage_Find(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, id := range tc.ids {
|
||||
params := &client.FindParams{}
|
||||
err := params.Set(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, ok := s.Find(params)
|
||||
c, ok := s.Find(id)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, tc.want, c)
|
||||
@@ -957,11 +959,7 @@ func TestStorage_Find(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("not_found", func(t *testing.T) {
|
||||
params := &client.FindParams{}
|
||||
err := params.Set(cliIPNone)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok := s.Find(params)
|
||||
_, ok := s.Find(cliIPNone)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
@@ -1027,6 +1025,127 @@ func TestStorage_FindLoose(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_FindByName(t *testing.T) {
|
||||
const (
|
||||
cliIP1 = "1.1.1.1"
|
||||
cliIP2 = "2.2.2.2"
|
||||
)
|
||||
|
||||
const (
|
||||
clientExistingName = "client_existing"
|
||||
clientAnotherExistingName = "client_another_existing"
|
||||
nonExistingClientName = "client_non_existing"
|
||||
)
|
||||
|
||||
var (
|
||||
clientExisting = &client.Persistent{
|
||||
Name: clientExistingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP1)},
|
||||
}
|
||||
|
||||
clientAnotherExisting = &client.Persistent{
|
||||
Name: clientAnotherExistingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*client.Persistent{
|
||||
clientExisting,
|
||||
clientAnotherExisting,
|
||||
}
|
||||
s := newStorage(t, clients)
|
||||
|
||||
testCases := []struct {
|
||||
want *client.Persistent
|
||||
name string
|
||||
clientName string
|
||||
}{{
|
||||
name: "existing",
|
||||
clientName: clientExistingName,
|
||||
want: clientExisting,
|
||||
}, {
|
||||
name: "another_existing",
|
||||
clientName: clientAnotherExistingName,
|
||||
want: clientAnotherExisting,
|
||||
}, {
|
||||
name: "non_existing",
|
||||
clientName: nonExistingClientName,
|
||||
want: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, ok := s.FindByName(tc.clientName)
|
||||
if tc.want == nil {
|
||||
assert.False(t, ok)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tc.want, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_FindByMAC(t *testing.T) {
|
||||
var (
|
||||
cliMAC = mustParseMAC("11:11:11:11:11:11")
|
||||
cliAnotherMAC = mustParseMAC("22:22:22:22:22:22")
|
||||
nonExistingClientMAC = mustParseMAC("33:33:33:33:33:33")
|
||||
)
|
||||
|
||||
var (
|
||||
clientExisting = &client.Persistent{
|
||||
Name: "client",
|
||||
MACs: []net.HardwareAddr{cliMAC},
|
||||
}
|
||||
|
||||
clientAnotherExisting = &client.Persistent{
|
||||
Name: "another_client",
|
||||
MACs: []net.HardwareAddr{cliAnotherMAC},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*client.Persistent{
|
||||
clientExisting,
|
||||
clientAnotherExisting,
|
||||
}
|
||||
s := newStorage(t, clients)
|
||||
|
||||
testCases := []struct {
|
||||
want *client.Persistent
|
||||
name string
|
||||
clientMAC net.HardwareAddr
|
||||
}{{
|
||||
name: "existing",
|
||||
clientMAC: cliMAC,
|
||||
want: clientExisting,
|
||||
}, {
|
||||
name: "another_existing",
|
||||
clientMAC: cliAnotherMAC,
|
||||
want: clientAnotherExisting,
|
||||
}, {
|
||||
name: "non_existing",
|
||||
clientMAC: nonExistingClientMAC,
|
||||
want: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, ok := s.FindByMAC(tc.clientMAC)
|
||||
if tc.want == nil {
|
||||
assert.False(t, ok)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tc.want, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_Update(t *testing.T) {
|
||||
const (
|
||||
clientName = "client_name"
|
||||
@@ -1043,7 +1162,7 @@ func TestStorage_Update(t *testing.T) {
|
||||
Name: obstructingName,
|
||||
IPs: []netip.Addr{obstructingIP},
|
||||
Subnets: []netip.Prefix{obstructingSubnet},
|
||||
ClientIDs: []client.ClientID{obstructingClientID},
|
||||
ClientIDs: []string{obstructingClientID},
|
||||
}
|
||||
|
||||
clientToUpdate := &client.Persistent{
|
||||
@@ -1092,7 +1211,7 @@ func TestStorage_Update(t *testing.T) {
|
||||
name: "duplicate_client_id",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_client_id",
|
||||
ClientIDs: []client.ClientID{obstructingClientID},
|
||||
ClientIDs: []string{obstructingClientID},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `updating client: another client "obstructing_name" ` +
|
||||
@@ -1119,19 +1238,19 @@ func TestStorage_Update(t *testing.T) {
|
||||
func TestStorage_RangeByName(t *testing.T) {
|
||||
sortedClients := []*client.Persistent{{
|
||||
Name: "clientA",
|
||||
ClientIDs: []client.ClientID{"A"},
|
||||
ClientIDs: []string{"A"},
|
||||
}, {
|
||||
Name: "clientB",
|
||||
ClientIDs: []client.ClientID{"B"},
|
||||
ClientIDs: []string{"B"},
|
||||
}, {
|
||||
Name: "clientC",
|
||||
ClientIDs: []client.ClientID{"C"},
|
||||
ClientIDs: []string{"C"},
|
||||
}, {
|
||||
Name: "clientD",
|
||||
ClientIDs: []client.ClientID{"D"},
|
||||
ClientIDs: []string{"D"},
|
||||
}, {
|
||||
Name: "clientE",
|
||||
ClientIDs: []client.ClientID{"E"},
|
||||
ClientIDs: []string{"E"},
|
||||
}}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -1169,20 +1288,29 @@ func TestStorage_RangeByName(t *testing.T) {
|
||||
|
||||
func TestStorage_CustomUpstreamConfig(t *testing.T) {
|
||||
const (
|
||||
existingClientID = "existing_client_id"
|
||||
existingName = "existing_name"
|
||||
existingClientID = "existing_client_id"
|
||||
|
||||
nonExistingClientID = "non_existing_client_id"
|
||||
)
|
||||
|
||||
var (
|
||||
existingIP = netip.MustParseAddr("192.0.2.1")
|
||||
nonExistingIP = netip.MustParseAddr("192.0.2.255")
|
||||
existingClientUID = client.MustNewUID()
|
||||
existingIP = netip.MustParseAddr("192.0.2.1")
|
||||
|
||||
dhcpCliIP = netip.MustParseAddr("192.0.2.2")
|
||||
dhcpCliMAC = errors.Must(net.ParseMAC("02:00:00:00:00:00"))
|
||||
nonExistingIP = netip.MustParseAddr("192.0.2.255")
|
||||
|
||||
testUpstreamTimeout = time.Second
|
||||
)
|
||||
|
||||
existingClient := &client.Persistent{
|
||||
Name: existingName,
|
||||
IPs: []netip.Addr{existingIP},
|
||||
ClientIDs: []string{existingClientID},
|
||||
UID: existingClientUID,
|
||||
Upstreams: []string{"192.0.2.0"},
|
||||
}
|
||||
|
||||
date := time.Now()
|
||||
clock := &faketime.Clock{
|
||||
OnNow: func() (now time.Time) {
|
||||
@@ -1192,30 +1320,7 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ipToMAC := map[netip.Addr]net.HardwareAddr{
|
||||
dhcpCliIP: dhcpCliMAC,
|
||||
}
|
||||
|
||||
dhcp := &testDHCP{
|
||||
OnLeases: func() (ls []*dhcpsvc.Lease) {
|
||||
panic("not implemented")
|
||||
},
|
||||
OnHostBy: func(ip netip.Addr) (host string) {
|
||||
panic("not implemented")
|
||||
},
|
||||
OnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) {
|
||||
return ipToMAC[ip]
|
||||
},
|
||||
}
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
s, err := client.NewStorage(ctx, &client.StorageConfig{
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
Clock: clock,
|
||||
DHCP: dhcp,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s := newTestStorage(t, clock)
|
||||
s.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{
|
||||
UpstreamTimeout: testUpstreamTimeout,
|
||||
})
|
||||
@@ -1224,21 +1329,8 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
|
||||
return s.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
|
||||
})
|
||||
|
||||
err = s.Add(ctx, &client.Persistent{
|
||||
Name: "client_first",
|
||||
IPs: []netip.Addr{existingIP},
|
||||
ClientIDs: []client.ClientID{existingClientID},
|
||||
UID: client.MustNewUID(),
|
||||
Upstreams: []string{"192.0.2.0"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Add(ctx, &client.Persistent{
|
||||
Name: "client_second",
|
||||
MACs: []net.HardwareAddr{dhcpCliMAC},
|
||||
UID: client.MustNewUID(),
|
||||
Upstreams: []string{"192.0.2.0"},
|
||||
})
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
err := s.Add(ctx, existingClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -1256,11 +1348,6 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
|
||||
cliID: "",
|
||||
cliAddr: existingIP,
|
||||
wantNilConf: assert.NotNil,
|
||||
}, {
|
||||
name: "client_dhcp",
|
||||
cliID: "",
|
||||
cliAddr: dhcpCliIP,
|
||||
wantNilConf: assert.NotNil,
|
||||
}, {
|
||||
name: "non_existing_client_id",
|
||||
cliID: nonExistingClientID,
|
||||
@@ -1293,193 +1380,4 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
|
||||
|
||||
assert.NotEqual(t, conf, updConf)
|
||||
})
|
||||
|
||||
t.Run("same_custom_config", func(t *testing.T) {
|
||||
firstConf := s.CustomUpstreamConfig(existingClientID, existingIP)
|
||||
require.NotNil(t, firstConf)
|
||||
|
||||
secondConf := s.CustomUpstreamConfig(existingClientID, existingIP)
|
||||
require.NotNil(t, secondConf)
|
||||
|
||||
assert.Same(t, firstConf, secondConf)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkFindParams_Set(b *testing.B) {
|
||||
const (
|
||||
testIPStr = "192.0.2.1"
|
||||
testCIDRStr = "192.0.2.0/24"
|
||||
testMACStr = "02:00:00:00:00:00"
|
||||
testClientID = "clientid"
|
||||
)
|
||||
|
||||
benchCases := []struct {
|
||||
wantErr error
|
||||
params *client.FindParams
|
||||
name string
|
||||
id string
|
||||
}{{
|
||||
wantErr: nil,
|
||||
params: &client.FindParams{
|
||||
ClientID: testClientID,
|
||||
},
|
||||
name: "client_id",
|
||||
id: testClientID,
|
||||
}, {
|
||||
wantErr: nil,
|
||||
params: &client.FindParams{
|
||||
RemoteIP: netip.MustParseAddr(testIPStr),
|
||||
},
|
||||
name: "ip_address",
|
||||
id: testIPStr,
|
||||
}, {
|
||||
wantErr: nil,
|
||||
params: &client.FindParams{
|
||||
Subnet: netip.MustParsePrefix(testCIDRStr),
|
||||
},
|
||||
name: "subnet",
|
||||
id: testCIDRStr,
|
||||
}, {
|
||||
wantErr: nil,
|
||||
params: &client.FindParams{
|
||||
MAC: errors.Must(net.ParseMAC(testMACStr)),
|
||||
},
|
||||
name: "mac_address",
|
||||
id: testMACStr,
|
||||
}, {
|
||||
wantErr: client.ErrBadIdentifier,
|
||||
params: &client.FindParams{},
|
||||
name: "bad_id",
|
||||
id: "!@#$%^&*()_+",
|
||||
}}
|
||||
|
||||
for _, bc := range benchCases {
|
||||
b.Run(bc.name, func(b *testing.B) {
|
||||
params := &client.FindParams{}
|
||||
var err error
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
err = params.Set(bc.id)
|
||||
}
|
||||
|
||||
assert.ErrorIs(b, err, bc.wantErr)
|
||||
assert.Equal(b, bc.params, params)
|
||||
})
|
||||
}
|
||||
|
||||
// Most recent results:
|
||||
//
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/AdguardTeam/AdGuardHome/internal/client
|
||||
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
|
||||
// BenchmarkFindParams_Set/client_id-8 49463488 24.27 ns/op 0 B/op 0 allocs/op
|
||||
// BenchmarkFindParams_Set/ip_address-8 18740977 62.22 ns/op 0 B/op 0 allocs/op
|
||||
// BenchmarkFindParams_Set/subnet-8 10848192 110.0 ns/op 0 B/op 0 allocs/op
|
||||
// BenchmarkFindParams_Set/mac_address-8 8148494 133.2 ns/op 8 B/op 1 allocs/op
|
||||
// BenchmarkFindParams_Set/bad_id-8 73894278 16.29 ns/op 0 B/op 0 allocs/op
|
||||
}
|
||||
|
||||
func BenchmarkStorage_Find(b *testing.B) {
|
||||
const (
|
||||
cliID = "cid"
|
||||
cliMAC = "02:00:00:00:00:00"
|
||||
)
|
||||
|
||||
const (
|
||||
cliNameWithID = "client_with_id"
|
||||
cliNameWithIP = "client_with_ip"
|
||||
cliNameWithCIDR = "client_with_cidr"
|
||||
cliNameWithMAC = "client_with_mac"
|
||||
)
|
||||
|
||||
var (
|
||||
cliIP = netip.MustParseAddr("192.0.2.1")
|
||||
cliCIDR = netip.MustParsePrefix("192.0.2.0/24")
|
||||
)
|
||||
|
||||
var (
|
||||
clientWithID = &client.Persistent{
|
||||
Name: cliNameWithID,
|
||||
ClientIDs: []client.ClientID{cliID},
|
||||
}
|
||||
clientWithIP = &client.Persistent{
|
||||
Name: cliNameWithIP,
|
||||
IPs: []netip.Addr{cliIP},
|
||||
}
|
||||
clientWithCIDR = &client.Persistent{
|
||||
Name: cliNameWithCIDR,
|
||||
Subnets: []netip.Prefix{cliCIDR},
|
||||
}
|
||||
clientWithMAC = &client.Persistent{
|
||||
Name: cliNameWithMAC,
|
||||
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*client.Persistent{
|
||||
clientWithID,
|
||||
clientWithIP,
|
||||
clientWithCIDR,
|
||||
clientWithMAC,
|
||||
}
|
||||
s := newStorage(b, clients)
|
||||
|
||||
benchCases := []struct {
|
||||
params *client.FindParams
|
||||
name string
|
||||
wantName string
|
||||
}{{
|
||||
params: &client.FindParams{
|
||||
ClientID: cliID,
|
||||
},
|
||||
name: "client_id",
|
||||
wantName: cliNameWithID,
|
||||
}, {
|
||||
params: &client.FindParams{
|
||||
RemoteIP: cliIP,
|
||||
},
|
||||
name: "ip_address",
|
||||
wantName: cliNameWithIP,
|
||||
}, {
|
||||
params: &client.FindParams{
|
||||
Subnet: cliCIDR,
|
||||
},
|
||||
name: "subnet",
|
||||
wantName: cliNameWithCIDR,
|
||||
}, {
|
||||
params: &client.FindParams{
|
||||
MAC: errors.Must(net.ParseMAC(cliMAC)),
|
||||
},
|
||||
name: "mac_address",
|
||||
wantName: cliNameWithMAC,
|
||||
}}
|
||||
|
||||
for _, bc := range benchCases {
|
||||
b.Run(bc.name, func(b *testing.B) {
|
||||
var p *client.Persistent
|
||||
var ok bool
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
p, ok = s.Find(bc.params)
|
||||
}
|
||||
|
||||
assert.True(b, ok)
|
||||
assert.NotNil(b, p)
|
||||
assert.Equal(b, bc.wantName, p.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// Most recent results:
|
||||
//
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/AdguardTeam/AdGuardHome/internal/client
|
||||
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
|
||||
// BenchmarkStorage_Find/client_id-8 7070107 154.4 ns/op 240 B/op 2 allocs/op
|
||||
// BenchmarkStorage_Find/ip_address-8 6831823 168.6 ns/op 248 B/op 2 allocs/op
|
||||
// BenchmarkStorage_Find/subnet-8 7209050 167.5 ns/op 256 B/op 2 allocs/op
|
||||
// BenchmarkStorage_Find/mac_address-8 5776131 199.7 ns/op 256 B/op 3 allocs/op
|
||||
}
|
||||
|
||||
@@ -138,7 +138,6 @@ func (m *upstreamManager) customUpstreamConfig(uid UID) (proxyConf *proxy.Custom
|
||||
|
||||
proxyConf = newCustomUpstreamConfig(cliConf, m.commonConf)
|
||||
cliConf.proxyConf = proxyConf
|
||||
cliConf.commonConfUpdate = m.confUpdate
|
||||
cliConf.isChanged = false
|
||||
|
||||
return proxyConf
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testLocalTLD is a common local TLD for tests.
|
||||
@@ -54,3 +56,11 @@ var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// mustParseMAC parses a hardware address from s and requires no errors.
|
||||
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
|
||||
mac, err := net.ParseMAC(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
return mac
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -178,9 +176,9 @@ func TestDHCPServer_AddLease(t *testing.T) {
|
||||
newIP = netip.MustParseAddr("192.168.0.3")
|
||||
newIPv6 = netip.MustParseAddr("2001:db8::2")
|
||||
|
||||
existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
|
||||
newMAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
|
||||
ipv6MAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
|
||||
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
newMAC = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07")
|
||||
)
|
||||
|
||||
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
|
||||
@@ -293,9 +291,9 @@ func TestDHCPServer_index(t *testing.T) {
|
||||
ip3 = netip.MustParseAddr("172.16.0.3")
|
||||
ip4 = netip.MustParseAddr("172.16.0.4")
|
||||
|
||||
mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
|
||||
mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
|
||||
mac3 = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
|
||||
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
mac3 = mustParseMAC(t, "02:03:04:05:06:07")
|
||||
)
|
||||
|
||||
t.Run("ip_idx", func(t *testing.T) {
|
||||
@@ -351,9 +349,9 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
||||
ip3 = netip.MustParseAddr("192.168.0.4")
|
||||
ip4 = netip.MustParseAddr("2001:db8::3")
|
||||
|
||||
mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
|
||||
mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
|
||||
mac3 = errors.Must(net.ParseMAC("06:05:04:03:02:02"))
|
||||
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
mac3 = mustParseMAC(t, "06:05:04:03:02:02")
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -454,9 +452,9 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
|
||||
newIP = netip.MustParseAddr("192.168.0.3")
|
||||
newIPv6 = netip.MustParseAddr("2001:db8::2")
|
||||
|
||||
existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
|
||||
newMAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
|
||||
ipv6MAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
|
||||
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
newMAC = mustParseMAC(t, "02:03:04:05:06:07")
|
||||
ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -561,13 +559,13 @@ func TestServer_Leases(t *testing.T) {
|
||||
Expiry: expiry,
|
||||
IP: netip.MustParseAddr("192.168.0.3"),
|
||||
Hostname: "example.host",
|
||||
HWAddr: errors.Must(net.ParseMAC("AA:AA:AA:AA:AA:AA")),
|
||||
HWAddr: mustParseMAC(t, "AA:AA:AA:AA:AA:AA"),
|
||||
IsStatic: false,
|
||||
}, {
|
||||
Expiry: time.Time{},
|
||||
IP: netip.MustParseAddr("192.168.0.4"),
|
||||
Hostname: "example.static.host",
|
||||
HWAddr: errors.Must(net.ParseMAC("BB:BB:BB:BB:BB:BB")),
|
||||
HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"),
|
||||
IsStatic: true,
|
||||
}}
|
||||
assert.ElementsMatch(t, wantLeases, srv.Leases())
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"github.com/AdguardTeam/golibs/container"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
@@ -52,7 +51,7 @@ func processAccessClients(
|
||||
} else if ipnet, err = netip.ParsePrefix(s); err == nil {
|
||||
*nets = append(*nets, ipnet)
|
||||
} else {
|
||||
err = client.ValidateClientID(s)
|
||||
err = ValidateClientID(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value %q at index %d: bad ip, cidr, or clientid", s, i)
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||
return "", nil
|
||||
}
|
||||
|
||||
hostSrvName := s.conf.TLSConf.ServerName
|
||||
hostSrvName := s.conf.ServerName
|
||||
if hostSrvName == "" {
|
||||
return "", nil
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||
clientID, err = clientIDFromClientServerName(
|
||||
hostSrvName,
|
||||
cliSrvName,
|
||||
s.conf.TLSConf.StrictSNICheck,
|
||||
s.conf.StrictSNICheck,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("clientid check: %w", err)
|
||||
|
||||
@@ -121,7 +121,7 @@ func TestServer_HandleBefore_tls(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s, _ := createTestTLS(t, &TLSConfig{
|
||||
s, _ := createTestTLS(t, TLSConfig{
|
||||
TLSListenAddrs: []*net.TCPAddr{{}},
|
||||
ServerName: tlsServerName,
|
||||
})
|
||||
@@ -259,7 +259,6 @@ func TestServer_HandleBefore_udp(t *testing.T) {
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
AllowedClients: tc.allowedClients,
|
||||
DisallowedClients: tc.disallowedClients,
|
||||
|
||||
@@ -7,13 +7,26 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
// ValidateClientID returns an error if id is not a valid ClientID.
|
||||
//
|
||||
// Keep in sync with [client.ValidateClientID].
|
||||
func ValidateClientID(id string) (err error) {
|
||||
err = netutil.ValidateHostnameLabel(id)
|
||||
if err != nil {
|
||||
// Replace the domain name label wrapper with our own.
|
||||
return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clientIDFromClientServerName extracts and validates a ClientID. hostSrvName
|
||||
// is the server name of the host. cliSrvName is the server name as sent by the
|
||||
// client. When strict is true, and client and host server name don't match,
|
||||
@@ -40,7 +53,7 @@ func clientIDFromClientServerName(
|
||||
}
|
||||
|
||||
clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1]
|
||||
err = client.ValidateClientID(clientID)
|
||||
err = ValidateClientID(clientID)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return "", err
|
||||
@@ -80,7 +93,7 @@ func clientIDFromDNSContextHTTPS(pctx *proxy.DNSContext) (clientID string, err e
|
||||
return "", fmt.Errorf("clientid check: invalid path %q: extra parts", origPath)
|
||||
}
|
||||
|
||||
err = client.ValidateClientID(clientID)
|
||||
err = ValidateClientID(clientID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("clientid check: %w", err)
|
||||
}
|
||||
|
||||
@@ -212,13 +212,13 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tlsConf := &TLSConfig{
|
||||
tlsConf := TLSConfig{
|
||||
ServerName: tc.confSrvName,
|
||||
StrictSNICheck: tc.strictSNI,
|
||||
}
|
||||
|
||||
srv := &Server{
|
||||
conf: ServerConfig{TLSConf: tlsConf},
|
||||
conf: ServerConfig{TLSConfig: tlsConf},
|
||||
baseLogger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||
@@ -167,34 +168,43 @@ type EDNSClientSubnet struct {
|
||||
UseCustom bool `yaml:"use_custom"`
|
||||
}
|
||||
|
||||
// TLSConfig contains the TLS configuration settings for DNS-over-HTTPS (DoH),
|
||||
// DNS-over-TLS (DoT), DNS-over-QUIC (DoQ), and Discovery of Designated
|
||||
// Resolvers (DDR).
|
||||
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
|
||||
type TLSConfig struct {
|
||||
// Cert is the TLS certificate used for TLS connections. It is nil if
|
||||
// encryption is disabled.
|
||||
Cert *tls.Certificate
|
||||
cert tls.Certificate
|
||||
|
||||
// TLSListenAddrs are the addresses to listen on for DoT connections. Each
|
||||
// item in the list must be non-nil if Cert is not nil.
|
||||
TLSListenAddrs []*net.TCPAddr
|
||||
TLSListenAddrs []*net.TCPAddr `yaml:"-" json:"-"`
|
||||
QUICListenAddrs []*net.UDPAddr `yaml:"-" json:"-"`
|
||||
HTTPSListenAddrs []*net.TCPAddr `yaml:"-" json:"-"`
|
||||
|
||||
// QUICListenAddrs are the addresses to listen on for DoQ connections. Each
|
||||
// item in the list must be non-nil if Cert is not nil.
|
||||
QUICListenAddrs []*net.UDPAddr
|
||||
// PEM-encoded certificates chain
|
||||
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"`
|
||||
// PEM-encoded private key
|
||||
PrivateKey string `yaml:"private_key" json:"private_key"`
|
||||
|
||||
// HTTPSListenAddrs should be the addresses AdGuard Home is listening on for
|
||||
// DoH connections. These addresses are announced with DDR. Each item in
|
||||
// the list must be non-nil.
|
||||
HTTPSListenAddrs []*net.TCPAddr
|
||||
CertificatePath string `yaml:"certificate_path" json:"certificate_path"`
|
||||
PrivateKeyPath string `yaml:"private_key_path" json:"private_key_path"`
|
||||
|
||||
CertificateChainData []byte `yaml:"-" json:"-"`
|
||||
PrivateKeyData []byte `yaml:"-" json:"-"`
|
||||
|
||||
// ServerName is the hostname of the server. Currently, it is only being
|
||||
// used for ClientID checking and Discovery of Designated Resolvers (DDR).
|
||||
ServerName string
|
||||
ServerName string `yaml:"-" json:"-"`
|
||||
|
||||
// DNS names from certificate (SAN) or CN value from Subject
|
||||
dnsNames []string
|
||||
|
||||
// OverrideTLSCiphers, when set, contains the names of the cipher suites to
|
||||
// use. If the slice is empty, the default safe suites are used.
|
||||
OverrideTLSCiphers []string `yaml:"override_tls_ciphers,omitempty" json:"-"`
|
||||
|
||||
// StrictSNICheck controls if the connections with SNI mismatching the
|
||||
// certificate's ones should be rejected.
|
||||
StrictSNICheck bool
|
||||
StrictSNICheck bool `yaml:"strict_sni_check" json:"-"`
|
||||
|
||||
// hasIPAddrs is set during the certificate parsing and is true if the
|
||||
// configured certificate contains at least a single IP address.
|
||||
hasIPAddrs bool
|
||||
}
|
||||
|
||||
// DNSCryptConfig is the DNSCrypt server configuration struct.
|
||||
@@ -229,11 +239,8 @@ type ServerConfig struct {
|
||||
// Remove that.
|
||||
AddrProcConf *client.DefaultAddrProcConfig
|
||||
|
||||
// TLSConf is the TLS configuration for DNS-over-TLS, DNS-over-QUIC, and
|
||||
// HTTPS. It must not be nil.
|
||||
TLSConf *TLSConfig
|
||||
|
||||
Config
|
||||
TLSConfig
|
||||
DNSCryptConfig
|
||||
TLSAllowUnencryptedDoH bool
|
||||
|
||||
@@ -274,10 +281,6 @@ type ServerConfig struct {
|
||||
|
||||
// ServePlainDNS defines if plain DNS is allowed for incoming requests.
|
||||
ServePlainDNS bool
|
||||
|
||||
// PendingRequestsEnabled defines if duplicate requests should be forwarded
|
||||
// to upstreams along with the original one.
|
||||
PendingRequestsEnabled bool
|
||||
}
|
||||
|
||||
// UpstreamMode is a enumeration of upstream mode representations. See
|
||||
@@ -321,9 +324,6 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
|
||||
UsePrivateRDNS: srvConf.UsePrivateRDNS,
|
||||
PrivateSubnets: s.privateNets,
|
||||
MessageConstructor: s,
|
||||
PendingRequests: &proxy.PendingRequestsConfig{
|
||||
Enabled: srvConf.PendingRequestsEnabled,
|
||||
},
|
||||
}
|
||||
|
||||
if srvConf.EDNSClientSubnet.UseCustom {
|
||||
@@ -608,33 +608,45 @@ func (conf *ServerConfig) ourAddrsSet() (m addrPortSet, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// prepareTLS sets up the TLS configuration for the DNS proxy.
|
||||
// prepareTLS - prepares TLS configuration for the DNS proxy
|
||||
func (s *Server) prepareTLS(proxyConfig *proxy.Config) (err error) {
|
||||
if s.conf.TLSConf.Cert == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if s.conf.TLSConf.TLSListenAddrs == nil && s.conf.TLSConf.QUICListenAddrs == nil {
|
||||
if len(s.conf.CertificateChainData) == 0 || len(s.conf.PrivateKeyData) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
proxyConfig.TLSListenAddr = s.conf.TLSConf.TLSListenAddrs
|
||||
proxyConfig.QUICListenAddr = s.conf.TLSConf.QUICListenAddrs
|
||||
if s.conf.TLSListenAddrs == nil && s.conf.QUICListenAddrs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(s.conf.TLSConf.Cert.Certificate[0])
|
||||
proxyConfig.TLSListenAddr = aghalg.CoalesceSlice(
|
||||
s.conf.TLSListenAddrs,
|
||||
proxyConfig.TLSListenAddr,
|
||||
)
|
||||
|
||||
proxyConfig.QUICListenAddr = aghalg.CoalesceSlice(
|
||||
s.conf.QUICListenAddrs,
|
||||
proxyConfig.QUICListenAddr,
|
||||
)
|
||||
|
||||
s.conf.cert, err = tls.X509KeyPair(s.conf.CertificateChainData, s.conf.PrivateKeyData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse TLS keypair: %w", err)
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(s.conf.cert.Certificate[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("x509.ParseCertificate(): %w", err)
|
||||
}
|
||||
|
||||
s.hasIPAddrs = aghtls.CertificateHasIP(cert)
|
||||
s.conf.hasIPAddrs = aghtls.CertificateHasIP(cert)
|
||||
|
||||
if s.conf.TLSConf.StrictSNICheck {
|
||||
if s.conf.StrictSNICheck {
|
||||
if len(cert.DNSNames) != 0 {
|
||||
s.dnsNames = cert.DNSNames
|
||||
s.conf.dnsNames = cert.DNSNames
|
||||
log.Debug("dns: using certificate's SAN as DNS names: %v", cert.DNSNames)
|
||||
slices.Sort(s.dnsNames)
|
||||
slices.Sort(s.conf.dnsNames)
|
||||
} else {
|
||||
s.dnsNames = []string{cert.Subject.CommonName}
|
||||
s.conf.dnsNames = append(s.conf.dnsNames, cert.Subject.CommonName)
|
||||
log.Debug("dns: using certificate's CN as DNS name: %s", cert.Subject.CommonName)
|
||||
}
|
||||
}
|
||||
@@ -683,11 +695,11 @@ func anyNameMatches(dnsNames []string, sni string) (ok bool) {
|
||||
// Called by 'tls' package when Client Hello is received
|
||||
// If the server name (from SNI) supplied by client is incorrect - we terminate the ongoing TLS handshake.
|
||||
func (s *Server) onGetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if s.conf.TLSConf.StrictSNICheck && !anyNameMatches(s.dnsNames, ch.ServerName) {
|
||||
if s.conf.StrictSNICheck && !anyNameMatches(s.conf.dnsNames, ch.ServerName) {
|
||||
log.Info("dns: tls: unknown SNI in Client Hello: %s", ch.ServerName)
|
||||
return nil, fmt.Errorf("invalid SNI")
|
||||
}
|
||||
return s.conf.TLSConf.Cert, nil
|
||||
return &s.conf.cert, nil
|
||||
}
|
||||
|
||||
// preparePlain prepares the plain-DNS configuration for the DNS proxy.
|
||||
|
||||
@@ -296,7 +296,6 @@ func TestServer_HandleDNSRequest_dns64(t *testing.T) {
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
UseDNS64: true,
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
@@ -336,7 +335,6 @@ func TestServer_dns64WithDisabledRDNS(t *testing.T) {
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
UseDNS64: true,
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
|
||||
@@ -103,26 +103,16 @@ type SystemResolvers interface {
|
||||
//
|
||||
// The zero Server is empty and ready for use.
|
||||
type Server struct {
|
||||
// addrProc, if not nil, is used to process clients' IP addresses with rDNS,
|
||||
// WHOIS, etc.
|
||||
addrProc client.AddressProcessor
|
||||
// dnsProxy is the DNS proxy for forwarding client's DNS requests.
|
||||
dnsProxy *proxy.Proxy
|
||||
|
||||
// bootstrap is the resolver for upstreams' hostnames.
|
||||
bootstrap upstream.Resolver
|
||||
|
||||
// clientIDCache is a temporary storage for ClientIDs that were extracted
|
||||
// during the BeforeRequestHandler stage.
|
||||
clientIDCache cache.Cache
|
||||
// dnsFilter is the DNS filter for filtering client's DNS requests and
|
||||
// responses.
|
||||
dnsFilter *filtering.DNSFilter
|
||||
|
||||
// dhcpServer is the DHCP server for accessing lease data.
|
||||
dhcpServer DHCP
|
||||
|
||||
// etcHosts contains the current data from the system's hosts files.
|
||||
etcHosts upstream.Resolver
|
||||
|
||||
// privateNets is the configured set of IP networks considered private.
|
||||
privateNets netutil.SubnetSet
|
||||
|
||||
// queryLog is the query log for client's DNS requests, responses and
|
||||
// filtering results.
|
||||
queryLog querylog.QueryLog
|
||||
@@ -130,43 +120,37 @@ type Server struct {
|
||||
// stats is the statistics collector for client's DNS usage data.
|
||||
stats stats.Interface
|
||||
|
||||
// sysResolvers used to fetch system resolvers to use by default for private
|
||||
// PTR resolving.
|
||||
sysResolvers SystemResolvers
|
||||
|
||||
// access drops disallowed clients.
|
||||
access *accessManager
|
||||
|
||||
// anonymizer masks the client's IP addresses if needed.
|
||||
anonymizer *aghnet.IPMut
|
||||
|
||||
// baseLogger is used to create loggers for other entities. It should not
|
||||
// have a prefix and must not be nil.
|
||||
baseLogger *slog.Logger
|
||||
|
||||
// dnsFilter is the DNS filter for filtering client's DNS requests and
|
||||
// responses.
|
||||
dnsFilter *filtering.DNSFilter
|
||||
|
||||
// dnsProxy is the DNS proxy for forwarding client's DNS requests.
|
||||
dnsProxy *proxy.Proxy
|
||||
|
||||
// internalProxy resolves internal requests from the application itself. It
|
||||
// isn't started and so no listen ports are required.
|
||||
internalProxy *proxy.Proxy
|
||||
// localDomainSuffix is the suffix used to detect internal hosts. It
|
||||
// must be a valid domain name plus dots on each side.
|
||||
localDomainSuffix string
|
||||
|
||||
// ipset processes DNS requests using ipset data. It must not be nil after
|
||||
// initialization. See [newIpsetHandler].
|
||||
ipset *ipsetHandler
|
||||
|
||||
// dns64Pref is the NAT64 prefix used for DNS64 response mapping. The major
|
||||
// part of DNS64 happens inside the [proxy] package, but there still are
|
||||
// some places where response mapping is needed (e.g. DHCP).
|
||||
dns64Pref netip.Prefix
|
||||
// privateNets is the configured set of IP networks considered private.
|
||||
privateNets netutil.SubnetSet
|
||||
|
||||
// localDomainSuffix is the suffix used to detect internal hosts. It
|
||||
// must be a valid domain name plus dots on each side.
|
||||
localDomainSuffix string
|
||||
// addrProc, if not nil, is used to process clients' IP addresses with rDNS,
|
||||
// WHOIS, etc.
|
||||
addrProc client.AddressProcessor
|
||||
|
||||
// sysResolvers used to fetch system resolvers to use by default for private
|
||||
// PTR resolving.
|
||||
sysResolvers SystemResolvers
|
||||
|
||||
// etcHosts contains the current data from the system's hosts files.
|
||||
etcHosts upstream.Resolver
|
||||
|
||||
// bootstrap is the resolver for upstreams' hostnames.
|
||||
bootstrap upstream.Resolver
|
||||
|
||||
// bootResolvers are the resolvers that should be used for
|
||||
// bootstrapping along with [etcHosts].
|
||||
@@ -175,26 +159,34 @@ type Server struct {
|
||||
// [upstream.Resolver] interface.
|
||||
bootResolvers []*upstream.UpstreamResolver
|
||||
|
||||
// dnsNames are the DNS names from certificate (SAN) or CN value from
|
||||
// Subject.
|
||||
dnsNames []string
|
||||
// dns64Pref is the NAT64 prefix used for DNS64 response mapping. The major
|
||||
// part of DNS64 happens inside the [proxy] package, but there still are
|
||||
// some places where response mapping is needed (e.g. DHCP).
|
||||
dns64Pref netip.Prefix
|
||||
|
||||
// anonymizer masks the client's IP addresses if needed.
|
||||
anonymizer *aghnet.IPMut
|
||||
|
||||
// clientIDCache is a temporary storage for ClientIDs that were extracted
|
||||
// during the BeforeRequestHandler stage.
|
||||
clientIDCache cache.Cache
|
||||
|
||||
// internalProxy resolves internal requests from the application itself. It
|
||||
// isn't started and so no listen ports are required.
|
||||
internalProxy *proxy.Proxy
|
||||
|
||||
// isRunning is true if the DNS server is running.
|
||||
isRunning bool
|
||||
|
||||
// protectionUpdateInProgress is used to make sure that only one goroutine
|
||||
// updating the protection configuration after a pause is running at a time.
|
||||
protectionUpdateInProgress atomic.Bool
|
||||
|
||||
// conf is the current configuration of the server.
|
||||
conf ServerConfig
|
||||
|
||||
// serverLock protects Server.
|
||||
serverLock sync.RWMutex
|
||||
|
||||
// protectionUpdateInProgress is used to make sure that only one goroutine
|
||||
// updating the protection configuration after a pause is running at a time.
|
||||
protectionUpdateInProgress atomic.Bool
|
||||
|
||||
// isRunning is true if the DNS server is running.
|
||||
isRunning bool
|
||||
|
||||
// hasIPAddrs is set during the certificate parsing and is true if the
|
||||
// configured certificate contains at least a single IP address.
|
||||
hasIPAddrs bool
|
||||
}
|
||||
|
||||
// defaultLocalDomainSuffix is the default suffix used to detect internal hosts
|
||||
|
||||
@@ -213,23 +213,17 @@ func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) {
|
||||
}, certPem, keyPem
|
||||
}
|
||||
|
||||
func createTestTLS(t *testing.T, tlsConf *TLSConfig) (s *Server, certPem []byte) {
|
||||
func createTestTLS(t *testing.T, tlsConf TLSConfig) (s *Server, certPem []byte) {
|
||||
t.Helper()
|
||||
|
||||
var keyPem []byte
|
||||
_, certPem, keyPem = createServerTLSConfig(t)
|
||||
|
||||
cert, err := tls.X509KeyPair(certPem, keyPem)
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsConf.Cert = &cert
|
||||
|
||||
s = createTestServer(t, &filtering.Config{
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: tlsConf,
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
@@ -238,7 +232,10 @@ func createTestTLS(t *testing.T, tlsConf *TLSConfig) (s *Server, certPem []byte)
|
||||
ServePlainDNS: true,
|
||||
})
|
||||
|
||||
err = s.Prepare(&s.conf)
|
||||
tlsConf.CertificateChainData, tlsConf.PrivateKeyData = certPem, keyPem
|
||||
s.conf.TLSConfig = tlsConf
|
||||
|
||||
err := s.Prepare(&s.conf)
|
||||
require.NoErrorf(t, err, "failed to prepare server: %s", err)
|
||||
|
||||
return s, certPem
|
||||
@@ -357,7 +354,6 @@ func TestServer(t *testing.T) {
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
@@ -399,7 +395,6 @@ func TestServer_timeout(t *testing.T) {
|
||||
t.Run("custom", func(t *testing.T) {
|
||||
srvConf := &ServerConfig{
|
||||
UpstreamTimeout: testTimeout,
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
@@ -427,7 +422,6 @@ func TestServer_timeout(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s.conf.TLSConf = &TLSConfig{}
|
||||
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
|
||||
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
@@ -442,7 +436,6 @@ func TestServer_timeout(t *testing.T) {
|
||||
|
||||
func TestServer_Prepare_fallbacks(t *testing.T) {
|
||||
srvConf := &ServerConfig{
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
FallbackDNS: []string{
|
||||
"#tls://1.1.1.1",
|
||||
@@ -473,7 +466,6 @@ func TestServerWithProtectionDisabled(t *testing.T) {
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
@@ -495,7 +487,7 @@ func TestServerWithProtectionDisabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDoTServer(t *testing.T) {
|
||||
s, certPem := createTestTLS(t, &TLSConfig{
|
||||
s, certPem := createTestTLS(t, TLSConfig{
|
||||
TLSListenAddrs: []*net.TCPAddr{{}},
|
||||
})
|
||||
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
|
||||
@@ -519,7 +511,7 @@ func TestDoTServer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDoQServer(t *testing.T) {
|
||||
s, _ := createTestTLS(t, &TLSConfig{
|
||||
s, _ := createTestTLS(t, TLSConfig{
|
||||
QUICListenAddrs: []*net.UDPAddr{{IP: net.IP{127, 0, 0, 1}}},
|
||||
})
|
||||
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
|
||||
@@ -604,7 +596,6 @@ func TestSafeSearch(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -699,7 +690,6 @@ func TestInvalidRequest(t *testing.T) {
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -731,7 +721,6 @@ func TestBlockedRequest(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -769,7 +758,6 @@ func TestServerCustomClientUpstream(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
CacheSize: defaultCacheSize,
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
@@ -850,7 +838,6 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) {
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -886,7 +873,6 @@ func TestBlockCNAME(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -961,7 +947,6 @@ func TestClientRulesForCNAMEMatching(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -1009,7 +994,6 @@ func TestNullBlockedRequest(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -1080,7 +1064,6 @@ func TestBlockedCustomIP(t *testing.T) {
|
||||
conf := &ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
@@ -1136,7 +1119,6 @@ func TestBlockedByHosts(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -1190,7 +1172,6 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
@@ -1254,7 +1235,6 @@ func TestRewrite(t *testing.T) {
|
||||
assert.NoError(t, s.Prepare(&ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{"8.8.8.8:53"},
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
@@ -1389,7 +1369,6 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
||||
s.conf.UDPListenAddrs = []*net.UDPAddr{{}}
|
||||
s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
|
||||
s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
|
||||
s.conf.TLSConf = &TLSConfig{}
|
||||
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
|
||||
s.conf.Config.ClientsContainer = EmptyClientsContainer{}
|
||||
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
|
||||
@@ -1478,7 +1457,6 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
||||
s.conf.UDPListenAddrs = []*net.UDPAddr{{}}
|
||||
s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
|
||||
s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
|
||||
s.conf.TLSConf = &TLSConfig{}
|
||||
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
|
||||
s.conf.Config.ClientsContainer = EmptyClientsContainer{}
|
||||
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
|
||||
@@ -1745,7 +1723,6 @@ func TestServer_Exchange(t *testing.T) {
|
||||
srv := createTestServer(t, &filtering.Config{
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
}, ServerConfig{
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{upsAddr},
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
@@ -1769,7 +1746,6 @@ func TestServer_Exchange(t *testing.T) {
|
||||
srv := createTestServer(t, &filtering.Config{
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
}, ServerConfig{
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{upsAddr},
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
|
||||
@@ -37,7 +37,6 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
srv := createTestServer(t, &filtering.Config{
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
}, ServerConfig{
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
|
||||
@@ -31,7 +31,6 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
|
||||
@@ -76,7 +76,6 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{},
|
||||
TCPListenAddrs: []*net.TCPAddr{},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
FallbackDNS: []string{"9.9.9.10"},
|
||||
@@ -160,7 +159,6 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
forwardConf := ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{},
|
||||
TCPListenAddrs: []*net.TCPAddr{},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
RatelimitSubnetLenIPv4: 24,
|
||||
@@ -371,7 +369,6 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
UpstreamTimeout: upsTimeout,
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
|
||||
@@ -246,9 +246,9 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
|
||||
|
||||
// TODO(e.burkov): Think about storing the FQDN version of the server's
|
||||
// name somewhere.
|
||||
domainName := dns.Fqdn(s.conf.TLSConf.ServerName)
|
||||
domainName := dns.Fqdn(s.conf.ServerName)
|
||||
|
||||
for _, addr := range s.conf.TLSConf.HTTPSListenAddrs {
|
||||
for _, addr := range s.conf.HTTPSListenAddrs {
|
||||
values := []dns.SVCBKeyValue{
|
||||
&dns.SVCBAlpn{Alpn: []string{"h2"}},
|
||||
&dns.SVCBPort{Port: uint16(addr.Port)},
|
||||
@@ -265,7 +265,7 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
|
||||
resp.Answer = append(resp.Answer, ans)
|
||||
}
|
||||
|
||||
if s.hasIPAddrs {
|
||||
if s.conf.hasIPAddrs {
|
||||
// Only add DNS-over-TLS resolvers in case the certificate contains IP
|
||||
// addresses.
|
||||
//
|
||||
|
||||
@@ -3,7 +3,6 @@ package dnsforward
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
@@ -78,7 +77,6 @@ func TestServer_ProcessInitial(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := ServerConfig{
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
AAAADisabled: tc.aaaaDisabled,
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
@@ -179,7 +177,6 @@ func TestServer_ProcessFilteringAfterResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := ServerConfig{
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
AAAADisabled: tc.aaaaDisabled,
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
@@ -319,8 +316,6 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
|
||||
}}
|
||||
|
||||
_, certPem, keyPem := createServerTLSConfig(t)
|
||||
cert, err := tls.X509KeyPair(certPem, keyPem)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -333,18 +328,19 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
ClientsContainer: EmptyClientsContainer{},
|
||||
},
|
||||
TLSConf: &TLSConfig{
|
||||
ServerName: ddrTestDomainName,
|
||||
Cert: &cert,
|
||||
TLSListenAddrs: tc.addrsDoT,
|
||||
HTTPSListenAddrs: tc.addrsDoH,
|
||||
QUICListenAddrs: tc.addrsDoQ,
|
||||
TLSConfig: TLSConfig{
|
||||
ServerName: ddrTestDomainName,
|
||||
CertificateChainData: certPem,
|
||||
PrivateKeyData: keyPem,
|
||||
TLSListenAddrs: tc.addrsDoT,
|
||||
HTTPSListenAddrs: tc.addrsDoH,
|
||||
QUICListenAddrs: tc.addrsDoQ,
|
||||
},
|
||||
ServePlainDNS: true,
|
||||
})
|
||||
// TODO(e.burkov): Generate a certificate actually containing the
|
||||
// IP addresses.
|
||||
s.hasIPAddrs = true
|
||||
s.conf.hasIPAddrs = true
|
||||
|
||||
req := createTestMessageWithType(tc.host, tc.qtype)
|
||||
|
||||
@@ -661,7 +657,6 @@ func TestServer_HandleDNSRequest_restrictLocal(t *testing.T) {
|
||||
}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
// TODO(s.chzhen): Add tests where EDNSClientSubnet.Enabled is true.
|
||||
// Improve Config declaration for tests.
|
||||
Config: Config{
|
||||
@@ -794,7 +789,6 @@ func TestServer_ProcessUpstream_localPTR(t *testing.T) {
|
||||
ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
@@ -824,7 +818,6 @@ func TestServer_ProcessUpstream_localPTR(t *testing.T) {
|
||||
ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
|
||||
@@ -16,7 +16,6 @@ func TestGenAnswerHTTPS_andSVCB(t *testing.T) {
|
||||
s := createTestServer(t, &filtering.Config{
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
}, ServerConfig{
|
||||
TLSConf: &TLSConfig{},
|
||||
Config: Config{
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
|
||||
@@ -78,11 +78,11 @@ func TestHostnameToHashes(t *testing.T) {
|
||||
wantLen: 2,
|
||||
}, {
|
||||
name: "private_domain_v2",
|
||||
host: "foo.dyndns.org",
|
||||
wantLen: 3,
|
||||
host: "foo.blogspot.co.uk",
|
||||
wantLen: 4,
|
||||
}, {
|
||||
name: "sub_private_domain_v2",
|
||||
host: "bar.foo.dyndns.org",
|
||||
host: "bar.foo.blogspot.co.uk",
|
||||
wantLen: 4,
|
||||
}}
|
||||
|
||||
|
||||
@@ -28,10 +28,6 @@ type clientsContainer struct {
|
||||
// filter. It must not be nil.
|
||||
baseLogger *slog.Logger
|
||||
|
||||
// logger is used for logging the operation of the client container. It
|
||||
// must not be nil.
|
||||
logger *slog.Logger
|
||||
|
||||
// storage stores information about persistent clients.
|
||||
storage *client.Storage
|
||||
|
||||
@@ -62,7 +58,6 @@ type clientsContainer struct {
|
||||
// BlockedClientChecker checks if a client is blocked by the current access
|
||||
// settings.
|
||||
type BlockedClientChecker interface {
|
||||
// TODO(s.chzhen): Accept [client.FindParams].
|
||||
IsBlockedClient(ip netip.Addr, clientID string) (blocked bool, rule string)
|
||||
}
|
||||
|
||||
@@ -85,7 +80,6 @@ func (clients *clientsContainer) Init(
|
||||
}
|
||||
|
||||
clients.baseLogger = baseLogger
|
||||
clients.logger = baseLogger.With(slogutil.KeyPrefix, "client_container")
|
||||
clients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize
|
||||
clients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime)
|
||||
|
||||
@@ -275,7 +269,7 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
|
||||
|
||||
BlockedServices: cli.BlockedServices.Clone(),
|
||||
|
||||
IDs: cli.Identifiers(),
|
||||
IDs: cli.IDs(),
|
||||
Tags: slices.Clone(cli.Tags),
|
||||
Upstreams: slices.Clone(cli.Upstreams),
|
||||
|
||||
@@ -362,27 +356,15 @@ func (clients *clientsContainer) clientOrArtificial(
|
||||
}, true
|
||||
}
|
||||
|
||||
// shouldCountClient is a wrapper around [client.Storage.Find] to make it a
|
||||
// shouldCountClient is a wrapper around [clientsContainer.find] to make it a
|
||||
// valid client information finder for the statistics. If no information about
|
||||
// the client is found, it returns true. Values of ids must be either a valid
|
||||
// ClientID or a valid IP address.
|
||||
//
|
||||
// TODO(s.chzhen): Accept [client.FindParams].
|
||||
// the client is found, it returns true.
|
||||
func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
params := &client.FindParams{}
|
||||
for _, id := range ids {
|
||||
err := params.Set(id)
|
||||
if err != nil {
|
||||
// Should not happen.
|
||||
clients.logger.Warn("parsing find params", slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
client, ok := clients.storage.Find(params)
|
||||
client, ok := clients.storage.Find(id)
|
||||
if ok {
|
||||
return !client.IgnoreStatistics
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ func clientToJSON(c *client.Persistent) (cj *clientJSON) {
|
||||
|
||||
return &clientJSON{
|
||||
Name: c.Name,
|
||||
IDs: c.Identifiers(),
|
||||
IDs: c.IDs(),
|
||||
Tags: c.Tags,
|
||||
UseGlobalSettings: !c.UseOwnSettings,
|
||||
FilteringEnabled: c.FilteringEnabled,
|
||||
@@ -428,53 +428,32 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
|
||||
// Deprecated: Remove it when migration to the new API is over.
|
||||
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
data := make([]map[string]*clientJSON, 0, len(q))
|
||||
params := &client.FindParams{}
|
||||
var err error
|
||||
|
||||
data := []map[string]*clientJSON{}
|
||||
for i := range len(q) {
|
||||
idStr := q.Get(fmt.Sprintf("ip%d", i))
|
||||
if idStr == "" {
|
||||
break
|
||||
}
|
||||
|
||||
err = params.Set(idStr)
|
||||
if err != nil {
|
||||
clients.logger.DebugContext(
|
||||
r.Context(),
|
||||
"finding client",
|
||||
"id", idStr,
|
||||
slogutil.KeyError, err,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
data = append(data, map[string]*clientJSON{
|
||||
idStr: clients.findClient(idStr, params),
|
||||
idStr: clients.findClient(idStr),
|
||||
})
|
||||
}
|
||||
|
||||
aghhttp.WriteJSONResponseOK(w, r, data)
|
||||
}
|
||||
|
||||
// findClient returns available information about a client by params from the
|
||||
// client's storage or access settings. idStr is the string representation of
|
||||
// typed params. params must not be nil. cj is guaranteed to be non-nil.
|
||||
func (clients *clientsContainer) findClient(
|
||||
idStr string,
|
||||
params *client.FindParams,
|
||||
) (cj *clientJSON) {
|
||||
c, ok := clients.storage.Find(params)
|
||||
// findClient returns available information about a client by idStr from the
|
||||
// client's storage or access settings. cj is guaranteed to be non-nil.
|
||||
func (clients *clientsContainer) findClient(idStr string) (cj *clientJSON) {
|
||||
ip, _ := netip.ParseAddr(idStr)
|
||||
c, ok := clients.storage.Find(idStr)
|
||||
if !ok {
|
||||
return clients.findRuntime(idStr, params)
|
||||
return clients.findRuntime(ip, idStr)
|
||||
}
|
||||
|
||||
cj = clientToJSON(c)
|
||||
disallowed, rule := clients.clientChecker.IsBlockedClient(
|
||||
params.RemoteIP,
|
||||
string(params.ClientID),
|
||||
)
|
||||
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
|
||||
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
|
||||
|
||||
return cj
|
||||
@@ -493,8 +472,7 @@ type searchClientJSON struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// handleSearchClient is the handler for the POST /control/clients/search HTTP
|
||||
// API.
|
||||
// handleSearchClient is the handler for the POST /control/clients/search HTTP API.
|
||||
func (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *http.Request) {
|
||||
q := searchQueryJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&q)
|
||||
@@ -504,25 +482,11 @@ func (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]map[string]*clientJSON, 0, len(q.Clients))
|
||||
params := &client.FindParams{}
|
||||
|
||||
data := []map[string]*clientJSON{}
|
||||
for _, c := range q.Clients {
|
||||
idStr := c.ID
|
||||
err = params.Set(idStr)
|
||||
if err != nil {
|
||||
clients.logger.DebugContext(
|
||||
r.Context(),
|
||||
"searching client",
|
||||
"id", idStr,
|
||||
slogutil.KeyError, err,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
data = append(data, map[string]*clientJSON{
|
||||
idStr: clients.findClient(idStr, params),
|
||||
idStr: clients.findClient(idStr),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -530,37 +494,38 @@ func (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
// findRuntime looks up the IP in runtime and temporary storages, like
|
||||
// /etc/hosts tables, DHCP leases, or blocklists. params must not be nil. cj
|
||||
// is guaranteed to be non-nil.
|
||||
func (clients *clientsContainer) findRuntime(
|
||||
idStr string,
|
||||
params *client.FindParams,
|
||||
) (cj *clientJSON) {
|
||||
var host string
|
||||
whois := &whois.Info{}
|
||||
|
||||
ip := params.RemoteIP
|
||||
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be
|
||||
// non-nil.
|
||||
func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *clientJSON) {
|
||||
rc := clients.storage.ClientRuntime(ip)
|
||||
if rc != nil {
|
||||
_, host = rc.Info()
|
||||
whois = whoisOrEmpty(rc)
|
||||
if rc == nil {
|
||||
// It is still possible that the IP used to be in the runtime clients
|
||||
// list, but then the server was reloaded. So, check the DNS server's
|
||||
// blocked IP list.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2428.
|
||||
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
|
||||
cj = &clientJSON{
|
||||
IDs: []string{idStr},
|
||||
Disallowed: &disallowed,
|
||||
DisallowedRule: &rule,
|
||||
WHOIS: &whois.Info{},
|
||||
}
|
||||
|
||||
return cj
|
||||
}
|
||||
|
||||
// Check the DNS server's blocked IP list regardless of whether a runtime
|
||||
// client was found or not. This is because it's still possible that the
|
||||
// runtime client associated with the IP address was stored previously, but
|
||||
// then the server was reloaded.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2428.
|
||||
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, string(params.ClientID))
|
||||
|
||||
return &clientJSON{
|
||||
Name: host,
|
||||
IDs: []string{idStr},
|
||||
WHOIS: whois,
|
||||
Disallowed: &disallowed,
|
||||
DisallowedRule: &rule,
|
||||
_, host := rc.Info()
|
||||
cj = &clientJSON{
|
||||
Name: host,
|
||||
IDs: []string{idStr},
|
||||
WHOIS: whoisOrEmpty(rc),
|
||||
}
|
||||
|
||||
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
|
||||
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
|
||||
|
||||
return cj
|
||||
}
|
||||
|
||||
// RegisterClientsHandlers registers HTTP handlers
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestClientsContainer_HandleAddClient(t *testing.T) {
|
||||
clientTwo := newPersistentClientWithIDs(t, "client2", []string{testClientIP2})
|
||||
|
||||
clientEmptyID := newPersistentClient("empty_client_id")
|
||||
clientEmptyID.ClientIDs = []client.ClientID{""}
|
||||
clientEmptyID.ClientIDs = []string{""}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -278,7 +278,7 @@ func TestClientsContainer_HandleUpdateClient(t *testing.T) {
|
||||
clientModified := newPersistentClientWithIDs(t, "client2", []string{testClientIP2})
|
||||
|
||||
clientEmptyID := newPersistentClient("empty_client_id")
|
||||
clientEmptyID.ClientIDs = []client.ClientID{""}
|
||||
clientEmptyID.ClientIDs = []string{""}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
@@ -24,7 +23,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/renameio/v2/maybe"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -263,128 +261,30 @@ type dnsConfig struct {
|
||||
// HostsFileEnabled defines whether to use information from the system hosts
|
||||
// file to resolve queries.
|
||||
HostsFileEnabled bool `yaml:"hostsfile_enabled"`
|
||||
|
||||
// PendingRequests configures duplicate requests policy.
|
||||
PendingRequests *pendingRequests `yaml:"pending_requests"`
|
||||
}
|
||||
|
||||
// pendingRequests is a block with pending requests configuration.
|
||||
type pendingRequests struct {
|
||||
// Enabled controls if duplicate requests should be sent to the upstreams
|
||||
// along with the original one.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
// tlsConfigSettings is the TLS configuration for DNS-over-TLS, DNS-over-QUIC,
|
||||
// and HTTPS. When adding new properties, update the [tlsConfigSettings.clone]
|
||||
// and [tlsConfigSettings.setPrivateFieldsAndCompare] methods as necessary.
|
||||
type tlsConfigSettings struct {
|
||||
// Enabled indicates whether encryption (DoT/DoH/HTTPS) is enabled.
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DoT/DoH/HTTPS) status
|
||||
ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
|
||||
ForceHTTPS bool `yaml:"force_https" json:"force_https"` // ForceHTTPS: if true, forces HTTP->HTTPS redirect
|
||||
PortHTTPS uint16 `yaml:"port_https" json:"port_https,omitempty"` // HTTPS port. If 0, HTTPS will be disabled
|
||||
PortDNSOverTLS uint16 `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DoT will be disabled
|
||||
PortDNSOverQUIC uint16 `yaml:"port_dns_over_quic" json:"port_dns_over_quic,omitempty"` // DNS-over-QUIC port. If 0, DoQ will be disabled
|
||||
|
||||
// ServerName is the hostname of the HTTPS/TLS server.
|
||||
ServerName string `yaml:"server_name" json:"server_name,omitempty"`
|
||||
|
||||
// ForceHTTPS, if true, forces an HTTP to HTTPS redirect.
|
||||
ForceHTTPS bool `yaml:"force_https" json:"force_https"`
|
||||
|
||||
// PortHTTPS is the HTTPS port. If 0, HTTPS will be disabled.
|
||||
PortHTTPS uint16 `yaml:"port_https" json:"port_https,omitempty"`
|
||||
|
||||
// PortDNSOverTLS is the DNS-over-TLS port. If 0, DoT will be disabled.
|
||||
PortDNSOverTLS uint16 `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"`
|
||||
|
||||
// PortDNSOverQUIC is the DNS-over-QUIC port. If 0, DoQ will be disabled.
|
||||
PortDNSOverQUIC uint16 `yaml:"port_dns_over_quic" json:"port_dns_over_quic,omitempty"`
|
||||
|
||||
// PortDNSCrypt is the port for DNSCrypt requests. If it's zero, DNSCrypt
|
||||
// is disabled.
|
||||
// PortDNSCrypt is the port for DNSCrypt requests. If it's zero,
|
||||
// DNSCrypt is disabled.
|
||||
PortDNSCrypt uint16 `yaml:"port_dnscrypt" json:"port_dnscrypt"`
|
||||
|
||||
// DNSCryptConfigFile is the path to the DNSCrypt config file. Must be set
|
||||
// if PortDNSCrypt is not zero.
|
||||
// DNSCryptConfigFile is the path to the DNSCrypt config file. Must be
|
||||
// set if PortDNSCrypt is not zero.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/dnsproxy and
|
||||
// https://github.com/ameshkov/dnscrypt.
|
||||
DNSCryptConfigFile string `yaml:"dnscrypt_config_file" json:"dnscrypt_config_file"`
|
||||
|
||||
// AllowUnencryptedDoH allows DoH queries via unencrypted HTTP (e.g. for
|
||||
// reverse proxying).
|
||||
//
|
||||
// TODO(s.chzhen): Add this option into the Web UI.
|
||||
// Allow DoH queries via unencrypted HTTP (e.g. for reverse proxying)
|
||||
AllowUnencryptedDoH bool `yaml:"allow_unencrypted_doh" json:"allow_unencrypted_doh"`
|
||||
|
||||
// CertificateChain is the PEM-encoded certificate chain. Must be empty if
|
||||
// [tlsConfigSettings.CertificatePath] is provided.
|
||||
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"`
|
||||
|
||||
// PrivateKey is the PEM-encoded private key. Must be empty if
|
||||
// [tlsConfigSettings.PrivateKeyPath] is provided.
|
||||
PrivateKey string `yaml:"private_key" json:"private_key"`
|
||||
|
||||
// CertificatePath is the path to the certificate file. Must be empty if
|
||||
// [tlsConfigSettings.CertificateChain] is provided.
|
||||
CertificatePath string `yaml:"certificate_path" json:"certificate_path"`
|
||||
|
||||
// PrivateKeyPath is the path to the private key file. Must be empty if
|
||||
// [tlsConfigSettings.PrivateKey] is provided.
|
||||
PrivateKeyPath string `yaml:"private_key_path" json:"private_key_path"`
|
||||
|
||||
// OverrideTLSCiphers, when set, contains the names of the cipher suites to
|
||||
// use. If the slice is empty, the default safe suites are used.
|
||||
OverrideTLSCiphers []string `yaml:"override_tls_ciphers,omitempty" json:"-"`
|
||||
|
||||
// CertificateChainData is the PEM-encoded byte data for the certificate
|
||||
// chain.
|
||||
CertificateChainData []byte `yaml:"-" json:"-"`
|
||||
|
||||
// PrivateKeyData is the PEM-encoded byte data for the private key.
|
||||
PrivateKeyData []byte `yaml:"-" json:"-"`
|
||||
|
||||
// StrictSNICheck controls if the connections with SNI mismatching the
|
||||
// certificate's ones should be rejected.
|
||||
StrictSNICheck bool `yaml:"strict_sni_check" json:"-"`
|
||||
}
|
||||
|
||||
// clone returns a deep copy of c.
|
||||
func (c *tlsConfigSettings) clone() (clone *tlsConfigSettings) {
|
||||
clone = &tlsConfigSettings{}
|
||||
*clone = *c
|
||||
|
||||
clone.OverrideTLSCiphers = slices.Clone(c.OverrideTLSCiphers)
|
||||
clone.CertificateChainData = slices.Clone(c.CertificateChainData)
|
||||
clone.PrivateKeyData = slices.Clone(c.PrivateKeyData)
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// setPrivateFieldsAndCompare sets any missing properties in conf to match those
|
||||
// in c and returns true if TLS configurations are equal. conf must not be be
|
||||
// nil.
|
||||
// It sets the following properties because these are not accepted from the
|
||||
// frontend:
|
||||
//
|
||||
// [tlsConfigSettings.AllowUnencryptedDoH]
|
||||
// [tlsConfigSettings.DNSCryptConfigFile]
|
||||
// [tlsConfigSettings.OverrideTLSCiphers]
|
||||
// [tlsConfigSettings.PortDNSCrypt]
|
||||
//
|
||||
// The following properties are skipped as they are set by
|
||||
// [tlsManager.loadTLSConfig]:
|
||||
//
|
||||
// [tlsConfigSettings.CertificateChainData]
|
||||
// [tlsConfigSettings.PrivateKeyData]
|
||||
func (c *tlsConfigSettings) setPrivateFieldsAndCompare(conf *tlsConfigSettings) (equal bool) {
|
||||
conf.OverrideTLSCiphers = slices.Clone(c.OverrideTLSCiphers)
|
||||
|
||||
// TODO(s.chzhen): Remove this once the frontend supports it.
|
||||
conf.AllowUnencryptedDoH = c.AllowUnencryptedDoH
|
||||
|
||||
conf.DNSCryptConfigFile = c.DNSCryptConfigFile
|
||||
conf.PortDNSCrypt = c.PortDNSCrypt
|
||||
|
||||
// TODO(a.garipov): Define a custom comparer.
|
||||
return cmp.Equal(c, conf)
|
||||
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
type queryLogConfig struct {
|
||||
@@ -480,9 +380,6 @@ var config = &configuration{
|
||||
UsePrivateRDNS: true,
|
||||
ServePlainDNS: true,
|
||||
HostsFileEnabled: true,
|
||||
PendingRequests: &pendingRequests{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
TLS: tlsConfigSettings{
|
||||
PortHTTPS: defaultPortHTTPS,
|
||||
@@ -671,7 +568,7 @@ func parseConfig() (err error) {
|
||||
}
|
||||
|
||||
// Do not wrap the error because it's informative enough as is.
|
||||
return validateTLSCipherIDs(config.TLS.OverrideTLSCiphers)
|
||||
return setContextTLSCipherIDs()
|
||||
}
|
||||
|
||||
// validateConfig returns error if the configuration is invalid.
|
||||
@@ -752,8 +649,9 @@ func (c *configuration) write(tlsMgr *tlsManager) (err error) {
|
||||
}
|
||||
|
||||
if tlsMgr != nil {
|
||||
tlsConf := tlsMgr.config()
|
||||
config.TLS = *tlsConf
|
||||
tlsConf := tlsConfigSettings{}
|
||||
tlsMgr.WriteDiskConfig(&tlsConf)
|
||||
config.TLS = tlsConf
|
||||
}
|
||||
|
||||
if globalContext.stats != nil {
|
||||
@@ -823,15 +721,21 @@ func (c *configuration) write(tlsMgr *tlsManager) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTLSCipherIDs validates the custom TLS cipher suite IDs.
|
||||
func validateTLSCipherIDs(cipherIDs []string) (err error) {
|
||||
if len(cipherIDs) == 0 {
|
||||
// setContextTLSCipherIDs sets the TLS cipher suite IDs to use.
|
||||
func setContextTLSCipherIDs() (err error) {
|
||||
if len(config.TLS.OverrideTLSCiphers) == 0 {
|
||||
log.Info("tls: using default ciphers")
|
||||
|
||||
globalContext.tlsCipherIDs = aghtls.SaferCipherSuites()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = aghtls.ParseCiphers(cipherIDs)
|
||||
log.Info("tls: overriding ciphers: %s", config.TLS.OverrideTLSCiphers)
|
||||
|
||||
globalContext.tlsCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("override_tls_ciphers: %w", err)
|
||||
return fmt.Errorf("parsing override ciphers: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -82,7 +82,7 @@ func (web *webAPI) requestVersionInfo(
|
||||
) (err error) {
|
||||
updater := web.conf.updater
|
||||
for range 3 {
|
||||
resp.VersionInfo, err = updater.VersionInfo(ctx, recheck)
|
||||
resp.VersionInfo, err = updater.VersionInfo(recheck)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func (web *webAPI) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = updater.Update(r.Context(), false)
|
||||
err = updater.Update(false)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
||||
|
||||
@@ -164,8 +164,11 @@ func (vr *versionResponse) setAllowedToAutoUpdate(tlsMgr *tlsManager) (err error
|
||||
return nil
|
||||
}
|
||||
|
||||
tlsConf := &tlsConfigSettings{}
|
||||
tlsMgr.WriteDiskConfig(tlsConf)
|
||||
|
||||
canUpdate := true
|
||||
if tlsConfUsesPrivilegedPorts(tlsMgr.config()) ||
|
||||
if tlsConfUsesPrivilegedPorts(tlsConf) ||
|
||||
config.HTTPConfig.Address.Port() < 1024 ||
|
||||
config.DNS.Port < 1024 {
|
||||
canUpdate, err = aghnet.CanBindPrivilegedPorts()
|
||||
|
||||
@@ -2,7 +2,6 @@ package home
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
@@ -39,8 +38,6 @@ const (
|
||||
)
|
||||
|
||||
// Called by other modules when configuration is changed
|
||||
//
|
||||
// TODO(s.chzhen): Remove this after refactoring.
|
||||
func onConfigModified() {
|
||||
err := config.write(globalContext.tls)
|
||||
if err != nil {
|
||||
@@ -112,6 +109,9 @@ func initDNS(
|
||||
return err
|
||||
}
|
||||
|
||||
tlsConf := &tlsConfigSettings{}
|
||||
tlsMgr.WriteDiskConfig(tlsConf)
|
||||
|
||||
return initDNSServer(
|
||||
globalContext.filters,
|
||||
globalContext.stats,
|
||||
@@ -119,15 +119,15 @@ func initDNS(
|
||||
globalContext.dhcpServer,
|
||||
anonymizer,
|
||||
httpRegister,
|
||||
tlsMgr,
|
||||
tlsConf,
|
||||
baseLogger,
|
||||
)
|
||||
}
|
||||
|
||||
// initDNSServer initializes the [context.dnsServer]. To only use the internal
|
||||
// proxy, none of the arguments are required, but tlsMgr and l still must not be
|
||||
// nil, in other cases all the arguments also must not be nil. It also must not
|
||||
// be called unless [config] and [globalContext] are initialized.
|
||||
// proxy, none of the arguments are required, but tlsConf and l still must not
|
||||
// be nil, in other cases all the arguments also must not be nil. It also must
|
||||
// not be called unless [config] and [globalContext] are initialized.
|
||||
//
|
||||
// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.
|
||||
func initDNSServer(
|
||||
@@ -137,7 +137,7 @@ func initDNSServer(
|
||||
dhcpSrv dnsforward.DHCP,
|
||||
anonymizer *aghnet.IPMut,
|
||||
httpReg aghhttp.RegisterFunc,
|
||||
tlsMgr *tlsManager,
|
||||
tlsConf *tlsConfigSettings,
|
||||
l *slog.Logger,
|
||||
) (err error) {
|
||||
globalContext.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
|
||||
@@ -165,8 +165,7 @@ func initDNSServer(
|
||||
dnsConf, err := newServerConfig(
|
||||
&config.DNS,
|
||||
config.Clients.Sources,
|
||||
tlsMgr.config(),
|
||||
tlsMgr,
|
||||
tlsConf,
|
||||
httpReg,
|
||||
globalContext.clients.storage,
|
||||
)
|
||||
@@ -237,12 +236,11 @@ func ipsToUDPAddrs(ips []netip.Addr, port uint16) (udpAddrs []*net.UDPAddr) {
|
||||
}
|
||||
|
||||
// newServerConfig converts values from the configuration file into the internal
|
||||
// DNS server configuration. All arguments must not be nil, except for httpReg.
|
||||
// DNS server configuration. All arguments must not be nil.
|
||||
func newServerConfig(
|
||||
dnsConf *dnsConfig,
|
||||
clientSrcConf *clientSourcesConfig,
|
||||
tlsConf *tlsConfigSettings,
|
||||
tlsMgr *tlsManager,
|
||||
httpReg aghhttp.RegisterFunc,
|
||||
clientsContainer dnsforward.ClientsContainer,
|
||||
) (newConf *dnsforward.ServerConfig, err error) {
|
||||
@@ -251,19 +249,14 @@ func newServerConfig(
|
||||
fwdConf := dnsConf.Config
|
||||
fwdConf.ClientsContainer = clientsContainer
|
||||
|
||||
intTLSConf, err := newDNSTLSConfig(tlsConf, hosts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("constructing tls config: %w", err)
|
||||
}
|
||||
|
||||
newConf = &dnsforward.ServerConfig{
|
||||
UDPListenAddrs: ipsToUDPAddrs(hosts, dnsConf.Port),
|
||||
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
|
||||
Config: fwdConf,
|
||||
TLSConf: intTLSConf,
|
||||
TLSConfig: newDNSTLSConfig(tlsConf, hosts),
|
||||
TLSAllowUnencryptedDoH: tlsConf.AllowUnencryptedDoH,
|
||||
UpstreamTimeout: time.Duration(dnsConf.UpstreamTimeout),
|
||||
TLSv12Roots: tlsMgr.rootCerts,
|
||||
TLSv12Roots: globalContext.tlsRoots,
|
||||
ConfigModified: onConfigModified,
|
||||
HTTPRegister: httpReg,
|
||||
LocalPTRResolvers: dnsConf.PrivateRDNSResolvers,
|
||||
@@ -273,7 +266,6 @@ func newServerConfig(
|
||||
ServeHTTP3: dnsConf.ServeHTTP3,
|
||||
UseHTTP3Upstreams: dnsConf.UseHTTP3Upstreams,
|
||||
ServePlainDNS: dnsConf.ServePlainDNS,
|
||||
PendingRequestsEnabled: dnsConf.PendingRequests.Enabled,
|
||||
}
|
||||
|
||||
var initialAddresses []netip.Addr
|
||||
@@ -306,19 +298,14 @@ func newServerConfig(
|
||||
}
|
||||
|
||||
// newDNSTLSConfig converts values from the configuration file into the internal
|
||||
// TLS settings for the DNS server. conf must not be nil.
|
||||
func newDNSTLSConfig(
|
||||
conf *tlsConfigSettings,
|
||||
addrs []netip.Addr,
|
||||
) (dnsConf *dnsforward.TLSConfig, err error) {
|
||||
// TLS settings for the DNS server. tlsConf must not be nil.
|
||||
func newDNSTLSConfig(conf *tlsConfigSettings, addrs []netip.Addr) (dnsConf dnsforward.TLSConfig) {
|
||||
if !conf.Enabled {
|
||||
return &dnsforward.TLSConfig{}, nil
|
||||
return dnsforward.TLSConfig{}
|
||||
}
|
||||
|
||||
dnsConf = &dnsforward.TLSConfig{
|
||||
ServerName: conf.ServerName,
|
||||
StrictSNICheck: conf.StrictSNICheck,
|
||||
}
|
||||
dnsConf = conf.TLSConfig
|
||||
dnsConf.ServerName = conf.ServerName
|
||||
|
||||
if conf.PortHTTPS != 0 {
|
||||
dnsConf.HTTPSListenAddrs = ipsToTCPAddrs(addrs, conf.PortHTTPS)
|
||||
@@ -332,22 +319,7 @@ func newDNSTLSConfig(
|
||||
dnsConf.QUICListenAddrs = ipsToUDPAddrs(addrs, conf.PortDNSOverQUIC)
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(conf.CertificateChainData, conf.PrivateKeyData)
|
||||
if err != nil {
|
||||
const format = "parsing tls key pair: %w"
|
||||
if conf.AllowUnencryptedDoH {
|
||||
// TODO(s.chzhen): Use [slog.Logger].
|
||||
log.Info("warning: %s: %s", format, err)
|
||||
|
||||
return dnsConf, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(format, err)
|
||||
}
|
||||
|
||||
dnsConf.Cert = &cert
|
||||
|
||||
return dnsConf, nil
|
||||
return dnsConf
|
||||
}
|
||||
|
||||
// newDNSCryptConfig converts values from the configuration file into the
|
||||
@@ -400,7 +372,8 @@ type dnsEncryption struct {
|
||||
// getDNSEncryption returns the TLS encryption addresses that AdGuard Home
|
||||
// listens on. tlsMgr must not be nil.
|
||||
func getDNSEncryption(tlsMgr *tlsManager) (de dnsEncryption) {
|
||||
tlsConf := tlsMgr.config()
|
||||
tlsConf := tlsConfigSettings{}
|
||||
tlsMgr.WriteDiskConfig(&tlsConf)
|
||||
|
||||
if !tlsConf.Enabled || len(tlsConf.ServerName) == 0 {
|
||||
return dnsEncryption{}
|
||||
|
||||
@@ -3,6 +3,7 @@ package home
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
@@ -79,6 +81,10 @@ type homeContext struct {
|
||||
workDir string // Location of our directory, used to protect against CWD being somewhere else
|
||||
pidFileName string // PID file name. Empty if no PID file was created.
|
||||
controlLock sync.Mutex
|
||||
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
|
||||
|
||||
// tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use.
|
||||
tlsCipherIDs []uint16
|
||||
|
||||
// firstRun, if true, tells AdGuard Home to only start the web interface
|
||||
// service, and only serve the first-run APIs.
|
||||
@@ -136,6 +142,7 @@ func Main(clientBuildFS fs.FS) {
|
||||
func setupContext(opts options) (err error) {
|
||||
globalContext.firstRun = detectFirstRun()
|
||||
|
||||
globalContext.tlsRoots = aghtls.SystemRootCAs()
|
||||
globalContext.mux = http.NewServeMux()
|
||||
|
||||
if !opts.noEtcHosts {
|
||||
@@ -267,13 +274,18 @@ func setupOpts(opts options) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// initContextClients initializes Context clients and related fields. All
|
||||
// arguments must not be nil.
|
||||
// initContextClients initializes Context clients and related fields.
|
||||
func initContextClients(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
sigHdlr *signalHandler,
|
||||
) (err error) {
|
||||
err = setupDNSFilteringConf(ctx, logger, config.Filtering)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
//lint:ignore SA1019 Migration is not over.
|
||||
config.DHCP.WorkDir = globalContext.workDir
|
||||
config.DHCP.DataDir = globalContext.getDataDir()
|
||||
@@ -346,13 +358,11 @@ func setupBindOpts(opts options) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupDNSFilteringConf sets up DNS filtering configuration settings. All
|
||||
// arguments must not be nil.
|
||||
// setupDNSFilteringConf sets up DNS filtering configuration settings.
|
||||
func setupDNSFilteringConf(
|
||||
ctx context.Context,
|
||||
baseLogger *slog.Logger,
|
||||
conf *filtering.Config,
|
||||
tlsMgr *tlsManager,
|
||||
) (err error) {
|
||||
const (
|
||||
dnsTimeout = 3 * time.Second
|
||||
@@ -378,7 +388,7 @@ func setupDNSFilteringConf(
|
||||
conf.Filters = slices.Clone(config.Filters)
|
||||
conf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
|
||||
conf.UserRules = slices.Clone(config.UserRules)
|
||||
conf.HTTPClient = httpClient(tlsMgr)
|
||||
conf.HTTPClient = httpClient()
|
||||
|
||||
cacheTime := time.Duration(conf.CacheTime) * time.Minute
|
||||
|
||||
@@ -487,14 +497,9 @@ func checkPorts() (err error) {
|
||||
}
|
||||
|
||||
// isUpdateEnabled returns true if the update is enabled for current
|
||||
// configuration. It also logs the decision. isCustomURL should be true if the
|
||||
// configuration. It also logs the decision. customURL should be true if the
|
||||
// updater is using a custom URL.
|
||||
func isUpdateEnabled(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
opts *options,
|
||||
isCustomURL bool,
|
||||
) (ok bool) {
|
||||
func isUpdateEnabled(ctx context.Context, l *slog.Logger, opts *options, customURL bool) (ok bool) {
|
||||
if opts.disableUpdate {
|
||||
l.DebugContext(ctx, "updates are disabled by command-line option")
|
||||
|
||||
@@ -505,13 +510,13 @@ func isUpdateEnabled(
|
||||
case
|
||||
version.ChannelDevelopment,
|
||||
version.ChannelCandidate:
|
||||
if isCustomURL {
|
||||
if customURL {
|
||||
l.DebugContext(ctx, "updates are enabled because custom url is used")
|
||||
} else {
|
||||
l.DebugContext(ctx, "updates are disabled for development and candidate builds")
|
||||
}
|
||||
|
||||
return isCustomURL
|
||||
return customURL
|
||||
default:
|
||||
l.DebugContext(ctx, "updates are enabled")
|
||||
|
||||
@@ -519,7 +524,7 @@ func isUpdateEnabled(
|
||||
}
|
||||
}
|
||||
|
||||
// initWeb initializes the web module. upd, baseLogger, and tlsMgr must not be
|
||||
// initWeb initializes the web module. upd, baseLogger, and tlsMgr must not be
|
||||
// nil.
|
||||
func initWeb(
|
||||
ctx context.Context,
|
||||
@@ -528,7 +533,7 @@ func initWeb(
|
||||
upd *updater.Updater,
|
||||
baseLogger *slog.Logger,
|
||||
tlsMgr *tlsManager,
|
||||
isCustomUpdURL bool,
|
||||
customURL bool,
|
||||
) (web *webAPI, err error) {
|
||||
logger := baseLogger.With(slogutil.KeyPrefix, "webapi")
|
||||
|
||||
@@ -544,7 +549,7 @@ func initWeb(
|
||||
}
|
||||
}
|
||||
|
||||
disableUpdate := !isUpdateEnabled(ctx, baseLogger, &opts, isCustomUpdURL)
|
||||
disableUpdate := !isUpdateEnabled(ctx, baseLogger, &opts, customURL)
|
||||
|
||||
webConf := &webConfig{
|
||||
updater: upd,
|
||||
@@ -625,23 +630,6 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
|
||||
err = initContextClients(ctx, slogLogger, sigHdlr)
|
||||
fatalOnError(err)
|
||||
|
||||
tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager")
|
||||
tlsMgr, err := newTLSManager(ctx, &tlsManagerConfig{
|
||||
logger: tlsMgrLogger,
|
||||
configModified: onConfigModified,
|
||||
tlsSettings: config.TLS,
|
||||
servePlainDNS: config.DNS.ServePlainDNS,
|
||||
})
|
||||
if err != nil {
|
||||
tlsMgrLogger.ErrorContext(ctx, "initializing", slogutil.KeyError, err)
|
||||
onConfigModified()
|
||||
}
|
||||
|
||||
globalContext.tls = tlsMgr
|
||||
|
||||
err = setupDNSFilteringConf(ctx, slogLogger, config.Filtering, tlsMgr)
|
||||
fatalOnError(err)
|
||||
|
||||
err = setupOpts(opts)
|
||||
fatalOnError(err)
|
||||
|
||||
@@ -650,12 +638,11 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
|
||||
|
||||
confPath := configFilePath()
|
||||
|
||||
updLogger := slogLogger.With(slogutil.KeyPrefix, "updater")
|
||||
upd, isCustomURL := newUpdater(ctx, updLogger, config, globalContext.workDir, confPath, execPath)
|
||||
upd, customURL := newUpdater(ctx, slogLogger, globalContext.workDir, confPath, execPath, config)
|
||||
|
||||
// TODO(e.burkov): This could be made earlier, probably as the option's
|
||||
// effect.
|
||||
cmdlineUpdate(ctx, updLogger, opts, upd, tlsMgr)
|
||||
cmdlineUpdate(ctx, slogLogger, opts, upd)
|
||||
|
||||
if !globalContext.firstRun {
|
||||
// Save the updated config.
|
||||
@@ -677,14 +664,19 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
|
||||
globalContext.auth, err = initUsers()
|
||||
fatalOnError(err)
|
||||
|
||||
web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, isCustomURL)
|
||||
fatalOnError(err)
|
||||
tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager")
|
||||
tlsMgr, err := newTLSManager(ctx, tlsMgrLogger, config.TLS, config.DNS.ServePlainDNS)
|
||||
if err != nil {
|
||||
log.Error("initializing tls: %s", err)
|
||||
onConfigModified()
|
||||
}
|
||||
|
||||
globalContext.web = web
|
||||
|
||||
tlsMgr.setWebAPI(web)
|
||||
globalContext.tls = tlsMgr
|
||||
sigHdlr.addTLSManager(tlsMgr)
|
||||
|
||||
globalContext.web, err = initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
|
||||
fatalOnError(err)
|
||||
|
||||
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&globalContext, config)
|
||||
fatalOnError(err)
|
||||
|
||||
@@ -714,23 +706,22 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
|
||||
checkPermissions(ctx, slogLogger, globalContext.workDir, confPath, dataDir, statsDir, querylogDir)
|
||||
}
|
||||
|
||||
web.start(ctx)
|
||||
globalContext.web.start(ctx)
|
||||
|
||||
// Wait for other goroutines to complete their job.
|
||||
<-done
|
||||
}
|
||||
|
||||
// newUpdater creates a new AdGuard Home updater. l and conf must not be nil.
|
||||
// workDir, confPath, and execPath must not be empty. isCustomURL is true if
|
||||
// the user has specified a custom version announcement URL.
|
||||
// newUpdater creates a new AdGuard Home updater. customURL is true if the user
|
||||
// has specified a custom version announcement URL.
|
||||
func newUpdater(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
conf *configuration,
|
||||
workDir string,
|
||||
confPath string,
|
||||
execPath string,
|
||||
) (upd *updater.Updater, isCustomURL bool) {
|
||||
config *configuration,
|
||||
) (upd *updater.Updater, customURL bool) {
|
||||
// envName is the name of the environment variable that can be used to
|
||||
// override the default version check URL.
|
||||
const envName = "ADGUARD_HOME_TEST_UPDATE_VERSION_URL"
|
||||
@@ -742,14 +733,14 @@ func newUpdater(
|
||||
case version.Channel() == version.ChannelRelease:
|
||||
// Only enable custom version URL for development builds.
|
||||
l.DebugContext(ctx, "custom version url is disabled for release builds")
|
||||
case !conf.UnsafeUseCustomUpdateIndexURL:
|
||||
case !config.UnsafeUseCustomUpdateIndexURL:
|
||||
l.DebugContext(ctx, "custom version url is disabled in config")
|
||||
default:
|
||||
versionURL, _ = url.Parse(customURLStr)
|
||||
}
|
||||
|
||||
err := urlutil.ValidateHTTPURL(versionURL)
|
||||
if isCustomURL = err == nil; !isCustomURL {
|
||||
if customURL = err == nil; !customURL {
|
||||
l.DebugContext(ctx, "parsing custom version url", slogutil.KeyError, err)
|
||||
|
||||
versionURL = updater.DefaultVersionURL()
|
||||
@@ -758,8 +749,7 @@ func newUpdater(
|
||||
l.DebugContext(ctx, "creating updater", "config_path", confPath)
|
||||
|
||||
return updater.NewUpdater(&updater.Config{
|
||||
Client: conf.Filtering.HTTPClient,
|
||||
Logger: l,
|
||||
Client: config.Filtering.HTTPClient,
|
||||
Version: version.Version(),
|
||||
Channel: version.Channel(),
|
||||
GOARCH: runtime.GOARCH,
|
||||
@@ -770,7 +760,7 @@ func newUpdater(
|
||||
ConfName: confPath,
|
||||
ExecPath: execPath,
|
||||
VersionCheckURL: versionURL,
|
||||
}), isCustomURL
|
||||
}), customURL
|
||||
}
|
||||
|
||||
// checkPermissions checks and migrates permissions of the files and directories
|
||||
@@ -999,9 +989,9 @@ func printWebAddrs(proto, addr string, port uint16) {
|
||||
//
|
||||
// TODO(s.chzhen): Implement separate functions for HTTP and HTTPS.
|
||||
func printHTTPAddresses(proto string, tlsMgr *tlsManager) {
|
||||
var tlsConf *tlsConfigSettings
|
||||
tlsConf := tlsConfigSettings{}
|
||||
if tlsMgr != nil {
|
||||
tlsConf = tlsMgr.config()
|
||||
tlsMgr.WriteDiskConfig(&tlsConf)
|
||||
}
|
||||
|
||||
port := config.HTTPConfig.Address.Port()
|
||||
@@ -1068,15 +1058,8 @@ type jsonError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// cmdlineUpdate updates current application and exits. l and tlsMgr must not
|
||||
// be nil.
|
||||
func cmdlineUpdate(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
opts options,
|
||||
upd *updater.Updater,
|
||||
tlsMgr *tlsManager,
|
||||
) {
|
||||
// cmdlineUpdate updates current application and exits. l must not be nil.
|
||||
func cmdlineUpdate(ctx context.Context, l *slog.Logger, opts options, upd *updater.Updater) {
|
||||
if !opts.performUpdate {
|
||||
return
|
||||
}
|
||||
@@ -1086,12 +1069,12 @@ func cmdlineUpdate(
|
||||
//
|
||||
// TODO(e.burkov): We could probably initialize the internal resolver
|
||||
// separately.
|
||||
err := initDNSServer(nil, nil, nil, nil, nil, nil, tlsMgr, l)
|
||||
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, l)
|
||||
fatalOnError(err)
|
||||
|
||||
l.InfoContext(ctx, "performing update via cli")
|
||||
|
||||
info, err := upd.VersionInfo(ctx, true)
|
||||
info, err := upd.VersionInfo(true)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "getting version info", slogutil.KeyError, err)
|
||||
|
||||
@@ -1104,7 +1087,7 @@ func cmdlineUpdate(
|
||||
os.Exit(osutil.ExitCodeSuccess)
|
||||
}
|
||||
|
||||
err = upd.Update(ctx, globalContext.firstRun)
|
||||
err = upd.Update(globalContext.firstRun)
|
||||
fatalOnError(err)
|
||||
|
||||
err = restartService()
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
// httpClient returns a new HTTP client that uses the AdGuard Home's own DNS
|
||||
// server for resolving hostnames. The resulting client should not be used
|
||||
// until [Context.dnsServer] is initialized. tlsMgr must not be nil.
|
||||
// until [Context.dnsServer] is initialized.
|
||||
//
|
||||
// TODO(a.garipov, e.burkov): This is rather messy. Refactor.
|
||||
func httpClient(tlsMgr *tlsManager) (c *http.Client) {
|
||||
func httpClient() (c *http.Client) {
|
||||
// Do not use Context.dnsServer.DialContext directly in the struct literal
|
||||
// below, since Context.dnsServer may be nil when this function is called.
|
||||
dialContext := func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
||||
@@ -27,8 +27,8 @@ func httpClient(tlsMgr *tlsManager) (c *http.Client) {
|
||||
DialContext: dialContext,
|
||||
Proxy: httpProxy,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: tlsMgr.rootCerts,
|
||||
CipherSuites: tlsMgr.customCipherIDs,
|
||||
RootCAs: globalContext.tlsRoots,
|
||||
CipherSuites: globalContext.tlsCipherIDs,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
@@ -151,7 +151,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
|
||||
|
||||
clientID := q.Get("client_id")
|
||||
if clientID != "" {
|
||||
err = client.ValidateClientID(clientID)
|
||||
err = dnsforward.ValidateClientID(clientID)
|
||||
if err != nil {
|
||||
respondJSONError(w, http.StatusBadRequest, err.Error())
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -22,11 +21,12 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/c2h5oh/datasize"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// tlsManager contains the current configuration and state of AdGuard Home TLS
|
||||
@@ -35,50 +35,14 @@ type tlsManager struct {
|
||||
// logger is used for logging the operation of the TLS Manager.
|
||||
logger *slog.Logger
|
||||
|
||||
// mu protects status, certLastMod, conf, and servePlainDNS.
|
||||
mu *sync.Mutex
|
||||
|
||||
// status is the current status of the configuration. It is never nil.
|
||||
status *tlsConfigStatus
|
||||
|
||||
// certLastMod is the last modification time of the certificate file.
|
||||
certLastMod time.Time
|
||||
|
||||
// rootCerts is a pool of root CAs for TLSv1.2.
|
||||
rootCerts *x509.CertPool
|
||||
|
||||
// web is the web UI and API server. It must not be nil.
|
||||
//
|
||||
// TODO(s.chzhen): Temporary cyclic dependency due to ongoing refactoring.
|
||||
// Resolve it.
|
||||
web *webAPI
|
||||
|
||||
// conf contains the TLS configuration settings. It must not be nil.
|
||||
conf *tlsConfigSettings
|
||||
|
||||
// configModified is called when the TLS configuration is changed via an
|
||||
// HTTP request.
|
||||
configModified func()
|
||||
|
||||
// customCipherIDs are the ID of the cipher suites that AdGuard Home must use.
|
||||
customCipherIDs []uint16
|
||||
|
||||
// servePlainDNS defines if plain DNS is allowed for incoming requests.
|
||||
servePlainDNS bool
|
||||
}
|
||||
|
||||
// tlsManagerConfig contains the settings for initializing the TLS manager.
|
||||
type tlsManagerConfig struct {
|
||||
// logger is used for logging the operation of the TLS Manager. It must not
|
||||
// be nil.
|
||||
logger *slog.Logger
|
||||
|
||||
// configModified is called when the TLS configuration is changed via an
|
||||
// HTTP request. It must not be nil.
|
||||
configModified func()
|
||||
|
||||
// tlsSettings contains the TLS configuration settings.
|
||||
tlsSettings tlsConfigSettings
|
||||
confLock sync.Mutex
|
||||
conf tlsConfigSettings
|
||||
|
||||
// servePlainDNS defines if plain DNS is allowed for incoming requests.
|
||||
servePlainDNS bool
|
||||
@@ -86,66 +50,38 @@ type tlsManagerConfig struct {
|
||||
|
||||
// newTLSManager initializes the manager of TLS configuration. m is always
|
||||
// non-nil while any returned error indicates that the TLS configuration isn't
|
||||
// valid. Thus TLS may be initialized later, e.g. via the web UI. conf must
|
||||
// not be nil. Note that [tlsManager.web] must be initialized later on by using
|
||||
// [tlsManager.setWebAPI].
|
||||
func newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager, err error) {
|
||||
// valid. Thus TLS may be initialized later, e.g. via the web UI. logger must
|
||||
// not be nil.
|
||||
func newTLSManager(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
conf tlsConfigSettings,
|
||||
servePlainDNS bool,
|
||||
) (m *tlsManager, err error) {
|
||||
m = &tlsManager{
|
||||
logger: conf.logger,
|
||||
mu: &sync.Mutex{},
|
||||
configModified: conf.configModified,
|
||||
status: &tlsConfigStatus{},
|
||||
conf: &conf.tlsSettings,
|
||||
servePlainDNS: conf.servePlainDNS,
|
||||
logger: logger,
|
||||
status: &tlsConfigStatus{},
|
||||
conf: conf,
|
||||
servePlainDNS: servePlainDNS,
|
||||
}
|
||||
|
||||
m.rootCerts = aghtls.SystemRootCAs()
|
||||
|
||||
if len(conf.tlsSettings.OverrideTLSCiphers) > 0 {
|
||||
m.customCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
|
||||
if m.conf.Enabled {
|
||||
err = m.load(ctx)
|
||||
if err != nil {
|
||||
// Should not happen because upstreams are already validated. See
|
||||
// [validateTLSCipherIDs].
|
||||
panic(err)
|
||||
m.conf.Enabled = false
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
m.logger.InfoContext(ctx, "overriding ciphers", "ciphers", config.TLS.OverrideTLSCiphers)
|
||||
} else {
|
||||
m.logger.InfoContext(ctx, "using default ciphers")
|
||||
m.setCertFileTime(ctx)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if !m.conf.Enabled {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
err = m.load(ctx)
|
||||
if err != nil {
|
||||
m.conf.Enabled = false
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
m.setCertFileTime(ctx)
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// setWebAPI stores the provided web API. It must be called before
|
||||
// [tlsManager.start], [tlsManager.reload], [tlsManager.handleTLSConfigure], or
|
||||
// [tlsManager.validateTLSSettings].
|
||||
//
|
||||
// TODO(s.chzhen): Remove it once cyclic dependency is resolved.
|
||||
func (m *tlsManager) setWebAPI(webAPI *webAPI) {
|
||||
m.web = webAPI
|
||||
}
|
||||
|
||||
// load reloads the TLS configuration from files or data from the config file.
|
||||
// m.mu is expected to be locked.
|
||||
func (m *tlsManager) load(ctx context.Context) (err error) {
|
||||
err = m.loadTLSConfig(ctx, m.conf, m.status)
|
||||
err = m.loadTLSConf(ctx, &m.conf, m.status)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
}
|
||||
@@ -153,16 +89,15 @@ func (m *tlsManager) load(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// config returns a deep copy of the stored TLS configuration.
|
||||
func (m *tlsManager) config() (conf *tlsConfigSettings) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.conf.clone()
|
||||
// WriteDiskConfig - write config
|
||||
func (m *tlsManager) WriteDiskConfig(conf *tlsConfigSettings) {
|
||||
m.confLock.Lock()
|
||||
*conf = m.conf
|
||||
m.confLock.Unlock()
|
||||
}
|
||||
|
||||
// setCertFileTime sets [tlsManager.certLastMod] from the certificate. If there
|
||||
// are errors, setCertFileTime logs them. m.mu is expected to be locked.
|
||||
// are errors, setCertFileTime logs them.
|
||||
func (m *tlsManager) setCertFileTime(ctx context.Context) {
|
||||
if len(m.conf.CertificatePath) == 0 {
|
||||
return
|
||||
@@ -184,24 +119,21 @@ func (m *tlsManager) setCertFileTime(ctx context.Context) {
|
||||
func (m *tlsManager) start(_ context.Context) {
|
||||
m.registerWebHandlers()
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.confLock.Lock()
|
||||
tlsConf := m.conf
|
||||
m.confLock.Unlock()
|
||||
|
||||
// The background context is used because the TLSConfigChanged wraps context
|
||||
// with timeout on its own and shuts down the server, which handles current
|
||||
// request.
|
||||
m.web.tlsConfigChanged(context.Background(), m.conf)
|
||||
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||
}
|
||||
|
||||
// reload updates the configuration and restarts the TLS manager. It logs any
|
||||
// encountered errors.
|
||||
//
|
||||
// TODO(s.chzhen): Consider returning an error.
|
||||
// reload updates the configuration and restarts the TLS manager.
|
||||
func (m *tlsManager) reload(ctx context.Context) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.confLock.Lock()
|
||||
tlsConf := m.conf
|
||||
m.confLock.Unlock()
|
||||
|
||||
if !tlsConf.Enabled || len(tlsConf.CertificatePath) == 0 {
|
||||
return
|
||||
@@ -223,7 +155,9 @@ func (m *tlsManager) reload(ctx context.Context) {
|
||||
|
||||
m.logger.InfoContext(ctx, "certificate file is modified")
|
||||
|
||||
m.confLock.Lock()
|
||||
err = m.load(ctx)
|
||||
m.confLock.Unlock()
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "reloading", slogutil.KeyError, err)
|
||||
|
||||
@@ -237,20 +171,26 @@ func (m *tlsManager) reload(ctx context.Context) {
|
||||
m.logger.ErrorContext(ctx, "reconfiguring dns server", slogutil.KeyError, err)
|
||||
}
|
||||
|
||||
m.confLock.Lock()
|
||||
tlsConf = m.conf
|
||||
m.confLock.Unlock()
|
||||
|
||||
// The background context is used because the TLSConfigChanged wraps context
|
||||
// with timeout on its own and shuts down the server, which handles current
|
||||
// request.
|
||||
m.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||
}
|
||||
|
||||
// reconfigureDNSServer updates the DNS server configuration using the stored
|
||||
// TLS settings. m.mu is expected to be locked.
|
||||
// TLS settings.
|
||||
func (m *tlsManager) reconfigureDNSServer() (err error) {
|
||||
tlsConf := &tlsConfigSettings{}
|
||||
m.WriteDiskConfig(tlsConf)
|
||||
|
||||
newConf, err := newServerConfig(
|
||||
&config.DNS,
|
||||
config.Clients.Sources,
|
||||
m.conf,
|
||||
m,
|
||||
tlsConf,
|
||||
httpRegister,
|
||||
globalContext.clients.storage,
|
||||
)
|
||||
@@ -266,11 +206,9 @@ func (m *tlsManager) reconfigureDNSServer() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTLSConfig loads and validates the TLS configuration. It also sets
|
||||
// [tlsConfigSettings.CertificateChainData] and
|
||||
// [tlsConfigSettings.PrivateKeyData] properties. The returned error is also
|
||||
// set in status.WarningValidation.
|
||||
func (m *tlsManager) loadTLSConfig(
|
||||
// loadTLSConf loads and validates the TLS configuration. The returned error is
|
||||
// also set in status.WarningValidation.
|
||||
func (m *tlsManager) loadTLSConf(
|
||||
ctx context.Context,
|
||||
tlsConf *tlsConfigSettings,
|
||||
status *tlsConfigStatus,
|
||||
@@ -362,10 +300,10 @@ type tlsConfigStatus struct {
|
||||
KeyType string `json:"key_type,omitempty"`
|
||||
|
||||
// NotBefore is the NotBefore field of the first certificate in the chain.
|
||||
NotBefore time.Time `json:"not_before"`
|
||||
NotBefore time.Time `json:"not_before,omitempty"`
|
||||
|
||||
// NotAfter is the NotAfter field of the first certificate in the chain.
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
NotAfter time.Time `json:"not_after,omitempty"`
|
||||
|
||||
// WarningValidation is a validation warning message with the issue
|
||||
// description.
|
||||
@@ -415,31 +353,21 @@ type tlsConfigSettingsExt struct {
|
||||
|
||||
// handleTLSStatus is the handler for the GET /control/tls/status HTTP API.
|
||||
func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var tlsConf *tlsConfigSettings
|
||||
var servePlainDNS bool
|
||||
func() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
tlsConf = m.conf.clone()
|
||||
servePlainDNS = m.servePlainDNS
|
||||
}()
|
||||
|
||||
m.confLock.Lock()
|
||||
data := tlsConfig{
|
||||
tlsConfigSettingsExt: tlsConfigSettingsExt{
|
||||
tlsConfigSettings: *tlsConf,
|
||||
ServePlainDNS: aghalg.BoolToNullBool(servePlainDNS),
|
||||
tlsConfigSettings: m.conf,
|
||||
ServePlainDNS: aghalg.BoolToNullBool(m.servePlainDNS),
|
||||
},
|
||||
tlsConfigStatus: m.status,
|
||||
}
|
||||
m.confLock.Unlock()
|
||||
|
||||
marshalTLS(w, r, data)
|
||||
}
|
||||
|
||||
// handleTLSValidate is the handler for the POST /control/tls/validate HTTP API.
|
||||
func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
setts, err := unmarshalTLS(r)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
||||
@@ -447,16 +375,11 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if setts.PrivateKeySaved {
|
||||
setts.PrivateKey = m.conf.PrivateKey
|
||||
}
|
||||
|
||||
if err = m.validateTLSSettings(setts); err != nil {
|
||||
m.logger.InfoContext(ctx, "validating tls settings", slogutil.KeyError, err)
|
||||
|
||||
if err = validateTLSSettings(setts); err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
return
|
||||
@@ -465,7 +388,7 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip the error check, since we are only interested in the value of
|
||||
// status.WarningValidation.
|
||||
status := &tlsConfigStatus{}
|
||||
_ = m.loadTLSConfig(ctx, &setts.tlsConfigSettings, status)
|
||||
_ = m.loadTLSConf(r.Context(), &setts.tlsConfigSettings, status)
|
||||
resp := tlsConfig{
|
||||
tlsConfigSettingsExt: setts,
|
||||
tlsConfigStatus: status,
|
||||
@@ -474,23 +397,42 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
marshalTLS(w, r, resp)
|
||||
}
|
||||
|
||||
// setConfig updates manager TLS configuration with the given one. m.mu is
|
||||
// expected to be locked.
|
||||
// setConfig updates manager conf with the given one.
|
||||
func (m *tlsManager) setConfig(
|
||||
ctx context.Context,
|
||||
newConf tlsConfigSettings,
|
||||
status *tlsConfigStatus,
|
||||
servePlain aghalg.NullBool,
|
||||
) (restartHTTPS bool) {
|
||||
if !m.conf.setPrivateFieldsAndCompare(&newConf) {
|
||||
m.confLock.Lock()
|
||||
defer m.confLock.Unlock()
|
||||
|
||||
// Reset the DNSCrypt data before comparing, since we currently do not
|
||||
// accept these from the frontend.
|
||||
//
|
||||
// TODO(a.garipov): Define a custom comparer for dnsforward.TLSConfig.
|
||||
newConf.DNSCryptConfigFile = m.conf.DNSCryptConfigFile
|
||||
newConf.PortDNSCrypt = m.conf.PortDNSCrypt
|
||||
if !cmp.Equal(m.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
|
||||
m.logger.InfoContext(ctx, "config has changed, restarting https server")
|
||||
restartHTTPS = true
|
||||
} else {
|
||||
m.logger.InfoContext(ctx, "config has not changed")
|
||||
}
|
||||
|
||||
m.conf = &newConf
|
||||
|
||||
// Note: don't do just `t.conf = data` because we must preserve all other members of t.conf
|
||||
m.conf.Enabled = newConf.Enabled
|
||||
m.conf.ServerName = newConf.ServerName
|
||||
m.conf.ForceHTTPS = newConf.ForceHTTPS
|
||||
m.conf.PortHTTPS = newConf.PortHTTPS
|
||||
m.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
|
||||
m.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
|
||||
m.conf.CertificateChain = newConf.CertificateChain
|
||||
m.conf.CertificatePath = newConf.CertificatePath
|
||||
m.conf.CertificateChainData = newConf.CertificateChainData
|
||||
m.conf.PrivateKey = newConf.PrivateKey
|
||||
m.conf.PrivateKeyPath = newConf.PrivateKeyPath
|
||||
m.conf.PrivateKeyData = newConf.PrivateKeyData
|
||||
m.status = status
|
||||
|
||||
if servePlain != aghalg.NBNull {
|
||||
@@ -512,28 +454,18 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
var restartHTTPS bool
|
||||
defer func() {
|
||||
if restartHTTPS {
|
||||
m.configModified()
|
||||
}
|
||||
}()
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if req.PrivateKeySaved {
|
||||
req.PrivateKey = m.conf.PrivateKey
|
||||
}
|
||||
|
||||
if err = m.validateTLSSettings(req); err != nil {
|
||||
if err = validateTLSSettings(req); err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
status := &tlsConfigStatus{}
|
||||
err = m.loadTLSConfig(ctx, &req.tlsConfigSettings, status)
|
||||
err = m.loadTLSConf(ctx, &req.tlsConfigSettings, status)
|
||||
if err != nil {
|
||||
resp := tlsConfig{
|
||||
tlsConfigSettingsExt: req,
|
||||
@@ -545,18 +477,20 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
restartHTTPS = m.setConfig(ctx, req.tlsConfigSettings, status, req.ServePlainDNS)
|
||||
restartHTTPS := m.setConfig(ctx, req.tlsConfigSettings, status, req.ServePlainDNS)
|
||||
m.setCertFileTime(ctx)
|
||||
|
||||
if req.ServePlainDNS != aghalg.NBNull {
|
||||
func() {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
m.confLock.Lock()
|
||||
defer m.confLock.Unlock()
|
||||
|
||||
config.DNS.ServePlainDNS = req.ServePlainDNS == aghalg.NBTrue
|
||||
}()
|
||||
}
|
||||
|
||||
onConfigModified()
|
||||
|
||||
err = m.reconfigureDNSServer()
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "reconfiguring dns server", slogutil.KeyError, err)
|
||||
@@ -572,64 +506,46 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
marshalTLS(w, r, resp)
|
||||
rc := http.NewResponseController(w)
|
||||
err = rc.Flush()
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "flushing response", slogutil.KeyError, err)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
// The background context is used because the TLSConfigChanged wraps context
|
||||
// with timeout on its own and shuts down the server, which handles current
|
||||
// request. It is also should be done in a separate goroutine due to the
|
||||
// request. It is also should be done in a separate goroutine due to the
|
||||
// same reason.
|
||||
if restartHTTPS {
|
||||
go m.web.tlsConfigChanged(context.Background(), &req.tlsConfigSettings)
|
||||
go func() {
|
||||
globalContext.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// validateTLSSettings returns error if the setts are not valid.
|
||||
func (m *tlsManager) validateTLSSettings(setts tlsConfigSettingsExt) (err error) {
|
||||
if !setts.Enabled {
|
||||
if setts.ServePlainDNS == aghalg.NBFalse {
|
||||
// TODO(a.garipov): Support full disabling of all DNS.
|
||||
return errors.Error("plain DNS is required in case encryption protocols are disabled")
|
||||
func validateTLSSettings(setts tlsConfigSettingsExt) (err error) {
|
||||
if setts.Enabled {
|
||||
err = validatePorts(
|
||||
tcpPort(config.HTTPConfig.Address.Port()),
|
||||
tcpPort(setts.PortHTTPS),
|
||||
tcpPort(setts.PortDNSOverTLS),
|
||||
tcpPort(setts.PortDNSCrypt),
|
||||
udpPort(config.DNS.Port),
|
||||
udpPort(setts.PortDNSOverQUIC),
|
||||
)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
} else if setts.ServePlainDNS == aghalg.NBFalse {
|
||||
// TODO(a.garipov): Support full disabling of all DNS.
|
||||
return errors.Error("plain DNS is required in case encryption protocols are disabled")
|
||||
}
|
||||
|
||||
var (
|
||||
tlsConf tlsConfigSettings
|
||||
webAPIAddr netip.Addr
|
||||
webAPIPort uint16
|
||||
plainDNSPort uint16
|
||||
)
|
||||
|
||||
func() {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
|
||||
tlsConf = config.TLS
|
||||
webAPIAddr = config.HTTPConfig.Address.Addr()
|
||||
webAPIPort = config.HTTPConfig.Address.Port()
|
||||
plainDNSPort = config.DNS.Port
|
||||
}()
|
||||
|
||||
err = validatePorts(
|
||||
tcpPort(webAPIPort),
|
||||
tcpPort(setts.PortHTTPS),
|
||||
tcpPort(setts.PortDNSOverTLS),
|
||||
tcpPort(setts.PortDNSCrypt),
|
||||
udpPort(plainDNSPort),
|
||||
udpPort(setts.PortDNSOverQUIC),
|
||||
)
|
||||
if err != nil {
|
||||
// Don't wrap the error because it's informative enough as is.
|
||||
return err
|
||||
if !webCheckPortAvailable(setts.PortHTTPS) {
|
||||
return fmt.Errorf("port %d is not available, cannot enable HTTPS on it", setts.PortHTTPS)
|
||||
}
|
||||
|
||||
// Don't wrap the error because it's informative enough as is.
|
||||
return m.checkPortAvailability(tlsConf, setts.tlsConfigSettings, webAPIAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePorts validates the uniqueness of TCP and UDP ports for AdGuard Home
|
||||
@@ -641,11 +557,10 @@ func validatePorts(
|
||||
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
||||
addPorts(
|
||||
tcpPorts,
|
||||
bindPort,
|
||||
dohPort,
|
||||
dotPort,
|
||||
dnscryptTCPPort,
|
||||
tcpPort(dnsPort),
|
||||
tcpPort(bindPort),
|
||||
tcpPort(dohPort),
|
||||
tcpPort(dotPort),
|
||||
tcpPort(dnscryptTCPPort),
|
||||
)
|
||||
|
||||
err = tcpPorts.Validate()
|
||||
@@ -654,7 +569,7 @@ func validatePorts(
|
||||
}
|
||||
|
||||
udpPorts := aghalg.UniqChecker[udpPort]{}
|
||||
addPorts(udpPorts, dnsPort, doqPort)
|
||||
addPorts(udpPorts, udpPort(dnsPort), udpPort(doqPort))
|
||||
|
||||
err = udpPorts.Validate()
|
||||
if err != nil {
|
||||
@@ -689,7 +604,7 @@ func (m *tlsManager) validateCertChain(
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
DNSName: srvName,
|
||||
Roots: m.rootCerts,
|
||||
Roots: globalContext.tlsRoots,
|
||||
Intermediates: pool,
|
||||
}
|
||||
_, err = main.Verify(opts)
|
||||
@@ -700,67 +615,6 @@ func (m *tlsManager) validateCertChain(
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPortAvailability checks [tlsConfigSettings.PortHTTPS],
|
||||
// [tlsConfigSettings.PortDNSOverTLS], and [tlsConfigSettings.PortDNSOverQUIC]
|
||||
// are available for use. It checks the current configuration and, if needed,
|
||||
// attempts to bind to the port. The function returns human-readable error
|
||||
// messages for the frontend. This is best-effort check to prevent an "address
|
||||
// already in use" error.
|
||||
//
|
||||
// TODO(a.garipov): Adapt for HTTP/3.
|
||||
func (m *tlsManager) checkPortAvailability(
|
||||
currConf tlsConfigSettings,
|
||||
newConf tlsConfigSettings,
|
||||
addr netip.Addr,
|
||||
) (err error) {
|
||||
const (
|
||||
networkTCP = "tcp"
|
||||
networkUDP = "udp"
|
||||
|
||||
protoHTTPS = "HTTPS"
|
||||
protoDoT = "DNS-over-TLS"
|
||||
protoDoQ = "DNS-over-QUIC"
|
||||
)
|
||||
|
||||
needBindingCheck := []struct {
|
||||
network string
|
||||
proto string
|
||||
currPort uint16
|
||||
newPort uint16
|
||||
}{{
|
||||
network: networkTCP,
|
||||
proto: protoHTTPS,
|
||||
currPort: currConf.PortHTTPS,
|
||||
newPort: newConf.PortHTTPS,
|
||||
}, {
|
||||
network: networkTCP,
|
||||
proto: protoDoT,
|
||||
currPort: currConf.PortDNSOverTLS,
|
||||
newPort: newConf.PortDNSOverTLS,
|
||||
}, {
|
||||
network: networkUDP,
|
||||
proto: protoDoQ,
|
||||
currPort: currConf.PortDNSOverQUIC,
|
||||
newPort: newConf.PortDNSOverQUIC,
|
||||
}}
|
||||
|
||||
var errs []error
|
||||
for _, v := range needBindingCheck {
|
||||
port := v.newPort
|
||||
if v.currPort == port {
|
||||
continue
|
||||
}
|
||||
|
||||
addrPort := netip.AddrPortFrom(addr, port)
|
||||
err = aghnet.CheckPort(v.network, addrPort)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("port %d for %s is not available", port, v.proto))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// errNoIPInCert is the error that is returned from [tlsManager.parseCertChain]
|
||||
// if the leaf certificate doesn't contain IPs.
|
||||
const errNoIPInCert errors.Error = `certificates has no IP addresses; ` +
|
||||
@@ -864,12 +718,27 @@ func (m *tlsManager) validateCertificates(
|
||||
) (err error) {
|
||||
// Check only the public certificate separately from the key.
|
||||
if len(certChain) > 0 {
|
||||
var ok bool
|
||||
ok, err = m.validateCertificate(ctx, status, certChain, serverName)
|
||||
if !ok {
|
||||
var certs []*x509.Certificate
|
||||
certs, status.ValidCert, err = m.parseCertChain(ctx, certChain)
|
||||
if !status.ValidCert {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
mainCert := certs[0]
|
||||
status.Subject = mainCert.Subject.String()
|
||||
status.Issuer = mainCert.Issuer.String()
|
||||
status.NotAfter = mainCert.NotAfter
|
||||
status.NotBefore = mainCert.NotBefore
|
||||
status.DNSNames = mainCert.DNSNames
|
||||
|
||||
if chainErr := m.validateCertChain(ctx, certs, serverName); chainErr != nil {
|
||||
// Let self-signed certs through and don't return this error to set
|
||||
// its message into the status.WarningValidation afterwards.
|
||||
err = chainErr
|
||||
} else {
|
||||
status.ValidChain = true
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the private key by parsing it.
|
||||
@@ -897,41 +766,6 @@ func (m *tlsManager) validateCertificates(
|
||||
return err
|
||||
}
|
||||
|
||||
// validateCertificate processes certificate data. status must not be nil, as
|
||||
// it is used to accumulate the validation results. Other parameters are
|
||||
// optional. If ok is true, the returned error, if any, is not critical.
|
||||
func (m *tlsManager) validateCertificate(
|
||||
ctx context.Context,
|
||||
status *tlsConfigStatus,
|
||||
certChain []byte,
|
||||
serverName string,
|
||||
) (ok bool, err error) {
|
||||
var certs []*x509.Certificate
|
||||
certs, status.ValidCert, err = m.parseCertChain(ctx, certChain)
|
||||
if !status.ValidCert {
|
||||
// Don't wrap the error, since it's informative enough as is.
|
||||
return false, err
|
||||
}
|
||||
|
||||
mainCert := certs[0]
|
||||
status.Subject = mainCert.Subject.String()
|
||||
status.Issuer = mainCert.Issuer.String()
|
||||
status.NotAfter = mainCert.NotAfter
|
||||
status.NotBefore = mainCert.NotBefore
|
||||
status.DNSNames = mainCert.DNSNames
|
||||
|
||||
err = m.validateCertChain(ctx, certs, serverName)
|
||||
if err != nil {
|
||||
// Let self-signed certs through and don't return this error to set
|
||||
// its message into the status.WarningValidation afterwards.
|
||||
return true, err
|
||||
}
|
||||
|
||||
status.ValidChain = true
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Key types.
|
||||
const (
|
||||
keyTypeECDSA = "ECDSA"
|
||||
@@ -994,18 +828,17 @@ func unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if data.PrivateKey == "" {
|
||||
return data, nil
|
||||
}
|
||||
if data.PrivateKey != "" {
|
||||
var key []byte
|
||||
key, err = base64.StdEncoding.DecodeString(data.PrivateKey)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("failed to base64-decode private key: %w", err)
|
||||
}
|
||||
|
||||
key, err := base64.StdEncoding.DecodeString(data.PrivateKey)
|
||||
if err != nil {
|
||||
return data, fmt.Errorf("failed to base64-decode private key: %w", err)
|
||||
}
|
||||
|
||||
data.PrivateKey = string(key)
|
||||
if data.PrivateKeyPath != "" {
|
||||
return data, fmt.Errorf("private key data and file can't be set together")
|
||||
data.PrivateKey = string(key)
|
||||
if data.PrivateKeyPath != "" {
|
||||
return data, fmt.Errorf("private key data and file can't be set together")
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO(s.chzhen): Consider moving to testdata.
|
||||
var testCertChainData = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
|
||||
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
|
||||
@@ -67,11 +66,7 @@ func TestValidateCertificates(t *testing.T) {
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
logger := slogutil.NewDiscardLogger()
|
||||
|
||||
m, err := newTLSManager(ctx, &tlsManagerConfig{
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
m, err := newTLSManager(ctx, logger, tlsConfigSettings{}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("bad_certificate", func(t *testing.T) {
|
||||
@@ -117,6 +112,7 @@ func TestValidateCertificates(t *testing.T) {
|
||||
// - [homeContext.clients.storage]
|
||||
// - [homeContext.dnsServer]
|
||||
// - [homeContext.mux]
|
||||
// - [homeContext.web]
|
||||
//
|
||||
// TODO(s.chzhen): Remove this once the TLS manager no longer accesses global
|
||||
// variables. Make tests that use this helper concurrent.
|
||||
@@ -127,12 +123,14 @@ func storeGlobals(tb testing.TB) {
|
||||
storage := globalContext.clients.storage
|
||||
dnsServer := globalContext.dnsServer
|
||||
mux := globalContext.mux
|
||||
web := globalContext.web
|
||||
|
||||
tb.Cleanup(func() {
|
||||
config = prevConfig
|
||||
globalContext.clients.storage = storage
|
||||
globalContext.dnsServer = dnsServer
|
||||
globalContext.mux = mux
|
||||
globalContext.web = web
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,8 +202,6 @@ func assertCertSerialNumber(tb testing.TB, conf *tlsConfigSettings, wantSN int64
|
||||
func TestTLSManager_Reload(t *testing.T) {
|
||||
storeGlobals(t)
|
||||
|
||||
config.DNS.Port = 0
|
||||
|
||||
var (
|
||||
logger = slogutil.NewDiscardLogger()
|
||||
ctx = testutil.ContextWithTimeout(t, testTimeout)
|
||||
@@ -225,6 +221,9 @@ func TestTLSManager_Reload(t *testing.T) {
|
||||
|
||||
globalContext.mux = http.NewServeMux()
|
||||
|
||||
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
const (
|
||||
snBefore int64 = 1
|
||||
snAfter int64 = 2
|
||||
@@ -237,24 +236,17 @@ func TestTLSManager_Reload(t *testing.T) {
|
||||
certDER, key := newCertAndKey(t, snBefore)
|
||||
writeCertAndKey(t, certDER, certPath, key, keyPath)
|
||||
|
||||
m, err := newTLSManager(ctx, &tlsManagerConfig{
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
m.setWebAPI(web)
|
||||
|
||||
conf := m.config()
|
||||
conf := &tlsConfigSettings{}
|
||||
m.WriteDiskConfig(conf)
|
||||
assertCertSerialNumber(t, conf, snBefore)
|
||||
|
||||
certDER, key = newCertAndKey(t, snAfter)
|
||||
@@ -262,11 +254,7 @@ func TestTLSManager_Reload(t *testing.T) {
|
||||
|
||||
m.reload(ctx)
|
||||
|
||||
// The [tlsManager.reload] method will start the DNS server and it should be
|
||||
// stopped after the test ends.
|
||||
testutil.CleanupAndRequireSuccess(t, globalContext.dnsServer.Stop)
|
||||
|
||||
conf = m.config()
|
||||
m.WriteDiskConfig(conf)
|
||||
assertCertSerialNumber(t, conf, snAfter)
|
||||
}
|
||||
|
||||
@@ -277,16 +265,13 @@ func TestTLSManager_HandleTLSStatus(t *testing.T) {
|
||||
err error
|
||||
)
|
||||
|
||||
m, err := newTLSManager(ctx, &tlsManagerConfig{
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificateChain: string(testCertChainData),
|
||||
PrivateKey: string(testPrivateKeyData),
|
||||
},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -306,86 +291,50 @@ func TestTLSManager_HandleTLSStatus(t *testing.T) {
|
||||
func TestValidateTLSSettings(t *testing.T) {
|
||||
storeGlobals(t)
|
||||
|
||||
globalContext.mux = http.NewServeMux()
|
||||
|
||||
var (
|
||||
logger = slogutil.NewDiscardLogger()
|
||||
ctx = testutil.ContextWithTimeout(t, testTimeout)
|
||||
err error
|
||||
)
|
||||
|
||||
m, err := newTLSManager(ctx, &tlsManagerConfig{
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
testutil.CleanupAndRequireSuccess(t, ln.Close)
|
||||
|
||||
addr := testutil.RequireTypeAssert[*net.TCPAddr](t, ln.Addr())
|
||||
|
||||
busyPort := addr.Port
|
||||
|
||||
globalContext.mux = http.NewServeMux()
|
||||
|
||||
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
m.setWebAPI(web)
|
||||
|
||||
tcpLn, err := net.Listen("tcp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, tcpLn.Close)
|
||||
|
||||
tcpAddr := testutil.RequireTypeAssert[*net.TCPAddr](t, tcpLn.Addr())
|
||||
busyTCPPort := tcpAddr.Port
|
||||
|
||||
udpLn, err := net.ListenPacket("udp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, udpLn.Close)
|
||||
|
||||
udpAddr := testutil.RequireTypeAssert[*net.UDPAddr](t, udpLn.LocalAddr())
|
||||
busyUDPPort := udpAddr.Port
|
||||
|
||||
testCases := []struct {
|
||||
setts tlsConfigSettingsExt
|
||||
name string
|
||||
wantErr string
|
||||
setts tlsConfigSettingsExt
|
||||
}{{
|
||||
name: "basic",
|
||||
wantErr: "",
|
||||
setts: tlsConfigSettingsExt{},
|
||||
wantErr: "",
|
||||
}, {
|
||||
name: "disabled_all",
|
||||
wantErr: "plain DNS is required in case encryption protocols are disabled",
|
||||
setts: tlsConfigSettingsExt{
|
||||
ServePlainDNS: aghalg.NBFalse,
|
||||
},
|
||||
name: "disabled_all",
|
||||
wantErr: "plain DNS is required in case encryption protocols are disabled",
|
||||
}, {
|
||||
name: "busy_https_port",
|
||||
wantErr: fmt.Sprintf("port %d for HTTPS is not available", busyTCPPort),
|
||||
setts: tlsConfigSettingsExt{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
PortHTTPS: uint16(busyTCPPort),
|
||||
PortHTTPS: uint16(busyPort),
|
||||
},
|
||||
},
|
||||
name: "busy_port",
|
||||
wantErr: fmt.Sprintf("port %d is not available, cannot enable HTTPS on it", busyPort),
|
||||
}, {
|
||||
name: "busy_dot_port",
|
||||
wantErr: fmt.Sprintf("port %d for DNS-over-TLS is not available", busyTCPPort),
|
||||
setts: tlsConfigSettingsExt{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
PortDNSOverTLS: uint16(busyTCPPort),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "busy_doq_port",
|
||||
wantErr: fmt.Sprintf("port %d for DNS-over-QUIC is not available", busyUDPPort),
|
||||
setts: tlsConfigSettingsExt{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
PortDNSOverQUIC: uint16(busyUDPPort),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "duplicate_port",
|
||||
wantErr: "validating tcp ports: duplicated values: [4433]",
|
||||
setts: tlsConfigSettingsExt{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
@@ -393,11 +342,13 @@ func TestValidateTLSSettings(t *testing.T) {
|
||||
PortDNSOverTLS: 4433,
|
||||
},
|
||||
},
|
||||
name: "duplicate_port",
|
||||
wantErr: "validating tcp ports: duplicated values: [4433]",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err = m.validateTLSSettings(tc.setts)
|
||||
err = validateTLSSettings(tc.setts)
|
||||
testutil.AssertErrorMsg(t, tc.wantErr, err)
|
||||
})
|
||||
}
|
||||
@@ -406,36 +357,33 @@ func TestValidateTLSSettings(t *testing.T) {
|
||||
func TestTLSManager_HandleTLSValidate(t *testing.T) {
|
||||
storeGlobals(t)
|
||||
|
||||
globalContext.mux = http.NewServeMux()
|
||||
|
||||
var (
|
||||
logger = slogutil.NewDiscardLogger()
|
||||
ctx = testutil.ContextWithTimeout(t, testTimeout)
|
||||
err error
|
||||
)
|
||||
|
||||
m, err := newTLSManager(ctx, &tlsManagerConfig{
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
globalContext.mux = http.NewServeMux()
|
||||
|
||||
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificateChain: string(testCertChainData),
|
||||
PrivateKey: string(testPrivateKeyData),
|
||||
},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
}, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
m.setWebAPI(web)
|
||||
|
||||
setts := &tlsConfigSettingsExt{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
|
||||
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
|
||||
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -473,7 +421,6 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
err = globalContext.dnsServer.Prepare(&dnsforward.ServerConfig{
|
||||
TLSConf: &dnsforward.TLSConfig{},
|
||||
Config: dnsforward.Config{
|
||||
UpstreamMode: dnsforward.UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &dnsforward.EDNSClientSubnet{Enabled: false},
|
||||
@@ -491,6 +438,9 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
|
||||
|
||||
globalContext.mux = http.NewServeMux()
|
||||
|
||||
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
config.DNS.BindHosts = []netip.Addr{netip.MustParseAddr("127.0.0.1")}
|
||||
config.DNS.Port = 0
|
||||
|
||||
@@ -505,33 +455,28 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
|
||||
writeCertAndKey(t, certDER, certPath, key, keyPath)
|
||||
|
||||
// Initialize the TLS manager and assert its configuration.
|
||||
m, err := newTLSManager(ctx, &tlsManagerConfig{
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
},
|
||||
servePlainDNS: true,
|
||||
})
|
||||
}, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
m.setWebAPI(web)
|
||||
|
||||
conf := m.config()
|
||||
conf := &tlsConfigSettings{}
|
||||
m.WriteDiskConfig(conf)
|
||||
assertCertSerialNumber(t, conf, wantSerialNumber)
|
||||
|
||||
// Prepare a request with the new TLS configuration.
|
||||
setts := &tlsConfigSettingsExt{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
PortHTTPS: 4433,
|
||||
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
|
||||
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
|
||||
Enabled: true,
|
||||
PortHTTPS: 4433,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificateChain: base64.StdEncoding.EncodeToString(testCertChainData),
|
||||
PrivateKey: base64.StdEncoding.EncodeToString(testPrivateKeyData),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -564,10 +509,10 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
|
||||
//
|
||||
// TODO(s.chzhen): Remove when [httpsServer.cond] is removed.
|
||||
assert.Eventually(t, func() bool {
|
||||
web.httpsServer.condLock.Lock()
|
||||
defer web.httpsServer.condLock.Unlock()
|
||||
globalContext.web.httpsServer.condLock.Lock()
|
||||
defer globalContext.web.httpsServer.condLock.Unlock()
|
||||
|
||||
cert = web.httpsServer.cert
|
||||
cert = globalContext.web.httpsServer.cert
|
||||
if cert.Leaf == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/netutil/httputil"
|
||||
@@ -156,9 +158,30 @@ func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) {
|
||||
return w
|
||||
}
|
||||
|
||||
// webCheckPortAvailable checks if port, which is considered an HTTPS port, is
|
||||
// available, unless the HTTPS server isn't active.
|
||||
//
|
||||
// TODO(a.garipov): Adapt for HTTP/3.
|
||||
func webCheckPortAvailable(port uint16) (ok bool) {
|
||||
if globalContext.web.httpsServer.server != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
addrPort := netip.AddrPortFrom(config.HTTPConfig.Address.Addr(), port)
|
||||
|
||||
err := aghnet.CheckPort("tcp", addrPort)
|
||||
if err != nil {
|
||||
log.Info("web: warning: checking https port: %s", err)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server
|
||||
// if necessary. tlsConf must not be nil.
|
||||
func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf *tlsConfigSettings) {
|
||||
// if necessary.
|
||||
func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
|
||||
defer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure)
|
||||
|
||||
web.logger.DebugContext(ctx, "applying new tls configuration")
|
||||
@@ -306,8 +329,8 @@ func (web *webAPI) tlsServerLoop(ctx context.Context) {
|
||||
Handler: hdlr,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{web.httpsServer.cert},
|
||||
RootCAs: web.tlsManager.rootCerts,
|
||||
CipherSuites: web.tlsManager.customCipherIDs,
|
||||
RootCAs: globalContext.tlsRoots,
|
||||
CipherSuites: globalContext.tlsCipherIDs,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
ReadTimeout: web.conf.ReadTimeout,
|
||||
@@ -340,8 +363,8 @@ func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {
|
||||
Addr: address,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{web.httpsServer.cert},
|
||||
RootCAs: web.tlsManager.rootCerts,
|
||||
CipherSuites: web.tlsManager.customCipherIDs,
|
||||
RootCAs: globalContext.tlsRoots,
|
||||
CipherSuites: globalContext.tlsCipherIDs,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
Handler: withMiddlewares(globalContext.mux, limitRequestBody),
|
||||
|
||||
@@ -64,7 +64,7 @@ type Entry struct {
|
||||
Domain string
|
||||
|
||||
// UpstreamStats contains the DNS query statistics for both the upstream and
|
||||
// fallback DNS servers. Don't modify items in the slice.
|
||||
// fallback DNS servers.
|
||||
UpstreamStats []*proxy.UpstreamStatistics
|
||||
|
||||
// Result is the result of processing the request.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/ioutil"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/c2h5oh/datasize"
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ const maxVersionRespSize datasize.ByteSize = 64 * datasize.KB
|
||||
|
||||
// VersionInfo downloads the latest version information. If forceRecheck is
|
||||
// false and there are cached results, those results are returned.
|
||||
func (u *Updater) VersionInfo(ctx context.Context, forceRecheck bool) (vi VersionInfo, err error) {
|
||||
func (u *Updater) VersionInfo(forceRecheck bool) (vi VersionInfo, err error) {
|
||||
u.mu.Lock()
|
||||
defer u.mu.Unlock()
|
||||
|
||||
@@ -45,17 +45,11 @@ func (u *Updater) VersionInfo(ctx context.Context, forceRecheck bool) (vi Versio
|
||||
return u.prevCheckResult, u.prevCheckError
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
vcu := u.versionCheckURL
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, vcu, nil)
|
||||
resp, err = u.client.Get(vcu)
|
||||
if err != nil {
|
||||
return VersionInfo{}, fmt.Errorf("constructing request to %s: %w", vcu, err)
|
||||
}
|
||||
|
||||
u.logger.DebugContext(ctx, "requesting version data", "url", vcu)
|
||||
|
||||
resp, err := u.client.Do(req)
|
||||
if err != nil {
|
||||
return VersionInfo{}, fmt.Errorf("requesting %s: %w", vcu, err)
|
||||
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
|
||||
|
||||
@@ -65,16 +59,16 @@ func (u *Updater) VersionInfo(ctx context.Context, forceRecheck bool) (vi Versio
|
||||
// ReadCloser.
|
||||
body, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return VersionInfo{}, fmt.Errorf("reading response from %s: %w", vcu, err)
|
||||
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
|
||||
}
|
||||
|
||||
u.prevCheckTime = now
|
||||
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(ctx, body)
|
||||
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
|
||||
|
||||
return u.prevCheckResult, u.prevCheckError
|
||||
}
|
||||
|
||||
func (u *Updater) parseVersionResponse(ctx context.Context, data []byte) (VersionInfo, error) {
|
||||
func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
|
||||
info := VersionInfo{
|
||||
CanAutoUpdate: aghalg.NBFalse,
|
||||
}
|
||||
@@ -98,7 +92,7 @@ func (u *Updater) parseVersionResponse(ctx context.Context, data []byte) (Versio
|
||||
info.Announcement = versionJSON["announcement"]
|
||||
info.AnnouncementURL = versionJSON["announcement_url"]
|
||||
|
||||
packageURL, key, found := u.downloadURL(ctx, versionJSON)
|
||||
packageURL, key, found := u.downloadURL(versionJSON)
|
||||
if !found {
|
||||
return info, fmt.Errorf("version.json: no package URL: key %q not found in object", key)
|
||||
}
|
||||
@@ -114,10 +108,7 @@ func (u *Updater) parseVersionResponse(ctx context.Context, data []byte) (Versio
|
||||
// downloadURL returns the download URL for current build as well as its key in
|
||||
// versionObj. If the key is not found, it additionally prints an informative
|
||||
// log message.
|
||||
func (u *Updater) downloadURL(
|
||||
ctx context.Context,
|
||||
versionObj map[string]string,
|
||||
) (dlURL, key string, ok bool) {
|
||||
func (u *Updater) downloadURL(versionObj map[string]string) (dlURL, key string, ok bool) {
|
||||
if u.goarch == "arm" && u.goarm != "" {
|
||||
key = fmt.Sprintf("download_%s_%sv%s", u.goos, u.goarch, u.goarm)
|
||||
} else if isMIPS(u.goarch) && u.gomips != "" {
|
||||
@@ -133,7 +124,7 @@ func (u *Updater) downloadURL(
|
||||
|
||||
keys := slices.Sorted(maps.Keys(versionObj))
|
||||
|
||||
u.logger.ErrorContext(ctx, "key not found", "missing", key, "got", keys)
|
||||
log.Error("updater: key %q not found; got keys %q", key, keys)
|
||||
|
||||
return "", key, false
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -59,7 +58,6 @@ func TestUpdater_VersionInfo(t *testing.T) {
|
||||
|
||||
u := updater.NewUpdater(&updater.Config{
|
||||
Client: srv.Client(),
|
||||
Logger: testLogger,
|
||||
Version: "v0.103.0-beta.1",
|
||||
Channel: version.ChannelBeta,
|
||||
GOARCH: "arm",
|
||||
@@ -67,8 +65,7 @@ func TestUpdater_VersionInfo(t *testing.T) {
|
||||
VersionCheckURL: fakeURL,
|
||||
})
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
info, err := u.VersionInfo(ctx, false)
|
||||
info, err := u.VersionInfo(false)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, counter, 1)
|
||||
@@ -78,14 +75,14 @@ func TestUpdater_VersionInfo(t *testing.T) {
|
||||
assert.Equal(t, aghalg.NBTrue, info.CanAutoUpdate)
|
||||
|
||||
t.Run("cache_check", func(t *testing.T) {
|
||||
_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), false)
|
||||
_, err = u.VersionInfo(false)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, counter, 1)
|
||||
})
|
||||
|
||||
t.Run("force_check", func(t *testing.T) {
|
||||
_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), true)
|
||||
_, err = u.VersionInfo(true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, counter, 2)
|
||||
@@ -94,7 +91,7 @@ func TestUpdater_VersionInfo(t *testing.T) {
|
||||
t.Run("api_fail", func(t *testing.T) {
|
||||
srv.Close()
|
||||
|
||||
_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), true)
|
||||
_, err = u.VersionInfo(true)
|
||||
var urlErr *url.Error
|
||||
assert.ErrorAs(t, err, &urlErr)
|
||||
})
|
||||
@@ -133,7 +130,6 @@ func TestUpdater_VersionInfo_others(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
u := updater.NewUpdater(&updater.Config{
|
||||
Client: fakeClient,
|
||||
Logger: testLogger,
|
||||
Version: "v0.103.0-beta.1",
|
||||
Channel: version.ChannelBeta,
|
||||
GOOS: "linux",
|
||||
@@ -143,8 +139,7 @@ func TestUpdater_VersionInfo_others(t *testing.T) {
|
||||
VersionCheckURL: fakeURL,
|
||||
})
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
info, err := u.VersionInfo(ctx, false)
|
||||
info, err := u.VersionInfo(false)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "v0.103.0-beta.2", info.NewVersion)
|
||||
|
||||
@@ -5,11 +5,9 @@ import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -24,14 +22,13 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/ioutil"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||
)
|
||||
|
||||
// Updater is the AdGuard Home updater.
|
||||
type Updater struct {
|
||||
client *http.Client
|
||||
logger *slog.Logger
|
||||
|
||||
version string
|
||||
channel string
|
||||
@@ -78,48 +75,27 @@ func DefaultVersionURL() *url.URL {
|
||||
|
||||
// Config is the AdGuard Home updater configuration.
|
||||
type Config struct {
|
||||
// Client is used to perform HTTP requests. It must not be nil.
|
||||
Client *http.Client
|
||||
|
||||
// Logger is used for logging the update process. It must not be nil.
|
||||
Logger *slog.Logger
|
||||
|
||||
// VersionCheckURL is URL to the latest version announcement. It must not
|
||||
// be nil, see [DefaultVersionURL].
|
||||
VersionCheckURL *url.URL
|
||||
|
||||
// Version is the current AdGuard Home version. It must not be empty.
|
||||
Version string
|
||||
|
||||
// Channel is the current AdGuard Home update channel. It must be a valid
|
||||
// channel, see [version.ChannelBeta] and the related constants.
|
||||
Channel string
|
||||
GOARCH string
|
||||
GOOS string
|
||||
GOARM string
|
||||
GOMIPS string
|
||||
|
||||
// GOARCH is the current CPU architecture. It must not be empty and must be
|
||||
// one of the supported architectures.
|
||||
GOARCH string
|
||||
|
||||
// GOOS is the current operating system. It must not be empty and must be
|
||||
// one of the supported OSs.
|
||||
GOOS string
|
||||
|
||||
// GOARM is the current ARM variant, if any. It must either be empty or be
|
||||
// a valid and supported GOARM value.
|
||||
GOARM string
|
||||
|
||||
// GOMIPS is the current MIPS variant, if any. It must either be empty or
|
||||
// be a valid and supported GOMIPS value.
|
||||
GOMIPS string
|
||||
|
||||
// ConfName is the name of the current configuration file. It must not be
|
||||
// empty.
|
||||
// ConfName is the name of the current configuration file. Typically,
|
||||
// "AdGuardHome.yaml".
|
||||
ConfName string
|
||||
|
||||
// WorkDir is the working directory that is used for temporary files. It
|
||||
// must not be empty.
|
||||
// WorkDir is the working directory that is used for temporary files.
|
||||
WorkDir string
|
||||
|
||||
// ExecPath is path to the executable file. It must not be empty.
|
||||
// ExecPath is path to the executable file.
|
||||
ExecPath string
|
||||
}
|
||||
|
||||
@@ -127,7 +103,6 @@ type Config struct {
|
||||
func NewUpdater(conf *Config) *Updater {
|
||||
return &Updater{
|
||||
client: conf.Client,
|
||||
logger: conf.Logger,
|
||||
|
||||
version: conf.Version,
|
||||
channel: conf.Channel,
|
||||
@@ -147,49 +122,49 @@ func NewUpdater(conf *Config) *Updater {
|
||||
|
||||
// Update performs the auto-update. It returns an error if the update failed.
|
||||
// If firstRun is true, it assumes the configuration file doesn't exist.
|
||||
func (u *Updater) Update(ctx context.Context, firstRun bool) (err error) {
|
||||
func (u *Updater) Update(firstRun bool) (err error) {
|
||||
u.mu.Lock()
|
||||
defer u.mu.Unlock()
|
||||
|
||||
u.logger.InfoContext(ctx, "staring update", "first_run", firstRun)
|
||||
log.Info("updater: updating")
|
||||
defer func() {
|
||||
if err != nil {
|
||||
u.logger.ErrorContext(ctx, "update failed", slogutil.KeyError, err)
|
||||
log.Info("updater: failed")
|
||||
} else {
|
||||
u.logger.InfoContext(ctx, "update finished")
|
||||
log.Info("updater: finished successfully")
|
||||
}
|
||||
}()
|
||||
|
||||
err = u.prepare(ctx)
|
||||
err = u.prepare()
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing: %w", err)
|
||||
}
|
||||
|
||||
defer u.clean(ctx)
|
||||
defer u.clean()
|
||||
|
||||
err = u.downloadPackageFile(ctx)
|
||||
err = u.downloadPackageFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("downloading package file: %w", err)
|
||||
}
|
||||
|
||||
err = u.unpack(ctx)
|
||||
err = u.unpack()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unpacking: %w", err)
|
||||
}
|
||||
|
||||
if !firstRun {
|
||||
err = u.check(ctx)
|
||||
err = u.check()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = u.backup(ctx, firstRun)
|
||||
err = u.backup(firstRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making backup: %w", err)
|
||||
}
|
||||
|
||||
err = u.replace(ctx)
|
||||
err = u.replace()
|
||||
if err != nil {
|
||||
return fmt.Errorf("replacing: %w", err)
|
||||
}
|
||||
@@ -206,7 +181,7 @@ func (u *Updater) NewVersion() (nv string) {
|
||||
}
|
||||
|
||||
// prepare fills all necessary fields in Updater object.
|
||||
func (u *Updater) prepare(ctx context.Context) (err error) {
|
||||
func (u *Updater) prepare() (err error) {
|
||||
u.updateDir = filepath.Join(u.workDir, fmt.Sprintf("agh-update-%s", u.newVersion))
|
||||
|
||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||
@@ -225,12 +200,11 @@ func (u *Updater) prepare(ctx context.Context) (err error) {
|
||||
u.backupExeName = filepath.Join(u.backupDir, filepath.Base(u.execPath))
|
||||
u.updateExeName = filepath.Join(u.updateDir, updateExeName)
|
||||
|
||||
u.logger.InfoContext(
|
||||
ctx,
|
||||
"updating",
|
||||
"from", version.Version(),
|
||||
"to", u.newVersion,
|
||||
"package_url", u.packageURL,
|
||||
log.Debug(
|
||||
"updater: updating from %s to %s using url: %s",
|
||||
version.Version(),
|
||||
u.newVersion,
|
||||
u.packageURL,
|
||||
)
|
||||
|
||||
u.currentExeName = u.execPath
|
||||
@@ -243,20 +217,23 @@ func (u *Updater) prepare(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
// unpack extracts the files from the downloaded archive.
|
||||
func (u *Updater) unpack(ctx context.Context) (err error) {
|
||||
func (u *Updater) unpack() error {
|
||||
var err error
|
||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||
|
||||
u.logger.InfoContext(ctx, "unpacking package", "package_name", pkgNameOnly)
|
||||
log.Debug("updater: unpacking package")
|
||||
if strings.HasSuffix(pkgNameOnly, ".zip") {
|
||||
u.unpackedFiles, err = u.unpackZip(ctx, u.packageName, u.updateDir)
|
||||
u.unpackedFiles, err = zipFileUnpack(u.packageName, u.updateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf(".zip unpack failed: %w", err)
|
||||
}
|
||||
|
||||
} else if strings.HasSuffix(pkgNameOnly, ".tar.gz") {
|
||||
u.unpackedFiles, err = u.unpackTarGz(ctx, u.packageName, u.updateDir)
|
||||
u.unpackedFiles, err = tarGzFileUnpack(u.packageName, u.updateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf(".tar.gz unpack failed: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("unknown package extension")
|
||||
}
|
||||
@@ -266,8 +243,8 @@ func (u *Updater) unpack(ctx context.Context) (err error) {
|
||||
|
||||
// check returns an error if the configuration file couldn't be used with the
|
||||
// version of AdGuard Home just downloaded.
|
||||
func (u *Updater) check(ctx context.Context) (err error) {
|
||||
u.logger.InfoContext(ctx, "checking configuration")
|
||||
func (u *Updater) check() (err error) {
|
||||
log.Debug("updater: checking configuration")
|
||||
|
||||
err = copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"), aghos.DefaultPermFile)
|
||||
if err != nil {
|
||||
@@ -291,9 +268,8 @@ func (u *Updater) check(ctx context.Context) (err error) {
|
||||
|
||||
// backup makes a backup of the current configuration and supporting files. It
|
||||
// ignores the configuration file if firstRun is true.
|
||||
func (u *Updater) backup(ctx context.Context, firstRun bool) (err error) {
|
||||
u.logger.InfoContext(ctx, "backing up current configuration")
|
||||
|
||||
func (u *Updater) backup(firstRun bool) (err error) {
|
||||
log.Debug("updater: backing up current configuration")
|
||||
_ = os.Mkdir(u.backupDir, aghos.DefaultPermDir)
|
||||
if !firstRun {
|
||||
err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"), aghos.DefaultPermFile)
|
||||
@@ -303,7 +279,7 @@ func (u *Updater) backup(ctx context.Context, firstRun bool) (err error) {
|
||||
}
|
||||
|
||||
wd := u.workDir
|
||||
err = u.copySupportingFiles(ctx, u.unpackedFiles, wd, u.backupDir)
|
||||
err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %w", wd, u.backupDir, err)
|
||||
}
|
||||
@@ -313,18 +289,13 @@ func (u *Updater) backup(ctx context.Context, firstRun bool) (err error) {
|
||||
|
||||
// replace moves the current executable with the updated one and also copies the
|
||||
// supporting files.
|
||||
func (u *Updater) replace(ctx context.Context) (err error) {
|
||||
err = u.copySupportingFiles(ctx, u.unpackedFiles, u.updateDir, u.workDir)
|
||||
func (u *Updater) replace() error {
|
||||
err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %w", u.updateDir, u.workDir, err)
|
||||
}
|
||||
|
||||
u.logger.InfoContext(
|
||||
ctx,
|
||||
"backing up current executable",
|
||||
"from", u.currentExeName,
|
||||
"to", u.backupExeName,
|
||||
)
|
||||
log.Debug("updater: renaming: %s to %s", u.currentExeName, u.backupExeName)
|
||||
err = os.Rename(u.currentExeName, u.backupExeName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -340,22 +311,14 @@ func (u *Updater) replace(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
u.logger.InfoContext(
|
||||
ctx,
|
||||
"replacing current executable",
|
||||
"from", u.updateExeName,
|
||||
"to", u.currentExeName,
|
||||
)
|
||||
log.Debug("updater: renamed: %s to %s", u.updateExeName, u.currentExeName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clean removes the temporary directory itself and all it's contents.
|
||||
func (u *Updater) clean(ctx context.Context) {
|
||||
err := os.RemoveAll(u.updateDir)
|
||||
if err != nil {
|
||||
u.logger.WarnContext(ctx, "removing update dir", slogutil.KeyError, err)
|
||||
}
|
||||
func (u *Updater) clean() {
|
||||
_ = os.RemoveAll(u.updateDir)
|
||||
}
|
||||
|
||||
// MaxPackageFileSize is a maximum package file length in bytes. The largest
|
||||
@@ -364,52 +327,34 @@ func (u *Updater) clean(ctx context.Context) {
|
||||
const MaxPackageFileSize = 32 * 1024 * 1024
|
||||
|
||||
// Download package file and save it to disk
|
||||
func (u *Updater) downloadPackageFile(ctx context.Context) (err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.packageURL, nil)
|
||||
func (u *Updater) downloadPackageFile() (err error) {
|
||||
var resp *http.Response
|
||||
resp, err = u.client.Get(u.packageURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("constructing package request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := u.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("requesting package: %w", err)
|
||||
return fmt.Errorf("http request failed: %w", err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
|
||||
|
||||
r := ioutil.LimitReader(resp.Body, MaxPackageFileSize)
|
||||
|
||||
u.logger.InfoContext(ctx, "reading http body")
|
||||
|
||||
log.Debug("updater: reading http body")
|
||||
// This use of ReadAll is now safe, because we limited body's Reader.
|
||||
body, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("io.ReadAll() failed: %w", err)
|
||||
}
|
||||
|
||||
err = os.Mkdir(u.updateDir, aghos.DefaultPermDir)
|
||||
if err != nil {
|
||||
// TODO(a.garipov): Consider returning this error.
|
||||
u.logger.WarnContext(ctx, "creating update dir", slogutil.KeyError, err)
|
||||
}
|
||||
|
||||
u.logger.InfoContext(ctx, "saving package", "to", u.packageName)
|
||||
_ = os.Mkdir(u.updateDir, aghos.DefaultPermDir)
|
||||
|
||||
log.Debug("updater: saving package to file")
|
||||
err = os.WriteFile(u.packageName, body, aghos.DefaultPermFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing package file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unpackTarGzFile unpacks one file from a .tar.gz archive into outDir. All
|
||||
// arguments must not be empty.
|
||||
func (u *Updater) unpackTarGzFile(
|
||||
ctx context.Context,
|
||||
outDir string,
|
||||
tr *tar.Reader,
|
||||
hdr *tar.Header,
|
||||
) (name string, err error) {
|
||||
func tarGzFileUnpackOne(outDir string, tr *tar.Reader, hdr *tar.Header) (name string, err error) {
|
||||
name = filepath.Base(hdr.Name)
|
||||
if name == "" {
|
||||
return "", nil
|
||||
@@ -432,18 +377,13 @@ func (u *Updater) unpackTarGzFile(
|
||||
return "", fmt.Errorf("creating directory %q: %w", outName, err)
|
||||
}
|
||||
|
||||
u.logger.InfoContext(ctx, "created directory", "name", outName)
|
||||
log.Debug("updater: created directory %q", outName)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if hdr.Typeflag != tar.TypeReg {
|
||||
u.logger.WarnContext(
|
||||
ctx,
|
||||
"unknown file type; skipping",
|
||||
"file_name", name,
|
||||
"type", hdr.Typeflag,
|
||||
)
|
||||
log.Info("updater: %s: unknown file type %d, skipping", name, hdr.Typeflag)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
@@ -460,19 +400,16 @@ func (u *Updater) unpackTarGzFile(
|
||||
return "", fmt.Errorf("io.Copy(): %w", err)
|
||||
}
|
||||
|
||||
u.logger.InfoContext(ctx, "created file", "name", outName)
|
||||
log.Debug("updater: created file %q", outName)
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// unpackTarGz unpack all files from a .tar.gz archive to outDir. Existing
|
||||
// files are overwritten. All files are created inside outDir. files are the
|
||||
// list of created files.
|
||||
func (u *Updater) unpackTarGz(
|
||||
ctx context.Context,
|
||||
tarfile string,
|
||||
outDir string,
|
||||
) (files []string, err error) {
|
||||
// Unpack all files from .tar.gz file to the specified directory
|
||||
// Existing files are overwritten
|
||||
// All files are created inside outDir, subdirectories are not created
|
||||
// Return the list of files (not directories) written
|
||||
func tarGzFileUnpack(tarfile, outDir string) (files []string, err error) {
|
||||
f, err := os.Open(tarfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("os.Open(): %w", err)
|
||||
@@ -500,7 +437,7 @@ func (u *Updater) unpackTarGz(
|
||||
}
|
||||
|
||||
var name string
|
||||
name, err = u.unpackTarGzFile(ctx, outDir, tarReader, hdr)
|
||||
name, err = tarGzFileUnpackOne(outDir, tarReader, hdr)
|
||||
|
||||
if name != "" {
|
||||
files = append(files, name)
|
||||
@@ -510,13 +447,7 @@ func (u *Updater) unpackTarGz(
|
||||
return files, err
|
||||
}
|
||||
|
||||
// unpackZipFile unpacks one file from a .zip archive into outDir. All
|
||||
// arguments must not be empty.
|
||||
func (u *Updater) unpackZipFile(
|
||||
ctx context.Context,
|
||||
outDir string,
|
||||
zf *zip.File,
|
||||
) (name string, err error) {
|
||||
func zipFileUnpackOne(outDir string, zf *zip.File) (name string, err error) {
|
||||
var rc io.ReadCloser
|
||||
rc, err = zf.Open()
|
||||
if err != nil {
|
||||
@@ -535,8 +466,7 @@ func (u *Updater) unpackZipFile(
|
||||
if name == "AdGuardHome" {
|
||||
// Top-level AdGuardHome/. Skip it.
|
||||
//
|
||||
// TODO(a.garipov): See the similar TODO in
|
||||
// [Updater.unpackTarGzFile].
|
||||
// TODO(a.garipov): See the similar todo in tarGzFileUnpack.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -545,7 +475,7 @@ func (u *Updater) unpackZipFile(
|
||||
return "", fmt.Errorf("creating directory %q: %w", outputName, err)
|
||||
}
|
||||
|
||||
u.logger.InfoContext(ctx, "created directory", "name", outputName)
|
||||
log.Debug("updater: created directory %q", outputName)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
@@ -562,19 +492,16 @@ func (u *Updater) unpackZipFile(
|
||||
return "", fmt.Errorf("io.Copy(): %w", err)
|
||||
}
|
||||
|
||||
u.logger.InfoContext(ctx, "created file", "name", outputName)
|
||||
log.Debug("updater: created file %q", outputName)
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// unpackZip unpack all files from a .zip archive to outDir. Existing files are
|
||||
// overwritten. All files are created inside outDir. files are the list of
|
||||
// created files.
|
||||
func (u *Updater) unpackZip(
|
||||
ctx context.Context,
|
||||
zipfile string,
|
||||
outDir string,
|
||||
) (files []string, err error) {
|
||||
// Unpack all files from .zip file to the specified directory
|
||||
// Existing files are overwritten
|
||||
// All files are created inside 'outDir', subdirectories are not created
|
||||
// Return the list of files (not directories) written
|
||||
func zipFileUnpack(zipfile, outDir string) (files []string, err error) {
|
||||
zrc, err := zip.OpenReader(zipfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zip.OpenReader(): %w", err)
|
||||
@@ -583,7 +510,7 @@ func (u *Updater) unpackZip(
|
||||
|
||||
for _, zf := range zrc.File {
|
||||
var name string
|
||||
name, err = u.unpackZipFile(ctx, outDir, zf)
|
||||
name, err = zipFileUnpackOne(outDir, zf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@@ -616,12 +543,7 @@ func copyFile(src, dst string, perm fs.FileMode) (err error) {
|
||||
// copySupportingFiles copies each file specified in files from srcdir to
|
||||
// dstdir. If a file specified as a path, only the name of the file is used.
|
||||
// It skips AdGuardHome, AdGuardHome.exe, and AdGuardHome.yaml.
|
||||
func (u *Updater) copySupportingFiles(
|
||||
ctx context.Context,
|
||||
files []string,
|
||||
srcdir string,
|
||||
dstdir string,
|
||||
) (err error) {
|
||||
func copySupportingFiles(files []string, srcdir, dstdir string) error {
|
||||
for _, f := range files {
|
||||
_, name := filepath.Split(f)
|
||||
if name == "AdGuardHome" || name == "AdGuardHome.exe" || name == "AdGuardHome.yaml" {
|
||||
@@ -631,12 +553,12 @@ func (u *Updater) copySupportingFiles(
|
||||
src := filepath.Join(srcdir, name)
|
||||
dst := filepath.Join(dstdir, name)
|
||||
|
||||
err = copyFile(src, dst, aghos.DefaultPermFile)
|
||||
err := copyFile(src, dst, aghos.DefaultPermFile)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
u.logger.InfoContext(ctx, "copied", "from", src, "to", dst)
|
||||
log.Debug("updater: copied: %q to %q", src, dst)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -59,7 +55,6 @@ func TestUpdater_internal(t *testing.T) {
|
||||
|
||||
u := NewUpdater(&Config{
|
||||
Client: fakeClient,
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
GOOS: tc.os,
|
||||
Version: "v0.103.0",
|
||||
ExecPath: exePath,
|
||||
@@ -73,13 +68,13 @@ func TestUpdater_internal(t *testing.T) {
|
||||
u.newVersion = "v0.103.1"
|
||||
u.packageURL = fakeURL.String()
|
||||
|
||||
require.NoError(t, u.prepare(newCtx(t)))
|
||||
require.NoError(t, u.downloadPackageFile(newCtx(t)))
|
||||
require.NoError(t, u.unpack(newCtx(t)))
|
||||
require.NoError(t, u.backup(newCtx(t), false))
|
||||
require.NoError(t, u.replace(newCtx(t)))
|
||||
require.NoError(t, u.prepare())
|
||||
require.NoError(t, u.downloadPackageFile())
|
||||
require.NoError(t, u.unpack())
|
||||
require.NoError(t, u.backup(false))
|
||||
require.NoError(t, u.replace())
|
||||
|
||||
u.clean(newCtx(t))
|
||||
u.clean()
|
||||
|
||||
require.True(t, t.Run("backup", func(t *testing.T) {
|
||||
var d []byte
|
||||
@@ -118,8 +113,3 @@ func TestUpdater_internal(t *testing.T) {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// newCtx is a helper that returns a new context with a timeout.
|
||||
func newCtx(tb testing.TB) (ctx context.Context) {
|
||||
return testutil.ContextWithTimeout(tb, 1*time.Second)
|
||||
}
|
||||
|
||||
@@ -10,21 +10,17 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testLogger is the common logger for tests.
|
||||
var testLogger = slogutil.NewDiscardLogger()
|
||||
func TestMain(m *testing.M) {
|
||||
testutil.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
func TestUpdater_Update(t *testing.T) {
|
||||
const jsonData = `{
|
||||
@@ -77,7 +73,6 @@ func TestUpdater_Update(t *testing.T) {
|
||||
|
||||
u := updater.NewUpdater(&updater.Config{
|
||||
Client: srv.Client(),
|
||||
Logger: testLogger,
|
||||
GOARCH: "amd64",
|
||||
GOOS: "linux",
|
||||
Version: "v0.103.0",
|
||||
@@ -87,12 +82,10 @@ func TestUpdater_Update(t *testing.T) {
|
||||
VersionCheckURL: versionCheckURL,
|
||||
})
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
_, err = u.VersionInfo(ctx, false)
|
||||
_, err = u.VersionInfo(false)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = testutil.ContextWithTimeout(t, testTimeout)
|
||||
err = u.Update(ctx, true)
|
||||
err = u.Update(true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check backup files
|
||||
@@ -131,15 +124,14 @@ func TestUpdater_Update(t *testing.T) {
|
||||
t.Skip("skipping config check test on windows")
|
||||
}
|
||||
|
||||
err = u.Update(testutil.ContextWithTimeout(t, testTimeout), false)
|
||||
err = u.Update(false)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("api_fail", func(t *testing.T) {
|
||||
srv.Close()
|
||||
|
||||
err = u.Update(testutil.ContextWithTimeout(t, testTimeout), true)
|
||||
|
||||
err = u.Update(true)
|
||||
var urlErr *url.Error
|
||||
assert.ErrorAs(t, err, &urlErr)
|
||||
})
|
||||
|
||||
@@ -6,6 +6,12 @@ We are using [OpenAPI specification](https://swagger.io/docs/specification/about
|
||||
|
||||
The easiest way would be to use [Swagger Editor](http://editor.swagger.io/) and just copy/paste the YAML file there.
|
||||
|
||||
## How to read the API doc
|
||||
|
||||
1. `yarn install`
|
||||
2. `yarn start`
|
||||
3. open `http://localhost:4000/`
|
||||
|
||||
## Changelog
|
||||
|
||||
[Here](CHANGELOG.md) we keep track of all non-compatible changes that are being made.
|
||||
|
||||
@@ -980,8 +980,7 @@
|
||||
- 'clients'
|
||||
'operationId': 'clientsSearch'
|
||||
'summary': >
|
||||
Retrieve information about clients by performing an exact match search
|
||||
using IP addresses, CIDRs, MAC addresses, or ClientIDs.
|
||||
Get information about clients by their IP addresses, CIDRs, MAC addresses, or ClientIDs.
|
||||
'requestBody':
|
||||
'content':
|
||||
'application/json':
|
||||
|
||||
12
openapi/package.json
Normal file
12
openapi/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "adguard-home-api",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node_modules/.bin/speccy serve -p 4000 openapi.yaml",
|
||||
"test": "node_modules/.bin/speccy lint openapi.yaml"
|
||||
},
|
||||
"dependencies": {
|
||||
"speccy": "^0.11.0"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user