Compare commits
276 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39e22ada96 | ||
|
|
5aee57e297 | ||
|
|
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 |
53
CHANGELOG.md
53
CHANGELOG.md
@@ -9,46 +9,18 @@ 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.)
|
||||
|
||||
See also the [v0.107.62 GitHub milestone][ms-v0.107.62].
|
||||
|
||||
[ms-v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/milestone/97?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
|
||||
## [v0.107.61] - 2025-04-22 (APPROX.)
|
||||
|
||||
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
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
|
||||
## [v0.107.60] - 2025-04-14
|
||||
|
||||
See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
|
||||
@@ -83,13 +55,15 @@ See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
|
||||
[#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
|
||||
[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].
|
||||
|
||||
### Fixed
|
||||
|
||||
- Rules with the `client` modifier not working ([#7708]).
|
||||
|
||||
- The search form not working in the query log ([#7704]).
|
||||
@@ -3130,12 +3104,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.61...HEAD
|
||||
[v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61
|
||||
-->
|
||||
|
||||
[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
|
||||
[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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.0'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
|
||||
'stages':
|
||||
@@ -278,7 +278,7 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.0'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
@@ -294,5 +294,5 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.0'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.0'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'channel': 'development'
|
||||
|
||||
@@ -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'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.0'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -656,7 +656,7 @@
|
||||
"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)",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"custom_ip": "Tilpasset IP",
|
||||
"blocking_ipv4": "IPv4-blokkering",
|
||||
"blocking_ipv6": "IPv6-blokkering",
|
||||
"blocked_response_ttl": "Blokkerte svars TTL",
|
||||
"blocked_response_ttl": "Blokkert svar TTL",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
|
||||
@@ -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": "输入缓存大小(字节)",
|
||||
|
||||
@@ -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": "輸入快取大小(位元組)",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
34
go.mod
34
go.mod
@@ -3,8 +3,8 @@ module github.com/AdguardTeam/AdGuardHome
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.75.5
|
||||
github.com/AdguardTeam/golibs v0.32.9
|
||||
github.com/AdguardTeam/dnsproxy v0.75.2
|
||||
github.com/AdguardTeam/golibs v0.32.7
|
||||
github.com/AdguardTeam/urlfilter v0.20.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/ameshkov/dnscrypt/v2 v2.4.0
|
||||
@@ -34,21 +34,21 @@ require (
|
||||
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/exp v0.0.0-20250305212735-054e65f0b394
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/sys v0.32.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 v0.120.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.8 // 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/ameshkov/dnsstamps v1.0.3 // indirect
|
||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
||||
@@ -61,7 +61,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-20250403155104-27863c87afa6 // 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
|
||||
@@ -89,26 +89,26 @@ require (
|
||||
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.1 // 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/sync v0.13.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20250406004356-f593adaf3fc1 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.32.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/api v0.228.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 // indirect
|
||||
google.golang.org/grpc v1.71.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // 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
|
||||
)
|
||||
|
||||
68
go.sum
68
go.sum
@@ -1,19 +1,19 @@
|
||||
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 v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
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.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
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.2 h1:bciOkzQh/GG8vcZGdFn6+rS3pu+2Npt9tbA4bNA/rsc=
|
||||
github.com/AdguardTeam/dnsproxy v0.75.2/go.mod h1:U/ouLftmXMIrkTAf8JepqbPuoQzsbXJo0Vxxn+LAdgA=
|
||||
github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4=
|
||||
github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA=
|
||||
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=
|
||||
@@ -72,8 +72,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-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
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=
|
||||
@@ -199,16 +199,16 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
|
||||
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.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||
go.uber.org/mock v0.5.1/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/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
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=
|
||||
@@ -227,8 +227,8 @@ 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/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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.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,10 +241,10 @@ 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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20250406004356-f593adaf3fc1 h1:LxyDqgHX2VuimV2UQSNFpQxz+NRUUsh8ulNcP3WvNG0=
|
||||
golang.org/x/telemetry v0.0.0-20250406004356-f593adaf3fc1/go.mod h1:RoaXAWDwS90j6FxVKwJdBV+0HCU+llrKUGgJaxiKl6M=
|
||||
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=
|
||||
@@ -268,12 +268,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
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/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
|
||||
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755 h1:AMLTAunltONNuzWgVPZXrjLWtXpsG6A3yLLPEoJ/IjU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250407143221-ac9807e6c755/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755 h1:TwXJCGVREgQ/cl18iY0Z4wJCTL/GmW+Um2oSwZiZPnc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250407143221-ac9807e6c755/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=
|
||||
@@ -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=
|
||||
|
||||
@@ -10,8 +10,7 @@ import (
|
||||
// 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].
|
||||
// NewLogin returns a web user login.
|
||||
//
|
||||
// TODO(s.chzhen): Add more constraints as needed.
|
||||
func NewLogin(s string) (l Login, err error) {
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
}
|
||||
@@ -32,13 +32,13 @@ func MustNewUserID() (uid UserID) {
|
||||
|
||||
// User represents a web user.
|
||||
type User struct {
|
||||
// Password stores the password information for the web user. It must not
|
||||
// be nil.
|
||||
Password Password
|
||||
// ID is the unique identifier for the web user. It must not be empty.
|
||||
ID UserID
|
||||
|
||||
// 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
|
||||
// Password stores the password information for the web user. It must not
|
||||
// be nil.
|
||||
Password Password
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -591,7 +498,7 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
|
||||
|
||||
foundMAC := s.dhcp.MACByIP(ip)
|
||||
if foundMAC != nil {
|
||||
return s.index.findByMAC(foundMAC)
|
||||
return s.FindByMAC(foundMAC)
|
||||
}
|
||||
|
||||
p = s.index.findByIPWithoutZone(ip)
|
||||
@@ -602,6 +509,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 +648,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,7 +682,7 @@ 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)
|
||||
}
|
||||
@@ -772,7 +690,7 @@ func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filter
|
||||
if !ok {
|
||||
foundMAC := s.dhcp.MACByIP(addr)
|
||||
if foundMAC != nil {
|
||||
c, ok = s.index.findByMAC(foundMAC)
|
||||
c, ok = s.FindByMAC(foundMAC)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
@@ -112,6 +111,9 @@ func initDNS(
|
||||
return err
|
||||
}
|
||||
|
||||
tlsConf := &tlsConfigSettings{}
|
||||
tlsMgr.WriteDiskConfig(tlsConf)
|
||||
|
||||
return initDNSServer(
|
||||
globalContext.filters,
|
||||
globalContext.stats,
|
||||
@@ -119,15 +121,16 @@ func initDNS(
|
||||
globalContext.dhcpServer,
|
||||
anonymizer,
|
||||
httpRegister,
|
||||
tlsConf,
|
||||
tlsMgr,
|
||||
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, 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.
|
||||
//
|
||||
// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.
|
||||
func initDNSServer(
|
||||
@@ -137,6 +140,7 @@ func initDNSServer(
|
||||
dhcpSrv dnsforward.DHCP,
|
||||
anonymizer *aghnet.IPMut,
|
||||
httpReg aghhttp.RegisterFunc,
|
||||
tlsConf *tlsConfigSettings,
|
||||
tlsMgr *tlsManager,
|
||||
l *slog.Logger,
|
||||
) (err error) {
|
||||
@@ -165,7 +169,7 @@ func initDNSServer(
|
||||
dnsConf, err := newServerConfig(
|
||||
&config.DNS,
|
||||
config.Clients.Sources,
|
||||
tlsMgr.config(),
|
||||
tlsConf,
|
||||
tlsMgr,
|
||||
httpReg,
|
||||
globalContext.clients.storage,
|
||||
@@ -251,16 +255,11 @@ 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,
|
||||
@@ -273,7 +272,6 @@ func newServerConfig(
|
||||
ServeHTTP3: dnsConf.ServeHTTP3,
|
||||
UseHTTP3Upstreams: dnsConf.UseHTTP3Upstreams,
|
||||
ServePlainDNS: dnsConf.ServePlainDNS,
|
||||
PendingRequestsEnabled: dnsConf.PendingRequests.Enabled,
|
||||
}
|
||||
|
||||
var initialAddresses []netip.Addr
|
||||
@@ -306,19 +304,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 +325,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 +378,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{}
|
||||
|
||||
@@ -487,14 +487,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 +500,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 +514,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 +523,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 +539,7 @@ func initWeb(
|
||||
}
|
||||
}
|
||||
|
||||
disableUpdate := !isUpdateEnabled(ctx, baseLogger, &opts, isCustomUpdURL)
|
||||
disableUpdate := !isUpdateEnabled(ctx, baseLogger, &opts, customURL)
|
||||
|
||||
webConf := &webConfig{
|
||||
updater: upd,
|
||||
@@ -650,12 +645,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, tlsMgr)
|
||||
|
||||
if !globalContext.firstRun {
|
||||
// Save the updated config.
|
||||
@@ -677,7 +671,7 @@ 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)
|
||||
web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
|
||||
fatalOnError(err)
|
||||
|
||||
globalContext.web = web
|
||||
@@ -720,17 +714,16 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
|
||||
<-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 +735,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 +751,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 +762,7 @@ func newUpdater(
|
||||
ConfName: confPath,
|
||||
ExecPath: execPath,
|
||||
VersionCheckURL: versionURL,
|
||||
}), isCustomURL
|
||||
}), customURL
|
||||
}
|
||||
|
||||
// checkPermissions checks and migrates permissions of the files and directories
|
||||
@@ -999,9 +991,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()
|
||||
@@ -1086,12 +1078,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{}, tlsMgr, 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 +1096,7 @@ func cmdlineUpdate(
|
||||
os.Exit(osutil.ExitCodeSuccess)
|
||||
}
|
||||
|
||||
err = upd.Update(ctx, globalContext.firstRun)
|
||||
err = upd.Update(globalContext.firstRun)
|
||||
fatalOnError(err)
|
||||
|
||||
err = restartService()
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ import (
|
||||
"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,9 +37,6 @@ 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
|
||||
|
||||
@@ -53,9 +52,6 @@ type tlsManager struct {
|
||||
// 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()
|
||||
@@ -63,6 +59,9 @@ type tlsManager struct {
|
||||
// customCipherIDs are the ID of the cipher suites that AdGuard Home must use.
|
||||
customCipherIDs []uint16
|
||||
|
||||
confLock sync.Mutex
|
||||
conf tlsConfigSettings
|
||||
|
||||
// servePlainDNS defines if plain DNS is allowed for incoming requests.
|
||||
servePlainDNS bool
|
||||
}
|
||||
@@ -92,10 +91,9 @@ type tlsManagerConfig struct {
|
||||
func newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager, err error) {
|
||||
m = &tlsManager{
|
||||
logger: conf.logger,
|
||||
mu: &sync.Mutex{},
|
||||
configModified: conf.configModified,
|
||||
status: &tlsConfigStatus{},
|
||||
conf: &conf.tlsSettings,
|
||||
conf: conf.tlsSettings,
|
||||
servePlainDNS: conf.servePlainDNS,
|
||||
}
|
||||
|
||||
@@ -114,22 +112,17 @@ func newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager,
|
||||
m.logger.InfoContext(ctx, "using default ciphers")
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.conf.Enabled {
|
||||
err = m.load(ctx)
|
||||
if err != nil {
|
||||
m.conf.Enabled = false
|
||||
|
||||
if !m.conf.Enabled {
|
||||
return m, nil
|
||||
return m, err
|
||||
}
|
||||
|
||||
m.setCertFileTime(ctx)
|
||||
}
|
||||
|
||||
err = m.load(ctx)
|
||||
if err != nil {
|
||||
m.conf.Enabled = false
|
||||
|
||||
return m, err
|
||||
}
|
||||
|
||||
m.setCertFileTime(ctx)
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -143,9 +136,8 @@ func (m *tlsManager) setWebAPI(webAPI *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 +145,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 +175,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)
|
||||
m.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 +211,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,6 +227,10 @@ 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.
|
||||
@@ -244,12 +238,15 @@ func (m *tlsManager) reload(ctx context.Context) {
|
||||
}
|
||||
|
||||
// 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,
|
||||
tlsConf,
|
||||
m,
|
||||
httpRegister,
|
||||
globalContext.clients.storage,
|
||||
@@ -266,11 +263,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 +357,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,23 +410,15 @@ 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)
|
||||
}
|
||||
@@ -447,9 +434,6 @@ 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
|
||||
}
|
||||
@@ -465,7 +449,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(ctx, &setts.tlsConfigSettings, status)
|
||||
resp := tlsConfig{
|
||||
tlsConfigSettingsExt: setts,
|
||||
tlsConfigStatus: status,
|
||||
@@ -474,23 +458,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,16 +515,6 @@ 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
|
||||
}
|
||||
@@ -533,7 +526,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
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 +538,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
|
||||
}()
|
||||
}
|
||||
|
||||
m.configModified()
|
||||
|
||||
err = m.reconfigureDNSServer()
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "reconfiguring dns server", slogutil.KeyError, err)
|
||||
@@ -572,18 +567,18 @@ 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() {
|
||||
m.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -204,8 +204,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)
|
||||
@@ -241,9 +239,11 @@ func TestTLSManager_Reload(t *testing.T) {
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
},
|
||||
},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
@@ -254,7 +254,8 @@ func TestTLSManager_Reload(t *testing.T) {
|
||||
|
||||
m.setWebAPI(web)
|
||||
|
||||
conf := m.config()
|
||||
conf := &tlsConfigSettings{}
|
||||
m.WriteDiskConfig(conf)
|
||||
assertCertSerialNumber(t, conf, snBefore)
|
||||
|
||||
certDER, key = newCertAndKey(t, snAfter)
|
||||
@@ -262,11 +263,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)
|
||||
}
|
||||
|
||||
@@ -281,9 +278,11 @@ func TestTLSManager_HandleTLSStatus(t *testing.T) {
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
CertificateChain: string(testCertChainData),
|
||||
PrivateKey: string(testPrivateKeyData),
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificateChain: string(testCertChainData),
|
||||
PrivateKey: string(testPrivateKeyData),
|
||||
},
|
||||
},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
@@ -343,49 +342,47 @@ func TestValidateTLSSettings(t *testing.T) {
|
||||
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),
|
||||
},
|
||||
},
|
||||
name: "busy_https_port",
|
||||
wantErr: fmt.Sprintf("port %d for HTTPS is not available", busyTCPPort),
|
||||
}, {
|
||||
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_dot_port",
|
||||
wantErr: fmt.Sprintf("port %d for DNS-over-TLS is not available", 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: "busy_doq_port",
|
||||
wantErr: fmt.Sprintf("port %d for DNS-over-QUIC is not available", busyUDPPort),
|
||||
}, {
|
||||
name: "duplicate_port",
|
||||
wantErr: "validating tcp ports: duplicated values: [4433]",
|
||||
setts: tlsConfigSettingsExt{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
@@ -393,6 +390,8 @@ func TestValidateTLSSettings(t *testing.T) {
|
||||
PortDNSOverTLS: 4433,
|
||||
},
|
||||
},
|
||||
name: "duplicate_port",
|
||||
wantErr: "validating tcp ports: duplicated values: [4433]",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -418,9 +417,11 @@ func TestTLSManager_HandleTLSValidate(t *testing.T) {
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
CertificateChain: string(testCertChainData),
|
||||
PrivateKey: string(testPrivateKeyData),
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificateChain: string(testCertChainData),
|
||||
PrivateKey: string(testPrivateKeyData),
|
||||
},
|
||||
},
|
||||
servePlainDNS: false,
|
||||
})
|
||||
@@ -433,9 +434,11 @@ func TestTLSManager_HandleTLSValidate(t *testing.T) {
|
||||
|
||||
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 +476,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},
|
||||
@@ -509,9 +511,11 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
|
||||
logger: logger,
|
||||
configModified: func() {},
|
||||
tlsSettings: tlsConfigSettings{
|
||||
Enabled: true,
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
Enabled: true,
|
||||
TLSConfig: dnsforward.TLSConfig{
|
||||
CertificatePath: certPath,
|
||||
PrivateKeyPath: keyPath,
|
||||
},
|
||||
},
|
||||
servePlainDNS: true,
|
||||
})
|
||||
@@ -522,16 +526,19 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
|
||||
|
||||
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),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -157,8 +157,8 @@ func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) {
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -199,7 +199,6 @@ run_linter gocognit --over='10' \
|
||||
./internal/aghhttp/ \
|
||||
./internal/aghrenameio/ \
|
||||
./internal/aghtest/ \
|
||||
./internal/aghuser/ \
|
||||
./internal/arpdb/ \
|
||||
./internal/client/ \
|
||||
./internal/configmigrate/ \
|
||||
@@ -251,7 +250,6 @@ run_linter fieldalignment \
|
||||
./internal/aghrenameio/ \
|
||||
./internal/aghtest/ \
|
||||
./internal/aghtls/ \
|
||||
./internal/aghuser/ \
|
||||
./internal/arpdb/ \
|
||||
./internal/client/ \
|
||||
./internal/configmigrate/ \
|
||||
@@ -282,7 +280,6 @@ run_linter gosec --exclude G115 --quiet \
|
||||
./internal/aghos/ \
|
||||
./internal/aghrenameio/ \
|
||||
./internal/aghtest/ \
|
||||
./internal/aghuser/ \
|
||||
./internal/arpdb/ \
|
||||
./internal/client/ \
|
||||
./internal/configmigrate/ \
|
||||
|
||||
Reference in New Issue
Block a user