Compare commits

..

10 Commits

Author SHA1 Message Date
Stanislav Chzhen
6109e3575f home: add tests 2025-05-13 22:23:41 +03:00
Stanislav Chzhen
88706e9cf2 home: auth tests 2025-05-07 15:46:22 +03:00
Eugene Burkov
b5c47054ab Pull request 2407: Update i18n
Merge in DNS/adguard-home from upd-all to master

Squashed commit of the following:

commit cb4a2379ee2543391ab85c6dd29bffc083544b2c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue May 6 17:25:50 2025 +0300

    client: upd i18n
2025-05-06 17:50:19 +03:00
Stanislav Chzhen
4776255604 Pull request 2402: AG-40703-fix-custom-cache
Merge in DNS/adguard-home from AG-40703-fix-custom-cache to master

Squashed commit of the following:

commit e9b9aa34d6969e87cc151573912c2f22a1b81cea
Merge: b8ec40b3d e5d0f0b11
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue May 6 16:58:37 2025 +0300

    Merge branch 'master' into AG-40703-fix-custom-cache

commit b8ec40b3dd9f59124bbf5cfc2b303a37750f7497
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue May 6 16:20:48 2025 +0300

    all: upd proxy

commit 026624543c319c022cf5d57d958cc5127cf2a629
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 28 15:46:28 2025 +0300

    all: fix custom cache
2025-05-06 17:07:57 +03:00
Stanislav Chzhen
e5d0f0b119 Pull request 2401: AGDNS-2817-fix-tls-test
Merge in DNS/adguard-home from AGDNS-2817-fix-tls-test to master

Squashed commit of the following:

commit 8cd435e05b6bfb988be90475b28db6703a763b2e
Merge: b9544ba8b af7c2e3a9
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 29 19:54:17 2025 +0300

    Merge branch 'master' into AGDNS-2817-fix-tls-test

commit b9544ba8b9097637a3b217142f092a242c819a63
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 28 15:28:05 2025 +0300

    home: fix tls test
2025-04-29 20:03:03 +03:00
Ainar Garipov
af7c2e3a9d Pull request 2403: 7790-fix-cache-label
Closes #7790.

Squashed commit of the following:

commit 9e871bcd8b0dfefbad41cd4e9893f918c9a4b090
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 29 14:48:58 2025 +0300

    client: fix cache label
2025-04-29 14:59:39 +03:00
Stanislav Chzhen
2c46bc92fe Pull request 2399: AGDNS-2686-fix-custom-upstream-cache
Merge in DNS/adguard-home from AGDNS-2686-fix-custom-upstream-cache to master

Squashed commit of the following:

commit 11ad20a225e0e21a59552dc885fbcb2d3acc1cef
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 24 18:51:35 2025 +0300

    client: imp docs

commit e6d73f2d7a9f2ea181b321dd0029cf6c42ddeba5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 24 17:36:05 2025 +0300

    all: imp chlog

commit b8fdd884b801db28f03efb00bd871df2332cf40a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 24 17:05:11 2025 +0300

    client: fix dhcp clients cache

commit 1760699fcb8e61580a48e61037b805b8aa8ca8b4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 24 15:37:50 2025 +0300

    all: upd chlog

commit c6f049c200736032e2d78a2023db7d8cc6c32917
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 24 15:33:53 2025 +0300

    client: imp tests

commit 7432de722292ef74bbdf5fbd875ea67d55b29040
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 24 15:32:26 2025 +0300

    client: fix custom upstream cache
2025-04-25 21:33:36 +03:00
Stanislav Chzhen
61a1403e4e Pull request 2378: AGDNS-2750-find-client
Merge in DNS/adguard-home from AGDNS-2750-find-client to master

Squashed commit of the following:

commit 98f1a8ca4622b6f502a5092273b9724203fe0bd8
Merge: 9270222d8 4ccc2a213
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 23 17:53:20 2025 +0300

    Merge branch 'master' into AGDNS-2750-find-client

commit 9270222d8e9e03038e9434b54496cbb6164463cd
Merge: 6468ceec8 c7c62ad3b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 21 19:40:58 2025 +0300

    Merge branch 'master' into AGDNS-2750-find-client

commit 6468ceec82d30084771a53ff6720a8c11c68bf2f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 21 19:40:52 2025 +0300

    home: imp docs

commit 3fd4735a0d6db4fdf2d46f3da9794a687fdcaa8b
Merge: 1311a5869 a8fdf1c55
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 18 19:43:36 2025 +0300

    Merge branch 'master' into AGDNS-2750-find-client

commit 1311a58695de00f20c9704378ee6e964a44d1c59
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 18 19:42:41 2025 +0300

    home: imp code

commit b1f2c4c883c9476c5135140abac31f8ae6609b4f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 16 16:47:59 2025 +0300

    home: imp code

commit d0a5abd66587c1ad602c2ccf6c8a45a3dfe39a5c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 15 14:58:31 2025 +0300

    client: imp naming

commit 5accdca325551237f003f1c416891b488fe5290b
Merge: 6a00232f7 4d258972d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 14 19:40:40 2025 +0300

    Merge branch 'master' into AGDNS-2750-find-client

commit 6a00232f76a0fe5ce781aa01637b6e04ace7250d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 14 19:30:32 2025 +0300

    home: imp code

commit 8633886457c6aab75f5676494b1f49d9811e9ab9
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 11 15:29:25 2025 +0300

    all: imp code

commit d6f16879e7b054a5ffac59131d2a6eff1da659c0
Merge: 58236fdec 6d282ae71
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 21:35:23 2025 +0300

    Merge branch 'master' into AGDNS-2750-find-client

commit 58236fdec5b64e83a44680ff8a89badc18ec81f1
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 21:23:01 2025 +0300

    all: upd ci

commit 3c4d946d7970987677d4ac984394e18987a29f9a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 21:16:03 2025 +0300

    all: upd go

commit cc1c97734506a9ffbe70fd3c676284e58a21ba46
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 20:58:56 2025 +0300

    all: imp code

commit 8f061c933152481a4c80eef2af575efd4919d82b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 9 16:49:11 2025 +0300

    all: imp docs

commit 8d19355f1c519211a56cec3f23d527922d4f2ee0
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 7 21:35:06 2025 +0300

    all: imp code

commit f1e853f57e5d54d13bedcdab4f8e21e112f3a356
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 2 14:57:40 2025 +0300

    all: imp code

commit 6a6ac7f899f29ddc90a583c80562233e646ba1d6
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 1 19:51:56 2025 +0300

    client: imp tests

commit 52040ee7393d0483c682f2f37d7b70f12f9cf621
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 1 19:28:18 2025 +0300

    all: imp code

commit 1e09208dbd2d35c3f6b2ade169324e23d1a643a5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 26 15:33:02 2025 +0300

    all: imp code

... and 2 more commits
2025-04-23 18:10:52 +03:00
Stanislav Chzhen
4ccc2a2138 Pull request 2398: 7781-fix-serve-plain-dns
Updates #7781.

Squashed commit of the following:

commit 5dff0be1763e7da7dd655bf1e34dfa8402ad96e8
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 22 18:15:42 2025 +0300

    home: fix serve plain dns
2025-04-22 20:31:15 +03:00
Eugene Burkov
72425b80a3 Pull request 2397: Update changelog
Merge in DNS/adguard-home from upd-chlog to master

Squashed commit of the following:

commit 10ff5bce544ef796eb22739c7d20d8bf92b0109f
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Apr 22 15:45:45 2025 +0300

    all: upd chlog
2025-04-22 17:39:45 +03:00
48 changed files with 1642 additions and 668 deletions

View File

@@ -9,26 +9,41 @@ The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/
<!-- <!--
## [v0.108.0] TBA ## [v0.108.0] TBA
## [v0.107.61] - 2025-04-22 (APPROX.) ## [v0.107.62] - 2025-04-30 (APPROX.)
See also the [v0.107.61 GitHub milestone][ms-v0.107.61]. See also the [v0.107.62 GitHub milestone][ms-v0.107.62].
[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1 [ms-v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/milestone/97?closed=1
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Fixed
- 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.
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.61] - 2025-04-22
See also the [v0.107.61 GitHub milestone][ms-v0.107.61].
### Security ### 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. - 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. **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.
[mr-xiang-li]: https://lixiang521.com/ ### Fixed
<!-- - Searching for persistent clients using an exact match for CIDR in the `POST /clients/search HTTP API`.
NOTE: Add new changes ABOVE THIS COMMENT.
--> [mr-xiang-li]: https://lixiang521.com/
[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1
## [v0.107.60] - 2025-04-14 ## [v0.107.60] - 2025-04-14
@@ -71,10 +86,6 @@ See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
See also the [v0.107.59 GitHub milestone][ms-v0.107.59]. See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
### Fixed
- Validation process for the DNS-over-TLS, DNS-over-QUIC, and HTTPS ports on the *Encryption Settings* page.
- Rules with the `client` modifier not working ([#7708]). - Rules with the `client` modifier not working ([#7708]).
- The search form not working in the query log ([#7704]). - The search form not working in the query log ([#7704]).
@@ -3115,11 +3126,12 @@ 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 [ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.62...HEAD
[v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61 [v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...v0.107.62
--> -->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...HEAD
[v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61
[v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60 [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.59]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59
[v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58 [v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58

View File

@@ -1,24 +1,24 @@
{ {
"client_settings": "Налады кліентаў", "client_settings": "Налады кліентаў",
"example_upstream_reserved": "upstream <0>для канкрэтных даменаў</0>;", "example_upstream_reserved": "upstream <0>для канкрэтных даменаў</0>;",
"example_multiple_upstreams_reserved": "некалькі DNS-сервераў <0>для канкрэтных даменаў</0>;", "example_multiple_upstreams_reserved": "некалькі сервер DNSаў <0>для канкрэтных даменаў</0>;",
"example_upstream_comment": "каментар.", "example_upstream_comment": "каментар.",
"upstream_parallel": "Ужыць адначасныя запыты да ўсіх сервераў для паскарэння апрацоўкі запыту", "upstream_parallel": "Ужыць адначасныя запыты да ўсіх сервераў для паскарэння апрацоўкі запыту",
"parallel_requests": "Паралельныя запыты", "parallel_requests": "Паралельныя запыты",
"load_balancing": "Размеркаванне нагрузкі", "load_balancing": "Размеркаванне нагрузкі",
"load_balancing_desc": "Запытвайце па адным серверы за раз. AdGuard Home будзе выкарыстоўваць выпадковы алгарытм для выбару сервера, так што самы хуткі сервер будзе выкарыстоўвацца часцей.", "load_balancing_desc": "Запытвайце па адным серверы за раз. AdGuard Home будзе выкарыстоўваць выпадковы алгарытм для выбару сервера, так што самы хуткі сервер будзе выкарыстоўвацца часцей.",
"bootstrap_dns": "Bootstrap DNS-серверы", "bootstrap_dns": "Bootstrap сервер DNSы",
"bootstrap_dns_desc": "IP-адрасы DNS-сервераў, якія выкарыстоўваюцца для вырашэння IP-адрасоў распознавальнікаў DoH/DoT, якія вы ўказваеце ў якасці перадачы. Каментары не дапускаюцца.", "bootstrap_dns_desc": "IP-адрасы сервер DNSаў, якія выкарыстоўваюцца для вырашэння IP-адрасоў распознавальнікаў DoH/DoT, якія вы ўказваеце ў якасці перадачы. Каментары не дапускаюцца.",
"fallback_dns_title": "Рэзервовыя DNS-серверы", "fallback_dns_title": "Рэзервовыя сервер DNSы",
"fallback_dns_desc": "Спіс рэзервовых DNS-сервераў, якія выкарыстоўваюцца, калі вышэйшыя DNS-серверы не адказваюць. Сінтаксіс такі ж, як і ў галоўным полі ўверх.", "fallback_dns_desc": "Спіс рэзервовых сервер DNSаў, якія выкарыстоўваюцца, калі вышэйшыя сервер DNSы не адказваюць. Сінтаксіс такі ж, як і ў галоўным полі ўверх.",
"fallback_dns_placeholder": "Увядзіце па адным рэзервовым серверы 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_desc": "DNS-серверы, якія AdGuard Home выкарыстоўвае для лакальных PTR-запытаў. Гэтыя серверы выкарыстоўваюцца, каб атрымаць даменавыя імёны кліентаў з прыватнымі IP-адрасамі, напрыклад «192.168.12.34», з дапамогай rDNS. Калі спіс пусты, AdGuard Home выкарыстоўвае прадвызначаныя DNS-серверы вашай АС.",
"local_ptr_default_resolver": "Па змаўчанні AdGuard Home выкарыстоўвае наступныя зваротныя DNS-рэзолверы: {{ip}}.", "local_ptr_default_resolver": "Па змаўчанні AdGuard Home выкарыстоўвае наступныя зваротныя DNS-рэзолверы: {{ip}}.",
"local_ptr_no_default_resolver": "AdGuard Home не змог вызначыць прыдатныя прыватныя адваротныя DNS-рэзолверы для гэтай сістэмы.", "local_ptr_no_default_resolver": "AdGuard Home не змог вызначыць прыдатныя прыватныя адваротныя DNS-рэзолверы для гэтай сістэмы.",
"local_ptr_placeholder": "Увядзіце па адным адрасе на радок", "local_ptr_placeholder": "Увядзіце па адным адрасе на радок",
"resolve_clients_title": "Уключыць запытванне даменавых імёнаў для кліентаў", "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_title": "Ужываць прыватныя адваротныя DNS-рэзолверы",
"use_private_ptr_resolvers_desc": "Пасылаць адваротныя DNS-запыты для лакальна абслугоўных адрасоў на паказаныя серверы. Калі адключана, AdGuard Home будзе адказваць NXDOMAIN на ўсе падобныя PTR-запыты, апроч запытаў пра кліентаў, ужо вядомых па DHCP, /etc/hosts і гэтак далей.", "use_private_ptr_resolvers_desc": "Пасылаць адваротныя DNS-запыты для лакальна абслугоўных адрасоў на паказаныя серверы. Калі адключана, AdGuard Home будзе адказваць NXDOMAIN на ўсе падобныя PTR-запыты, апроч запытаў пра кліентаў, ужо вядомых па DHCP, /etc/hosts і гэтак далей.",
"check_dhcp_servers": "Праверыць DHCP-серверы", "check_dhcp_servers": "Праверыць DHCP-серверы",
@@ -101,13 +101,13 @@
"compact": "Компактный", "compact": "Компактный",
"nothing_found": "Нічога не знойдзена", "nothing_found": "Нічога не знойдзена",
"faq": "FAQ", "faq": "FAQ",
"version": "версія", "version": "Версія",
"address": "Адрас", "address": "Адрас",
"protocol": "Пратакол", "protocol": "Пратакол",
"on": "УКЛ", "on": "УКЛ",
"off": "Выкл", "off": "Выкл",
"copyright": "Усе правы захаваныя", "copyright": "Усе правы захаваныя",
"homepage": "Галоўная", "homepage": "Хатняя старонка",
"report_an_issue": "Паведаміць пра праблему", "report_an_issue": "Паведаміць пра праблему",
"privacy_policy": "Палітыка прыватнасці", "privacy_policy": "Палітыка прыватнасці",
"enable_protection": "Уключыць абарону", "enable_protection": "Уключыць абарону",
@@ -165,8 +165,8 @@
"custom_filtering_rules": "Карыстальніцкія правілы фільтрацыі", "custom_filtering_rules": "Карыстальніцкія правілы фільтрацыі",
"encryption_settings": "Налады шыфравання", "encryption_settings": "Налады шыфравання",
"dhcp_settings": "Налады DHCP", "dhcp_settings": "Налады DHCP",
"upstream_dns": "Upstream DNS-серверы", "upstream_dns": "Upstream сервер DNSы",
"upstream_dns_help": "Увядзіце адрасы сервераў па адным у радку. <a>Даведацца больш </a> пра наладжванне DNS-сервераў.", "upstream_dns_help": "Увядзіце адрасы сервераў па адным у радку. <a>Даведацца больш </a> пра наладжванне сервер DNSаў.",
"upstream_dns_configured_in_file": "Наладжаны ў {{path}}", "upstream_dns_configured_in_file": "Наладжаны ў {{path}}",
"test_upstream_btn": "Тэст upstream сервераў", "test_upstream_btn": "Тэст upstream сервераў",
"upstreams": "Upstreams", "upstreams": "Upstreams",
@@ -182,7 +182,7 @@
"enabled_save_search_toast": "Уключаны бяспечны пошук", "enabled_save_search_toast": "Уключаны бяспечны пошук",
"updated_save_search_toast": "Налады бяспечнага пошуку абноўлены", "updated_save_search_toast": "Налады бяспечнага пошуку абноўлены",
"enabled_table_header": "УКЛ.", "enabled_table_header": "УКЛ.",
"name_table_header": "Імя", "name_table_header": "Назва",
"list_url_table_header": "URL-адрас спіса", "list_url_table_header": "URL-адрас спіса",
"rules_count_table_header": "Колькасць правілаў:", "rules_count_table_header": "Колькасць правілаў:",
"last_time_updated_table_header": "Апошняе абнаўленне", "last_time_updated_table_header": "Апошняе абнаўленне",
@@ -196,7 +196,7 @@
"no_whitelist_added": "Белыя спісы не дададзены", "no_whitelist_added": "Белыя спісы не дададзены",
"add_blocklist": "Дадаць чорны спіс", "add_blocklist": "Дадаць чорны спіс",
"add_allowlist": "Дадаць белы спіс", "add_allowlist": "Дадаць белы спіс",
"cancel_btn": "Адмена", "cancel_btn": "Скасаваць",
"enter_name_hint": "Увядзіце імя", "enter_name_hint": "Увядзіце імя",
"enter_url_or_path_hint": "Увядзіце URL-адрас ці абсалютны шлях да спіса", "enter_url_or_path_hint": "Увядзіце URL-адрас ці абсалютны шлях да спіса",
"check_updates_btn": "Праверыць абнаўленні", "check_updates_btn": "Праверыць абнаўленні",
@@ -219,7 +219,7 @@
"example_meaning_host_block": "адказаць 127.0.0.1 для example.org (але не для яго паддаменаў);", "example_meaning_host_block": "адказаць 127.0.0.1 для example.org (але не для яго паддаменаў);",
"example_comment": "! Так можна дадаваць апісанне.", "example_comment": "! Так можна дадаваць апісанне.",
"example_comment_meaning": "каментар;", "example_comment_meaning": "каментар;",
"example_comment_hash": "# І вось так таксама.", "example_comment_hash": "# Таксама каментарый.",
"example_regex_meaning": "блакаваць доступ да даменаў, якія адпавядаюць зададзенаму рэгулярнаму выразу.", "example_regex_meaning": "блакаваць доступ да даменаў, якія адпавядаюць зададзенаму рэгулярнаму выразу.",
"example_upstream_regular": "звычайны DNS (наўзверх UDP);", "example_upstream_regular": "звычайны DNS (наўзверх UDP);",
"example_upstream_regular_port": "звычайны DNS (праз UDP, імя хаста);", "example_upstream_regular_port": "звычайны DNS (праз UDP, імя хаста);",
@@ -233,13 +233,13 @@
"example_upstream_tcp_port": "звычайны DNS (праз TCP, імя хаста);", "example_upstream_tcp_port": "звычайны DNS (праз TCP, імя хаста);",
"example_upstream_tcp_hostname": "звычайны DNS (праз TCP, імя хаста);", "example_upstream_tcp_hostname": "звычайны DNS (праз TCP, імя хаста);",
"all_lists_up_to_date_toast": "Усе спісы ўжо абноўлены", "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_ok_toast": "Паказаныя серверы DNS працуюць карэктна",
"dns_test_not_ok_toast": "Сервер «{{key}}»: немагчыма выкарыстоўваць, праверце слушнасць напісання", "dns_test_not_ok_toast": "Сервер «{{key}}»: немагчыма выкарыстоўваць, праверце слушнасць напісання",
"dns_test_parsing_error_toast": "Раздзел {{section}}: радок {{line}}: немагчыма выкарыстоўваць, праверце слушнасць напісання", "dns_test_parsing_error_toast": "Раздзел {{section}}: радок {{line}}: немагчыма выкарыстоўваць, праверце слушнасць напісання",
"dns_test_warning_toast": "Upstream «{{key}}» не адказвае на тэставыя запыты і можа не працаваць належным чынам", "dns_test_warning_toast": "Upstream «{{key}}» не адказвае на тэставыя запыты і можа не працаваць належным чынам",
"unblock": "Адблакаваць", "unblock": "Адблакаваць",
"block": "Заблакаваць", "block": "Заблакіраваць",
"disallow_this_client": "Забараніць доступ гэтаму кліенту", "disallow_this_client": "Забараніць доступ гэтаму кліенту",
"allow_this_client": "Дазволіць доступ гэтаму кліенту", "allow_this_client": "Дазволіць доступ гэтаму кліенту",
"block_for_this_client_only": "Заблакаваць толькі для гэтага кліента", "block_for_this_client_only": "Заблакаваць толькі для гэтага кліента",
@@ -259,7 +259,7 @@
"no_logs_found": "Логі не знойдзены", "no_logs_found": "Логі не знойдзены",
"refresh_btn": "Абнавіць", "refresh_btn": "Абнавіць",
"previous_btn": "Назад", "previous_btn": "Назад",
"next_btn": "Наперад", "next_btn": "Далей",
"loading_table_status": "Загрузка...", "loading_table_status": "Загрузка...",
"page_table_footer_text": "Старонка", "page_table_footer_text": "Старонка",
"rows_table_footer_text": "радкоў", "rows_table_footer_text": "радкоў",
@@ -280,7 +280,7 @@
"query_log_retention_confirm": "Вы ўпэўнены, што хочаце змяніць тэрмін захоўвання запытаў? Пры памяншэнні інтэрвалу, некаторыя даныя могуць быць страчаны", "query_log_retention_confirm": "Вы ўпэўнены, што хочаце змяніць тэрмін захоўвання запытаў? Пры памяншэнні інтэрвалу, некаторыя даныя могуць быць страчаны",
"anonymize_client_ip": "Ананімізацыя IP-адрасы кліента", "anonymize_client_ip": "Ананімізацыя IP-адрасы кліента",
"anonymize_client_ip_desc": "Не захоўвайце поўныя IP-адрасы гэтых удзельнікаў у часопісах або статыстыцы", "anonymize_client_ip_desc": "Не захоўвайце поўныя IP-адрасы гэтых удзельнікаў у часопісах або статыстыцы",
"dns_config": "Налады DNS-сервера", "dns_config": "Налады сервер DNSа",
"dns_cache_config": "Налада кэша DNS", "dns_cache_config": "Налада кэша DNS",
"dns_cache_config_desc": "Тут можна наладзіць кэш DNS", "dns_cache_config_desc": "Тут можна наладзіць кэш DNS",
"blocking_mode": "Рэжым блакавання", "blocking_mode": "Рэжым блакавання",
@@ -342,14 +342,14 @@
"unknown_filter": "Невядомы фільтр {{filterId}}", "unknown_filter": "Невядомы фільтр {{filterId}}",
"known_tracker": "Вядомы трэкер", "known_tracker": "Вядомы трэкер",
"install_welcome_title": "Сардэчна запрашаем у AdGuard Home!", "install_welcome_title": "Сардэчна запрашаем у AdGuard Home!",
"install_welcome_desc": "AdGuard Home гэта DNS-сервер, што блакуе рэкламу і трэкінг. Яго мэта даць вам магчымасць кантраляваць усю ваша сеціва і ўсе падлучаныя прылады. Ён не патрабуе ўсталёўкі кліенцкіх праграм.", "install_welcome_desc": "AdGuard Home гэта сервер DNS, што блакуе рэкламу і трэкінг. Яго мэта даць вам магчымасць кантраляваць усю ваша сеціва і ўсе падлучаныя прылады. Ён не патрабуе ўсталёўкі кліенцкіх праграм.",
"install_settings_title": "Ўэб-інтэрфейс адміністравання", "install_settings_title": "Ўэб-інтэрфейс адміністравання",
"install_settings_listen": "Інтэрфейс сеціва", "install_settings_listen": "Інтэрфейс сеціва",
"install_settings_port": "Порт", "install_settings_port": "Порт",
"install_settings_interface_link": "Ваш ўэб-інтэрфейс адміністравання AdGuard Home будзе даступны па наступных адрасах:", "install_settings_interface_link": "Ваш ўэб-інтэрфейс адміністравання AdGuard Home будзе даступны па наступных адрасах:",
"form_error_port": "Увядзіце карэктны нумар порта", "form_error_port": "Увядзіце карэктны нумар порта",
"install_settings_dns": "DNS-сервер", "install_settings_dns": "DNS-сервер",
"install_settings_dns_desc": "Вам будзе трэба наладзіць свае прылады ці роўтар на выкарыстанне DNS-сервера на адным з наступных адрасоў:", "install_settings_dns_desc": "Вам будзе трэба наладзіць свае прылады ці роўтар на выкарыстанне сервер DNSа на адным з наступных адрасоў:",
"install_settings_all_interfaces": "Усе інтэрфейсы", "install_settings_all_interfaces": "Усе інтэрфейсы",
"install_auth_title": "Аўтарызацыя", "install_auth_title": "Аўтарызацыя",
"install_auth_desc": "Настойліва рэкамендуецца наладзіць аўтэнтыфікацыю паролем для ўэб-інтэрфейсу AdGuard Home. Нават калі ён даступны толькі ў вашай лакальнай сетцы, важна абараніць яго ад неабмежаванага доступу.", "install_auth_desc": "Настойліва рэкамендуецца наладзіць аўтэнтыфікацыю паролем для ўэб-інтэрфейсу AdGuard Home. Нават калі ён даступны толькі ў вашай лакальнай сетцы, важна абараніць яго ад неабмежаванага доступу.",
@@ -365,17 +365,17 @@
"install_submit_desc": "Працэдура налады завершана і вы гатовы пачаць выкарыстанне AdGuard Home.", "install_submit_desc": "Працэдура налады завершана і вы гатовы пачаць выкарыстанне AdGuard Home.",
"install_devices_router": "Роўтар", "install_devices_router": "Роўтар",
"install_devices_router_desc": "Такая наладка аўтаматычна пакрые ўсе прылады, што выкарыстоўваюць ваш хатні роўтар, і вам не трэба будзе наладжваць кожнае з іх у асобнасці.", "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_1": "Адкрыйце налады вашага роўтара. Звычайна вы можаце адкрыць іх у вашым браўзары, напрыклад, http://192.168.0.1/ ці http://192.168.1.1/. Вас могуць папрасіць увесці пароль. Калі вы не помніце яго, пароль часта можна скінуць, націснуўшы на кнопку на самым роўтары. Некаторыя роўтары патрабуюць адмысловага дадатку, які ў гэтым выпадку павінен быць ужо ўсталявана на ваш кампутар ці тэлефон.",
"install_devices_router_list_2": "Знайдзіце налады DHCP ці DNS. Знайдзіце літары «DNS» поруч з тэкставым полем, у якое можна ўвесці два ці тры шэрагі лічбаў, падзеленых на 4 групы ад адной до трох лічбаў.", "install_devices_router_list_2": "Знайдзіце налады DHCP ці DNS. Знайдзіце літары «DNS» поруч з тэкставым полем, у якое можна ўвесці два ці тры шэрагі лічбаў, падзеленых на 4 групы ад адной до трох лічбаў.",
"install_devices_router_list_3": "Увядзіце туды адрас вашага AdGuard Home.", "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_1": "Адкрыйце Панэль кіравання праз меню «Пуск» ці праз пошук Windows.",
"install_devices_windows_list_2": "Перайдзіце ў «Сеціва і інтэрнэт», а потым у «Цэнтр кіравання сеціва і агульным доступам».", "install_devices_windows_list_2": "Перайдзіце ў «Сеціва і інтэрнэт», а потым у «Цэнтр кіравання сеціва і агульным доступам».",
"install_devices_windows_list_3": "У левым боку экрана клікніце «Змена параметраў адаптара».", "install_devices_windows_list_3": "У левым боку экрана клікніце «Змена параметраў адаптара».",
"install_devices_windows_list_4": "Пстрыкніце правай кнопкай мышы ваша актыўнае злучэнне і абярыце Уласцівасці.", "install_devices_windows_list_4": "Пстрыкніце правай кнопкай мышы ваша актыўнае злучэнне і абярыце Уласцівасці.",
"install_devices_windows_list_5": "Знайдзіце ў спісе пункт «IP версіі 4 (TCP/IPv4)», вылучыце яго і потым ізноў націсніце «Уласцівасці».", "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_1": "Клікніце па абразку Apple і перайдзіце ў Сістэмныя налады.",
"install_devices_macos_list_2": "Клікніце па іконцы Сеціва.", "install_devices_macos_list_2": "Клікніце па іконцы Сеціва.",
"install_devices_macos_list_3": "Абярыце першае падлучэнне ў спісе і націсніце кнопку «Дадаткова».", "install_devices_macos_list_3": "Абярыце першае падлучэнне ў спісе і націсніце кнопку «Дадаткова».",
@@ -415,7 +415,7 @@
"encryption_key": "Прыватны ключ", "encryption_key": "Прыватны ключ",
"encryption_key_input": "Скапіюйце сюды прыватны ключ у PEM-кадоўцы.", "encryption_key_input": "Скапіюйце сюды прыватны ключ у PEM-кадоўцы.",
"encryption_enable": "Уключыць шыфраванне (HTTPS, DNS-over-HTTPS і DNS-over-TLS)", "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_valid": "Ланцужок сертыфікатаў валідны",
"encryption_chain_invalid": "Ланцужок сертыфікатаў не валідны", "encryption_chain_invalid": "Ланцужок сертыфікатаў не валідны",
"encryption_key_valid": "Валідны {{type}} прыватны ключ", "encryption_key_valid": "Валідны {{type}} прыватны ключ",
@@ -435,8 +435,8 @@
"update_announcement": "AdGuard Home {{version}} ужо даступная! <0>Націсніце сюды</0>, каб даведацца больш.", "update_announcement": "AdGuard Home {{version}} ужо даступная! <0>Націсніце сюды</0>, каб даведацца больш.",
"setup_guide": "Інструкцыя па наладзе", "setup_guide": "Інструкцыя па наладзе",
"dns_addresses": "Адрасы DNS", "dns_addresses": "Адрасы DNS",
"dns_start": "DNS-сервер запускаецца", "dns_start": "сервер DNS запускаецца",
"dns_status_error": "Памылка праверкі стану DNS-сервера", "dns_status_error": "Памылка праверкі стану сервер DNSа",
"down": "Уніз", "down": "Уніз",
"fix": "Выправіць", "fix": "Выправіць",
"dns_providers": "<0>Спіс вядомых DNS-правайдараў</0> на выбар.", "dns_providers": "<0>Спіс вядомых DNS-правайдараў</0> на выбар.",
@@ -449,7 +449,7 @@
"settings_global": "Глабальныя", "settings_global": "Глабальныя",
"settings_custom": "Свае", "settings_custom": "Свае",
"table_client": "Кліент", "table_client": "Кліент",
"table_name": "Імя", "table_name": "Назва",
"save_btn": "Захаваць", "save_btn": "Захаваць",
"client_add": "Дадаць кліента", "client_add": "Дадаць кліента",
"client_new": "Новы кліент", "client_new": "Новы кліент",
@@ -475,7 +475,7 @@
"auto_clients_title": "Кліенты (runtime)", "auto_clients_title": "Кліенты (runtime)",
"auto_clients_desc": "Інфармацыя аб IP-адрасах прылад, якія выкарыстоўваюць або могуць выкарыстоўваць AdGuard Home. Гэтая інфармацыя збіраецца з некалькіх крыніц, уключаючы файлы хостаў, зваротны DNS і г.д.", "auto_clients_desc": "Інфармацыя аб IP-адрасах прылад, якія выкарыстоўваюць або могуць выкарыстоўваць AdGuard Home. Гэтая інфармацыя збіраецца з некалькіх крыніц, уключаючы файлы хостаў, зваротны DNS і г.д.",
"access_title": "Налады доступу", "access_title": "Налады доступу",
"access_desc": "Тут вы можаце наладзіць правілы доступу да DNS-серверу AdGuard Home", "access_desc": "Тут вы можаце наладзіць правілы доступу да сервер DNSу AdGuard Home",
"access_allowed_title": "Дазволеныя кліенты", "access_allowed_title": "Дазволеныя кліенты",
"access_allowed_desc": "Спіс CIDR, IP-адрасоў або <a>ClientID</a>. Калі ў гэтым спісе ёсць запісы, AdGuard Home будзе прымаць запыты толькі ад гэтых кліентаў.", "access_allowed_desc": "Спіс CIDR, IP-адрасоў або <a>ClientID</a>. Калі ў гэтым спісе ёсць запісы, AdGuard Home будзе прымаць запыты толькі ад гэтых кліентаў.",
"access_disallowed_title": "Забароненыя кліенты", "access_disallowed_title": "Забароненыя кліенты",
@@ -596,7 +596,7 @@
"disable_ipv6_desc": "Ігнараваць усе запыты DNS для адрасоў IPv6 (тып AAAA) і выдаленне дадзеных IPv6 з адказаў тыпу HTTPS.", "disable_ipv6_desc": "Ігнараваць усе запыты DNS для адрасоў IPv6 (тып AAAA) і выдаленне дадзеных IPv6 з адказаў тыпу HTTPS.",
"fastest_addr": "Найхуткі IP-адрас", "fastest_addr": "Найхуткі IP-адрас",
"fastest_addr_desc": "Апытайце ўсе DNS-серверы і вярніце самы хуткі IP-адрас сярод усіх адказаў. Гэта замарудзіць выкананне DNS-запытаў, бо нам давядзецца чакаць адказаў ад усіх DNS-сервераў, але палепшыць агульную ўзаемасувязь.", "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_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", "autofix_warning_result": "У выніку ўсе DNS-запыты ад вашай сістэмы будуць па змаўчанні апрацоўвацца AdGuard Home.\n",
"tags_title": "Тэгі", "tags_title": "Тэгі",
@@ -634,12 +634,12 @@
"validated_with_dnssec": "Проверено с помощью DNSSEC", "validated_with_dnssec": "Проверено с помощью DNSSEC",
"all_queries": "Усе запыты", "all_queries": "Усе запыты",
"show_blocked_responses": "Заблакавана", "show_blocked_responses": "Заблакавана",
"show_whitelisted_responses": "Белы спіс", "show_whitelisted_responses": "У белым спісе",
"show_processed_responses": "Апрацавана", "show_processed_responses": "Апрацавана",
"blocked_safebrowsing": "Заблакіравана згодна з базай даных Safe Browsing", "blocked_safebrowsing": "Заблакіравана згодна з базай даных Safe Browsing",
"blocked_adult_websites": "Заблакавана Бацькоўскім кантролем", "blocked_adult_websites": "Заблакавана Бацькоўскім кантролем",
"blocked_threats": "Заблакавана пагроз", "blocked_threats": "Заблакавана пагроз",
"allowed": "Дазволены", "allowed": "У белым спісе",
"filtered": "Адфільтраваныя", "filtered": "Адфільтраваныя",
"rewritten": "Перапісаныя", "rewritten": "Перапісаныя",
"safe_search": "Бяспечны пошук", "safe_search": "Бяспечны пошук",
@@ -738,7 +738,7 @@
"thursday_short": "Чц.", "thursday_short": "Чц.",
"friday_short": "Пт.", "friday_short": "Пт.",
"saturday_short": "Сб.", "saturday_short": "Сб.",
"upstream_dns_cache_configuration": "Канфігурацыя кэша upstream DNS-сервераў", "upstream_dns_cache_configuration": "Канфігурацыя кэша upstream сервер DNSаў",
"enable_upstream_dns_cache": "Ўключыць кэшаванне для карыстацкай канфігурацыі upstream-сервераў гэтага кліента", "enable_upstream_dns_cache": "Ўключыць кэшаванне для карыстацкай канфігурацыі upstream-сервераў гэтага кліента",
"dns_cache_size": "Памер кэша DNS, у байтах" "dns_cache_size": "Памер кэша DNS, у байтах"
} }

View File

@@ -656,7 +656,7 @@
"blocklist": "Zakázaný", "blocklist": "Zakázaný",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Velikost mezipaměti", "cache_size": "Velikost mezipaměti",
"cache_size_desc": "Velikost mezipaměti DNS (v bajtech). Chcete-li ukládání do mezipaměti zakázat, ponechte prázdné.", "cache_size_desc": "Velikost mezipaměti DNS (v bajtech). Chcete-li ukládání do mezipaměti zakázat, nastavte 0.",
"cache_ttl_min_override": "Přepsat minimální hodnotu TTL", "cache_ttl_min_override": "Přepsat minimální hodnotu TTL",
"cache_ttl_max_override": "Přepsat maximální hodnotu TTL", "cache_ttl_max_override": "Přepsat maximální hodnotu TTL",
"enter_cache_size": "Zadejte velikost mezipaměti (v bajtech)", "enter_cache_size": "Zadejte velikost mezipaměti (v bajtech)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Sortliste", "blocklist": "Sortliste",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Cache-størrelse", "cache_size": "Cache-størrelse",
"cache_size_desc": "DNS cache-størrelse (i bytes). Lad stå tomt for at deaktivere cache.", "cache_size_desc": "DNS cache-størrelse (i bytes). Sæt til 0 for at deaktivere cache.",
"cache_ttl_min_override": "Tilsidesæt minimum TTL", "cache_ttl_min_override": "Tilsidesæt minimum TTL",
"cache_ttl_max_override": "Tilsidesæt maksimal TTL", "cache_ttl_max_override": "Tilsidesæt maksimal TTL",
"enter_cache_size": "Angiv cache-størrelse (bytes)", "enter_cache_size": "Angiv cache-størrelse (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Sperrliste", "blocklist": "Sperrliste",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Größe des Cache", "cache_size": "Größe des Cache",
"cache_size_desc": "Größe des DNS-Zwischenspeichers (in Bytes)", "cache_size_desc": "Größe des DNS-Cache (in Bytes). Um das Caching zu deaktivieren, setzen Sie den Wert auf 0.",
"cache_ttl_min_override": "TTL-Minimalwert überschreiben", "cache_ttl_min_override": "TTL-Minimalwert überschreiben",
"cache_ttl_max_override": "TTL-Höchstwert überschreiben", "cache_ttl_max_override": "TTL-Höchstwert überschreiben",
"enter_cache_size": "Größe des Cache (Bytes) eingeben", "enter_cache_size": "Größe des Cache (Bytes) eingeben",

View File

@@ -656,7 +656,7 @@
"blocklist": "Blocklist", "blocklist": "Blocklist",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Cache size", "cache_size": "Cache size",
"cache_size_desc": "DNS cache size (in bytes). To disable caching, leave empty.", "cache_size_desc": "DNS cache size (in bytes). To disable caching, set to 0.",
"cache_ttl_min_override": "Override minimum TTL", "cache_ttl_min_override": "Override minimum TTL",
"cache_ttl_max_override": "Override maximum TTL", "cache_ttl_max_override": "Override maximum TTL",
"enter_cache_size": "Enter cache size (bytes)", "enter_cache_size": "Enter cache size (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista de bloqueo", "blocklist": "Lista de bloqueo",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Tamaño de la caché", "cache_size": "Tamaño de la caché",
"cache_size_desc": "Tamaño de la caché DNS (en bytes). Para deshabilitar el almacenamiento en caché, déjalo vacío.", "cache_size_desc": "Tamaño de la caché DNS (en bytes). Para desactivar el almacenamiento en caché, configúralo en 0.",
"cache_ttl_min_override": "Anular TTL mínimo", "cache_ttl_min_override": "Anular TTL mínimo",
"cache_ttl_max_override": "Anular TTL máximo", "cache_ttl_max_override": "Anular TTL máximo",
"enter_cache_size": "Ingresa el tamaño de la caché (bytes)", "enter_cache_size": "Ingresa el tamaño de la caché (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Liste de blocage", "blocklist": "Liste de blocage",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Taille du cache", "cache_size": "Taille du cache",
"cache_size_desc": "Taille du cache DNS (en octets). Pour désactiver la mise en cache, laissez vide.", "cache_size_desc": "Taille du cache DNS (en octets). Pour désactiver la mise en cache, mettez la valeur sur 0.",
"cache_ttl_min_override": "Remplacer le TTL minimum", "cache_ttl_min_override": "Remplacer le TTL minimum",
"cache_ttl_max_override": "Remplacer le TTL maximum", "cache_ttl_max_override": "Remplacer le TTL maximum",
"enter_cache_size": "Entrer la taille du cache (octets)", "enter_cache_size": "Entrer la taille du cache (octets)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista nera", "blocklist": "Lista nera",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Dimensioni cache", "cache_size": "Dimensioni cache",
"cache_size_desc": "Dimensione della cache DNS (in byte). Per disabilitare la memorizzazione nella cache, lascia vuoto.", "cache_size_desc": "Dimensione della cache DNS (in byte). Per disabilitare la cache, impostare su 0.",
"cache_ttl_min_override": "Sovrascrivi TTL minimo", "cache_ttl_min_override": "Sovrascrivi TTL minimo",
"cache_ttl_max_override": "Sovrascrivi TTL massimo", "cache_ttl_max_override": "Sovrascrivi TTL massimo",
"enter_cache_size": "Immetti dimensioni cache (in byte)", "enter_cache_size": "Immetti dimensioni cache (in byte)",

View File

@@ -656,7 +656,7 @@
"blocklist": "ブロックリスト", "blocklist": "ブロックリスト",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "キャッシュサイズ", "cache_size": "キャッシュサイズ",
"cache_size_desc": "DNSキャッシュサイズバイト単位※キャッシュを無効化するには、この欄を空してください。", "cache_size_desc": "DNSキャッシュサイズバイト単位※キャッシュを無効化するには、「0」ゼロしてください。",
"cache_ttl_min_override": "最小TTLの上書き秒単位", "cache_ttl_min_override": "最小TTLの上書き秒単位",
"cache_ttl_max_override": "最大TTLの上書き秒単位", "cache_ttl_max_override": "最大TTLの上書き秒単位",
"enter_cache_size": "キャッシュサイズ(バイト単位)を入力してください", "enter_cache_size": "キャッシュサイズ(バイト単位)を入力してください",

View File

@@ -656,7 +656,7 @@
"blocklist": "차단 목록", "blocklist": "차단 목록",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "캐시 크기", "cache_size": "캐시 크기",
"cache_size_desc": "DNS 캐시 크기(바이트). 캐싱을 비활성화하려면 비워 둡니다.", "cache_size_desc": "DNS 캐시 크기(바이트). 캐싱을 사용하지 않으려면 0으로 설정합니다.",
"cache_ttl_min_override": "최소 TTL (초) 무시", "cache_ttl_min_override": "최소 TTL (초) 무시",
"cache_ttl_max_override": "최대 TTL (초) 무시", "cache_ttl_max_override": "최대 TTL (초) 무시",
"enter_cache_size": "캐시 크기를 입력하세요", "enter_cache_size": "캐시 크기를 입력하세요",

View File

@@ -656,7 +656,7 @@
"blocklist": "Blokkeerlijst", "blocklist": "Blokkeerlijst",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Cache grootte", "cache_size": "Cache grootte",
"cache_size_desc": "DNS-cachegrootte (in bytes). Leeg laten om caching uit te schakelen.", "cache_size_desc": "DNS-cachegrootte (in bytes). Om caching uit te schakelen, stel deze in op 0.",
"cache_ttl_min_override": "Minimale TTL overschrijven", "cache_ttl_min_override": "Minimale TTL overschrijven",
"cache_ttl_max_override": "Maximale TTL overschrijven", "cache_ttl_max_override": "Maximale TTL overschrijven",
"enter_cache_size": "Cache grootte invoeren (bytes)", "enter_cache_size": "Cache grootte invoeren (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista de bloqueio", "blocklist": "Lista de bloqueio",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Tamanho do cache", "cache_size": "Tamanho do cache",
"cache_size_desc": "Tamanho do cache do DNS (em bytes). Para desativar o cache, deixe em branco.", "cache_size_desc": "Tamanho do cache do DNS (em bytes). Para desativar o cache, defina como 0.",
"cache_ttl_min_override": "Sobrepor o TTL mínimo", "cache_ttl_min_override": "Sobrepor o TTL mínimo",
"cache_ttl_max_override": "Sobrepor o TTL máximo", "cache_ttl_max_override": "Sobrepor o TTL máximo",
"enter_cache_size": "Digite o tamanho do cache (bytes)", "enter_cache_size": "Digite o tamanho do cache (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista de bloqueio", "blocklist": "Lista de bloqueio",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Tamanho do cache", "cache_size": "Tamanho do cache",
"cache_size_desc": "Tamanho do cache DNS (em bytes). Para desativar o cache, deixar o campo vazio.", "cache_size_desc": "Tamanho do cache DNS (em bytes). Para desativar o cache, defina como 0.",
"cache_ttl_min_override": "Sobrepor o TTL mínimo", "cache_ttl_min_override": "Sobrepor o TTL mínimo",
"cache_ttl_max_override": "Sobrepor o TTL máximo", "cache_ttl_max_override": "Sobrepor o TTL máximo",
"enter_cache_size": "Digite o tamanho do cache (bytes)", "enter_cache_size": "Digite o tamanho do cache (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Чёрный список", "blocklist": "Чёрный список",
"milliseconds_abbreviation": "мс", "milliseconds_abbreviation": "мс",
"cache_size": "Размер кеша", "cache_size": "Размер кеша",
"cache_size_desc": "Размера кеша DNS (в байтах). Чтобы отключить кэширование, оставьте поле пустым.", "cache_size_desc": "Размер кеша DNS (в байтах). Чтобы отключить кеширование, установите значение 0.",
"cache_ttl_min_override": "Переопределить минимальный TTL", "cache_ttl_min_override": "Переопределить минимальный TTL",
"cache_ttl_max_override": "Переопределить максимальный TTL", "cache_ttl_max_override": "Переопределить максимальный TTL",
"enter_cache_size": "Введите размер кеша (в байтах)", "enter_cache_size": "Введите размер кеша (в байтах)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Zoznam blokovaní", "blocklist": "Zoznam blokovaní",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Veľkosť cache", "cache_size": "Veľkosť cache",
"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_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete vypnúť ukladanie do vyrovnávacej pamäte, nastavte hodnotu 0.",
"cache_ttl_min_override": "Prepísať minimálne TTL", "cache_ttl_min_override": "Prepísať minimálne TTL",
"cache_ttl_max_override": "Prepísať maximálne TTL", "cache_ttl_max_override": "Prepísať maximálne TTL",
"enter_cache_size": "Zadať veľkosť cache (v bajtoch)", "enter_cache_size": "Zadať veľkosť cache (v bajtoch)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Engel listesi", "blocklist": "Engel listesi",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Önbellek boyutu", "cache_size": "Önbellek boyutu",
"cache_size_desc": "DNS önbellek boyutu (bayt cinsinden). Önbelleğe almayı devre dışı bırakmak için boş bırakın.", "cache_size_desc": "DNS önbellek boyutu (bayt cinsinden). Önbelleği devre dışı bırakmak için 0 olarak ayarlayın.",
"cache_ttl_min_override": "Minimum kullanım süresini geçersiz kıl", "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", "cache_ttl_max_override": "Maksimum kullanım süresini geçersiz kıl",
"enter_cache_size": "Önbellek boyutunu girin (bayt)", "enter_cache_size": "Önbellek boyutunu girin (bayt)",

View File

@@ -656,7 +656,7 @@
"blocklist": "黑名单", "blocklist": "黑名单",
"milliseconds_abbreviation": "毫秒", "milliseconds_abbreviation": "毫秒",
"cache_size": "缓存大小", "cache_size": "缓存大小",
"cache_size_desc": "DNS 缓存大小(单位:字节)。若要关闭缓存,请留空。", "cache_size_desc": "DNS 缓存大小(单位:字节)。若要禁用缓存,请设置为 0。",
"cache_ttl_min_override": "覆盖最小 TTL 值", "cache_ttl_min_override": "覆盖最小 TTL 值",
"cache_ttl_max_override": "覆盖最大 TTL 值", "cache_ttl_max_override": "覆盖最大 TTL 值",
"enter_cache_size": "输入缓存大小(字节)", "enter_cache_size": "输入缓存大小(字节)",

View File

@@ -656,7 +656,7 @@
"blocklist": "封鎖清單", "blocklist": "封鎖清單",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "快取大小", "cache_size": "快取大小",
"cache_size_desc": "DNS 快取大小 (位元組)。若要停用快取,請留空。", "cache_size_desc": "DNS 快取大小位元組。若要停用快取,請設為 0。",
"cache_ttl_min_override": "覆寫最小的存活時間TTL", "cache_ttl_min_override": "覆寫最小的存活時間TTL",
"cache_ttl_max_override": "覆寫最大的存活時間TTL", "cache_ttl_max_override": "覆寫最大的存活時間TTL",
"enter_cache_size": "輸入快取大小(位元組)", "enter_cache_size": "輸入快取大小(位元組)",

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.24.2 go 1.24.2
require ( require (
github.com/AdguardTeam/dnsproxy v0.75.3 github.com/AdguardTeam/dnsproxy v0.75.4
github.com/AdguardTeam/golibs v0.32.8 github.com/AdguardTeam/golibs v0.32.8
github.com/AdguardTeam/urlfilter v0.20.0 github.com/AdguardTeam/urlfilter v0.20.0
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1

4
go.sum
View File

@@ -10,8 +10,8 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 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 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
github.com/AdguardTeam/dnsproxy v0.75.3 h1:pxlMNO+cP1A3px40PY/old6SAE82pkdLPUA2P3KY8u0= github.com/AdguardTeam/dnsproxy v0.75.4 h1:hTnHh9HoTYKKhKqePpIxCzfecl7dAXykZTw2gcj0I5U=
github.com/AdguardTeam/dnsproxy v0.75.3/go.mod h1:50OyTHao+uQzUJiXay08hgfvWQ3o2Q2WV99W8u8ypDE= github.com/AdguardTeam/dnsproxy v0.75.4/go.mod h1:50OyTHao+uQzUJiXay08hgfvWQ3o2Q2WV99W8u8ypDE=
github.com/AdguardTeam/golibs v0.32.8 h1:O3mc3kYcPkW3kbmd+gqzFNgUka13a+iBgFLThwOYSQE= github.com/AdguardTeam/golibs v0.32.8 h1:O3mc3kYcPkW3kbmd+gqzFNgUka13a+iBgFLThwOYSQE=
github.com/AdguardTeam/golibs v0.32.8/go.mod h1:McV1QFFlKLElKa306V4OL/T2kr7564PhsayfvTWYBVs= github.com/AdguardTeam/golibs v0.32.8/go.mod h1:McV1QFFlKLElKa306V4OL/T2kr7564PhsayfvTWYBVs=
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs= github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=

View File

@@ -355,12 +355,8 @@ func (ds *DefaultSessionStorage) store(s *Session) (err error) {
return nil return nil
} }
// FindByToken implements the [SessionStorage] interface for // FindByToken implements the [SessionStorage] interface for *DefaultSessionStorage.
// *DefaultSessionStorage. func (ds *DefaultSessionStorage) FindByToken(ctx context.Context, t SessionToken) (s *Session, err error) {
func (ds *DefaultSessionStorage) FindByToken(
ctx context.Context,
t SessionToken,
) (s *Session, err error) {
ds.mu.Lock() ds.mu.Lock()
defer ds.mu.Unlock() defer ds.mu.Unlock()

View File

@@ -11,8 +11,34 @@ import (
"slices" "slices"
"github.com/AdguardTeam/AdGuardHome/internal/whois" "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 // Source represents the source from which the information about the client has
// been obtained. // been obtained.
type Source uint8 type Source uint8

View File

@@ -35,7 +35,7 @@ type index struct {
nameToUID map[string]UID nameToUID map[string]UID
// clientIDToUID maps ClientID to UID. // clientIDToUID maps ClientID to UID.
clientIDToUID map[string]UID clientIDToUID map[ClientID]UID
// ipToUID maps IP address to UID. // ipToUID maps IP address to UID.
ipToUID map[netip.Addr]UID ipToUID map[netip.Addr]UID
@@ -54,7 +54,7 @@ type index struct {
func newIndex() (ci *index) { func newIndex() (ci *index) {
return &index{ return &index{
nameToUID: map[string]UID{}, nameToUID: map[string]UID{},
clientIDToUID: map[string]UID{}, clientIDToUID: map[ClientID]UID{},
ipToUID: map[netip.Addr]UID{}, ipToUID: map[netip.Addr]UID{},
subnetToUID: aghalg.NewSortedMap[netip.Prefix, UID](subnetCompare), subnetToUID: aghalg.NewSortedMap[netip.Prefix, UID](subnetCompare),
macToUID: map[macKey]UID{}, 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 // find finds persistent client by string representation of the ClientID, IP
// address, or MAC. // address, or MAC.
func (ci *index) find(id string) (c *Persistent, ok bool) { func (ci *index) find(id string) (c *Persistent, ok bool) {
c, ok = ci.findByClientID(id) c, ok = ci.findByClientID(ClientID(id))
if ok { if ok {
return c, true return c, true
} }
@@ -230,7 +230,7 @@ func (ci *index) find(id string) (c *Persistent, ok bool) {
} }
// findByClientID finds persistent client by ClientID. // findByClientID finds persistent client by ClientID.
func (ci *index) findByClientID(clientID string) (c *Persistent, ok bool) { func (ci *index) findByClientID(clientID ClientID) (c *Persistent, ok bool) {
uid, ok := ci.clientIDToUID[clientID] uid, ok := ci.clientIDToUID[clientID]
if ok { if ok {
return ci.uidToClient[uid], true return ci.uidToClient[uid], true
@@ -275,6 +275,26 @@ func (ci *index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
return nil, false 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. // findByMAC finds persistent client by MAC.
func (ci *index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) { func (ci *index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
k := macToKey(mac) k := macToKey(mac)

View File

@@ -5,6 +5,7 @@ import (
"net/netip" "net/netip"
"testing" "testing"
"github.com/AdguardTeam/golibs/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -58,12 +59,12 @@ func TestClientIndex_Find(t *testing.T) {
clientWithMAC = &Persistent{ clientWithMAC = &Persistent{
Name: "client_with_mac", Name: "client_with_mac",
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)}, MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
} }
clientWithID = &Persistent{ clientWithID = &Persistent{
Name: "client_with_id", Name: "client_with_id",
ClientIDs: []string{cliID}, ClientIDs: []ClientID{cliID},
} }
clientLinkLocal = &Persistent{ clientLinkLocal = &Persistent{
@@ -141,10 +142,10 @@ func TestClientIndex_Clashes(t *testing.T) {
Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)}, Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
}, { }, {
Name: "client_with_mac", Name: "client_with_mac",
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)}, MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
}, { }, {
Name: "client_with_id", Name: "client_with_id",
ClientIDs: []string{cliID}, ClientIDs: []ClientID{cliID},
}} }}
ci := newIDIndex(clients) ci := newIDIndex(clients)
@@ -181,17 +182,6 @@ 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) { func TestMACToKey(t *testing.T) {
testCases := []struct { testCases := []struct {
want any want any
@@ -200,44 +190,44 @@ func TestMACToKey(t *testing.T) {
}{{ }{{
name: "column6", name: "column6",
in: "00:00:5e:00:53:01", in: "00:00:5e:00:53:01",
want: [6]byte(mustParseMAC("00:00:5e:00:53:01")), want: [6]byte(errors.Must(net.ParseMAC("00:00:5e:00:53:01"))),
}, { }, {
name: "column8", name: "column8",
in: "02:00:5e:10:00:00:00:01", in: "02:00:5e:10:00:00:00:01",
want: [8]byte(mustParseMAC("02:00:5e:10:00:00:00:01")), want: [8]byte(errors.Must(net.ParseMAC("02:00:5e:10:00:00:00:01"))),
}, { }, {
name: "column20", name: "column20",
in: "00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01", in: "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")), 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"))),
}, { }, {
name: "hyphen6", name: "hyphen6",
in: "00-00-5e-00-53-01", in: "00-00-5e-00-53-01",
want: [6]byte(mustParseMAC("00-00-5e-00-53-01")), want: [6]byte(errors.Must(net.ParseMAC("00-00-5e-00-53-01"))),
}, { }, {
name: "hyphen8", name: "hyphen8",
in: "02-00-5e-10-00-00-00-01", in: "02-00-5e-10-00-00-00-01",
want: [8]byte(mustParseMAC("02-00-5e-10-00-00-00-01")), want: [8]byte(errors.Must(net.ParseMAC("02-00-5e-10-00-00-00-01"))),
}, { }, {
name: "hyphen20", name: "hyphen20",
in: "00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01", in: "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")), 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"))),
}, { }, {
name: "dot6", name: "dot6",
in: "0000.5e00.5301", in: "0000.5e00.5301",
want: [6]byte(mustParseMAC("0000.5e00.5301")), want: [6]byte(errors.Must(net.ParseMAC("0000.5e00.5301"))),
}, { }, {
name: "dot8", name: "dot8",
in: "0200.5e10.0000.0001", in: "0200.5e10.0000.0001",
want: [8]byte(mustParseMAC("0200.5e10.0000.0001")), want: [8]byte(errors.Must(net.ParseMAC("0200.5e10.0000.0001"))),
}, { }, {
name: "dot20", name: "dot20",
in: "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001", in: "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001",
want: [20]byte(mustParseMAC("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"))),
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
mac := mustParseMAC(tc.in) mac := errors.Must(net.ParseMAC(tc.in))
key := macToKey(mac) key := macToKey(mac)
assert.Equal(t, tc.want, key) assert.Equal(t, tc.want, key)
@@ -302,19 +292,19 @@ func TestIndex_FindByIPWithoutZone(t *testing.T) {
func TestClientIndex_RangeByName(t *testing.T) { func TestClientIndex_RangeByName(t *testing.T) {
sortedClients := []*Persistent{{ sortedClients := []*Persistent{{
Name: "clientA", Name: "clientA",
ClientIDs: []string{"A"}, ClientIDs: []ClientID{"A"},
}, { }, {
Name: "clientB", Name: "clientB",
ClientIDs: []string{"B"}, ClientIDs: []ClientID{"B"},
}, { }, {
Name: "clientC", Name: "clientC",
ClientIDs: []string{"C"}, ClientIDs: []ClientID{"C"},
}, { }, {
Name: "clientD", Name: "clientD",
ClientIDs: []string{"D"}, ClientIDs: []ClientID{"D"},
}, { }, {
Name: "clientE", Name: "clientE",
ClientIDs: []string{"E"}, ClientIDs: []ClientID{"E"},
}} }}
testCases := []struct { testCases := []struct {
@@ -349,3 +339,115 @@ 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)
})
}
}

View File

@@ -15,7 +15,6 @@ import (
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -71,7 +70,9 @@ type Persistent struct {
// Tags is a list of client tags that categorize the client. // Tags is a list of client tags that categorize the client.
Tags []string Tags []string
// Upstreams is a list of custom upstream DNS servers for the client. // 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 []string Upstreams []string
// IPs is a list of IP addresses that identify the client. The client must // IPs is a list of IP addresses that identify the client. The client must
@@ -90,15 +91,16 @@ type Persistent struct {
// ClientIDs identifying the client. The client must have at least one ID // ClientIDs identifying the client. The client must have at least one ID
// (IP, subnet, MAC, or ClientID). // (IP, subnet, MAC, or ClientID).
ClientIDs []string ClientIDs []ClientID
// UID is the unique identifier of the persistent client. // UID is the unique identifier of the persistent client.
UID UID UID UID
// UpstreamsCacheSize is the cache size for custom upstreams. // UpstreamsCacheSize defines the size of the custom upstream cache.
UpstreamsCacheSize uint32 UpstreamsCacheSize uint32
// UpstreamsCacheEnabled specifies whether custom upstreams are used. // UpstreamsCacheEnabled specifies whether the custom upstream cache is
// used. If true, the list of Upstreams should not be empty.
UpstreamsCacheEnabled bool UpstreamsCacheEnabled bool
// UseOwnSettings specifies whether custom filtering settings are used. // UseOwnSettings specifies whether custom filtering settings are used.
@@ -134,7 +136,7 @@ func (c *Persistent) validate(ctx context.Context, l *slog.Logger, allTags []str
switch { switch {
case c.Name == "": case c.Name == "":
return errors.Error("empty name") return errors.Error("empty name")
case c.IDsLen() == 0: case c.idendifiersLen() == 0:
return errors.Error("id required") return errors.Error("id required")
case c.UID == UID{}: case c.UID == UID{}:
return errors.Error("uid required") return errors.Error("uid required")
@@ -237,28 +239,15 @@ func (c *Persistent) setID(id string) (err error) {
return err return err
} }
c.ClientIDs = append(c.ClientIDs, strings.ToLower(id)) c.ClientIDs = append(c.ClientIDs, ClientID(strings.ToLower(id)))
return nil return nil
} }
// ValidateClientID returns an error if id is not a valid ClientID. // Identifiers returns a list of client identifiers containing at least one
// // element.
// TODO(s.chzhen): It's an exact copy of the [dnsforward.ValidateClientID] to func (c *Persistent) Identifiers() (ids []string) {
// avoid the import cycle. Remove it. ids = make([]string, 0, c.idendifiersLen())
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 { for _, ip := range c.IPs {
ids = append(ids, ip.String()) ids = append(ids, ip.String())
@@ -272,11 +261,15 @@ func (c *Persistent) IDs() (ids []string) {
ids = append(ids, mac.String()) ids = append(ids, mac.String())
} }
return append(ids, c.ClientIDs...) for _, cid := range c.ClientIDs {
ids = append(ids, string(cid))
}
return ids
} }
// IDsLen returns a length of ClientIDs. // identifiersLen returns the number of client identifiers.
func (c *Persistent) IDsLen() (n int) { func (c *Persistent) idendifiersLen() (n int) {
return len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs) return len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs)
} }

View File

@@ -7,6 +7,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
"strings"
"sync" "sync"
"time" "time"
@@ -18,6 +19,7 @@ import (
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile" "github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
@@ -433,48 +435,186 @@ func (s *Storage) Add(ctx context.Context, p *Persistent) (err error) {
ctx, ctx,
"client added", "client added",
"name", p.Name, "name", p.Name,
"ids", p.IDs(), "ids", p.Identifiers(),
"clients_count", s.index.size(), "clients_count", s.index.size(),
) )
return nil return nil
} }
// FindByName finds persistent client by name. And returns its shallow copy. // FindParams represents the parameters for searching a client. At least one
func (s *Storage) FindByName(name string) (p *Persistent, ok bool) { // field must be non-empty.
s.mu.Lock() type FindParams struct {
defer s.mu.Unlock() // ClientID is a unique identifier for the client used in DoH, DoT, and DoQ
// DNS queries.
ClientID ClientID
p, ok = s.index.findByName(name) // RemoteIP is the IP address used as a client search parameter.
if ok { RemoteIP netip.Addr
return p.ShallowClone(), ok
}
return nil, false // 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
} }
// Find finds persistent client by string representation of the ClientID, IP // ErrBadIdentifier is returned by [FindParams.Set] when it cannot parse the
// address, or MAC. And returns its shallow copy. // 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): Accept ClientIDData structure instead, which will contain // TODO(s.chzhen): Add support for UID.
// the parsed IP address, if any. func (p *FindParams) Set(id string) (err error) {
func (s *Storage) Find(id string) (p *Persistent, ok bool) { *p = FindParams{}
isClientID := true
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.
isClientID = false
}
if canBeValidIPPrefixString(id) {
p.Subnet, err = netip.ParsePrefix(id)
if err == nil {
isClientID = false
}
}
if canBeMACString(id) {
p.MAC, err = net.ParseMAC(id)
if err == nil {
isClientID = false
}
}
if !isClientID {
return nil
}
if !isValidClientID(id) {
return ErrBadIdentifier
}
p.ClientID = ClientID(id)
return nil
}
// canBeValidIPPrefixString is a best-effort check to determine if s is a valid
// CIDR before using [netip.ParsePrefix], aimed at reducing allocations.
//
// TODO(s.chzhen): Replace this implementation with the more robust version
// from golibs.
func canBeValidIPPrefixString(s string) (ok bool) {
ipStr, bitStr, ok := strings.Cut(s, "/")
if !ok {
return false
}
if bitStr == "" || len(bitStr) > 3 {
return false
}
bits := 0
for _, c := range bitStr {
if c < '0' || c > '9' {
return false
}
bits = bits*10 + int(c-'0')
}
if bits > 128 {
return false
}
return netutil.IsValidIPString(ipStr)
}
// canBeMACString is a best-effort check to determine if s is a valid MAC
// address before using [net.ParseMAC], aimed at reducing allocations.
//
// TODO(s.chzhen): Replace this implementation with the more robust version
// from golibs.
func canBeMACString(s string) (ok bool) {
switch len(s) {
case
len("0000.0000.0000"),
len("00:00:00:00:00:00"),
len("0000.0000.0000.0000"),
len("00:00:00:00:00:00:00:00"),
len("0000.0000.0000.0000.0000.0000.0000.0000.0000.0000"),
len("00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"):
return true
default:
return false
}
}
// 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) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
p, ok = s.index.find(id) 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
}
}
}
// 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)
if ok { if ok {
return p.ShallowClone(), ok return p, true
} }
ip, err := netip.ParseAddr(id) foundMAC := s.dhcp.MACByIP(addr)
if err != nil {
return nil, false
}
foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil { if foundMAC != nil {
return s.FindByMAC(foundMAC) return s.index.findByMAC(foundMAC)
} }
return nil, false return nil, false
@@ -487,6 +627,8 @@ func (s *Storage) Find(id string) (p *Persistent, ok bool) {
// //
// Note that multiple clients can have the same IP address with different zones. // Note that multiple clients can have the same IP address with different zones.
// Therefore, the result of this method is indeterminate. // 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) { func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@@ -498,7 +640,7 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
foundMAC := s.dhcp.MACByIP(ip) foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil { if foundMAC != nil {
return s.FindByMAC(foundMAC) return s.index.findByMAC(foundMAC)
} }
p = s.index.findByIPWithoutZone(ip) p = s.index.findByIPWithoutZone(ip)
@@ -509,17 +651,6 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
return nil, false 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 // RemoveByName removes persistent client information. ok is false if no such
// client exists by that name. // client exists by that name.
func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) { func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) {
@@ -648,9 +779,9 @@ func (s *Storage) CustomUpstreamConfig(
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
c, ok := s.index.findByClientID(id) c, ok := s.index.findByClientID(ClientID(id))
if !ok { if !ok {
c, ok = s.index.findByIP(addr) c, ok = s.findByIP(addr)
} }
if !ok { if !ok {
@@ -682,7 +813,7 @@ func (s *Storage) ClearUpstreamCache() {
// ClientID or client IP address, and applies it to the filtering settings. // ClientID or client IP address, and applies it to the filtering settings.
// setts must not be nil. // setts must not be nil.
func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filtering.Settings) { func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filtering.Settings) {
c, ok := s.index.findByClientID(id) c, ok := s.index.findByClientID(ClientID(id))
if !ok { if !ok {
c, ok = s.index.findByIP(addr) c, ok = s.index.findByIP(addr)
} }
@@ -690,7 +821,7 @@ func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filter
if !ok { if !ok {
foundMAC := s.dhcp.MACByIP(addr) foundMAC := s.dhcp.MACByIP(addr)
if foundMAC != nil { if foundMAC != nil {
c, ok = s.FindByMAC(foundMAC) c, ok = s.index.findByMAC(foundMAC)
} }
} }

View File

@@ -15,6 +15,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/whois" "github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile" "github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
@@ -350,15 +351,15 @@ func TestClientsDHCP(t *testing.T) {
cliName1 = "one.dhcp" cliName1 = "one.dhcp"
cliIP2 = netip.MustParseAddr("2.2.2.2") cliIP2 = netip.MustParseAddr("2.2.2.2")
cliMAC2 = mustParseMAC("22:22:22:22:22:22") cliMAC2 = errors.Must(net.ParseMAC("22:22:22:22:22:22"))
cliName2 = "two.dhcp" cliName2 = "two.dhcp"
cliIP3 = netip.MustParseAddr("3.3.3.3") cliIP3 = netip.MustParseAddr("3.3.3.3")
cliMAC3 = mustParseMAC("33:33:33:33:33:33") cliMAC3 = errors.Must(net.ParseMAC("33:33:33:33:33:33"))
cliName3 = "three.dhcp" cliName3 = "three.dhcp"
prsCliIP = netip.MustParseAddr("4.3.2.1") prsCliIP = netip.MustParseAddr("4.3.2.1")
prsCliMAC = mustParseMAC("AA:AA:AA:AA:AA:AA") prsCliMAC = errors.Must(net.ParseMAC("AA:AA:AA:AA:AA:AA"))
prsCliName = "persistent.dhcp" prsCliName = "persistent.dhcp"
otherARPCliName = "other.arp" otherARPCliName = "other.arp"
@@ -519,7 +520,11 @@ func TestClientsDHCP(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
prsCli, ok := storage.Find(prsCliIP.String()) params := &client.FindParams{}
err = params.Set(prsCliIP.String())
require.NoError(t, err)
prsCli, ok := storage.Find(params)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, prsCliName, prsCli.Name) assert.Equal(t, prsCliName, prsCli.Name)
@@ -663,17 +668,6 @@ func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
return s 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) { func TestStorage_Add(t *testing.T) {
const ( const (
existingName = "existing_name" existingName = "existing_name"
@@ -693,7 +687,7 @@ func TestStorage_Add(t *testing.T) {
Name: existingName, Name: existingName,
IPs: []netip.Addr{existingIP}, IPs: []netip.Addr{existingIP},
Subnets: []netip.Prefix{existingSubnet}, Subnets: []netip.Prefix{existingSubnet},
ClientIDs: []string{existingClientID}, ClientIDs: []client.ClientID{existingClientID},
UID: existingClientUID, UID: existingClientUID,
} }
@@ -761,7 +755,7 @@ func TestStorage_Add(t *testing.T) {
name: "duplicate_client_id", name: "duplicate_client_id",
cli: &client.Persistent{ cli: &client.Persistent{
Name: "duplicate_client_id", Name: "duplicate_client_id",
ClientIDs: []string{existingClientID}, ClientIDs: []client.ClientID{existingClientID},
UID: client.MustNewUID(), UID: client.MustNewUID(),
}, },
wantErrMsg: `adding client: another client "existing_name" ` + wantErrMsg: `adding client: another client "existing_name" ` +
@@ -898,12 +892,12 @@ func TestStorage_Find(t *testing.T) {
clientWithMAC = &client.Persistent{ clientWithMAC = &client.Persistent{
Name: "client_with_mac", Name: "client_with_mac",
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)}, MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
} }
clientWithID = &client.Persistent{ clientWithID = &client.Persistent{
Name: "client_with_id", Name: "client_with_id",
ClientIDs: []string{cliID}, ClientIDs: []client.ClientID{cliID},
} }
clientLinkLocal = &client.Persistent{ clientLinkLocal = &client.Persistent{
@@ -950,7 +944,11 @@ func TestStorage_Find(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
for _, id := range tc.ids { for _, id := range tc.ids {
c, ok := s.Find(id) params := &client.FindParams{}
err := params.Set(id)
require.NoError(t, err)
c, ok := s.Find(params)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, tc.want, c) assert.Equal(t, tc.want, c)
@@ -959,7 +957,11 @@ func TestStorage_Find(t *testing.T) {
} }
t.Run("not_found", func(t *testing.T) { t.Run("not_found", func(t *testing.T) {
_, ok := s.Find(cliIPNone) params := &client.FindParams{}
err := params.Set(cliIPNone)
require.NoError(t, err)
_, ok := s.Find(params)
assert.False(t, ok) assert.False(t, ok)
}) })
} }
@@ -1025,127 +1027,6 @@ 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) { func TestStorage_Update(t *testing.T) {
const ( const (
clientName = "client_name" clientName = "client_name"
@@ -1162,7 +1043,7 @@ func TestStorage_Update(t *testing.T) {
Name: obstructingName, Name: obstructingName,
IPs: []netip.Addr{obstructingIP}, IPs: []netip.Addr{obstructingIP},
Subnets: []netip.Prefix{obstructingSubnet}, Subnets: []netip.Prefix{obstructingSubnet},
ClientIDs: []string{obstructingClientID}, ClientIDs: []client.ClientID{obstructingClientID},
} }
clientToUpdate := &client.Persistent{ clientToUpdate := &client.Persistent{
@@ -1211,7 +1092,7 @@ func TestStorage_Update(t *testing.T) {
name: "duplicate_client_id", name: "duplicate_client_id",
cli: &client.Persistent{ cli: &client.Persistent{
Name: "duplicate_client_id", Name: "duplicate_client_id",
ClientIDs: []string{obstructingClientID}, ClientIDs: []client.ClientID{obstructingClientID},
UID: client.MustNewUID(), UID: client.MustNewUID(),
}, },
wantErrMsg: `updating client: another client "obstructing_name" ` + wantErrMsg: `updating client: another client "obstructing_name" ` +
@@ -1238,19 +1119,19 @@ func TestStorage_Update(t *testing.T) {
func TestStorage_RangeByName(t *testing.T) { func TestStorage_RangeByName(t *testing.T) {
sortedClients := []*client.Persistent{{ sortedClients := []*client.Persistent{{
Name: "clientA", Name: "clientA",
ClientIDs: []string{"A"}, ClientIDs: []client.ClientID{"A"},
}, { }, {
Name: "clientB", Name: "clientB",
ClientIDs: []string{"B"}, ClientIDs: []client.ClientID{"B"},
}, { }, {
Name: "clientC", Name: "clientC",
ClientIDs: []string{"C"}, ClientIDs: []client.ClientID{"C"},
}, { }, {
Name: "clientD", Name: "clientD",
ClientIDs: []string{"D"}, ClientIDs: []client.ClientID{"D"},
}, { }, {
Name: "clientE", Name: "clientE",
ClientIDs: []string{"E"}, ClientIDs: []client.ClientID{"E"},
}} }}
testCases := []struct { testCases := []struct {
@@ -1288,29 +1169,20 @@ func TestStorage_RangeByName(t *testing.T) {
func TestStorage_CustomUpstreamConfig(t *testing.T) { func TestStorage_CustomUpstreamConfig(t *testing.T) {
const ( const (
existingName = "existing_name" existingClientID = "existing_client_id"
existingClientID = "existing_client_id"
nonExistingClientID = "non_existing_client_id" nonExistingClientID = "non_existing_client_id"
) )
var ( var (
existingClientUID = client.MustNewUID() existingIP = netip.MustParseAddr("192.0.2.1")
existingIP = netip.MustParseAddr("192.0.2.1")
nonExistingIP = netip.MustParseAddr("192.0.2.255") nonExistingIP = netip.MustParseAddr("192.0.2.255")
dhcpCliIP = netip.MustParseAddr("192.0.2.2")
dhcpCliMAC = errors.Must(net.ParseMAC("02:00:00:00:00:00"))
testUpstreamTimeout = time.Second 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() date := time.Now()
clock := &faketime.Clock{ clock := &faketime.Clock{
OnNow: func() (now time.Time) { OnNow: func() (now time.Time) {
@@ -1320,7 +1192,30 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
}, },
} }
s := newTestStorage(t, clock) 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.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{ s.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{
UpstreamTimeout: testUpstreamTimeout, UpstreamTimeout: testUpstreamTimeout,
}) })
@@ -1329,8 +1224,21 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
return s.Shutdown(testutil.ContextWithTimeout(t, testTimeout)) return s.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
}) })
ctx := testutil.ContextWithTimeout(t, testTimeout) err = s.Add(ctx, &client.Persistent{
err := s.Add(ctx, existingClient) 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"},
})
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@@ -1348,6 +1256,11 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
cliID: "", cliID: "",
cliAddr: existingIP, cliAddr: existingIP,
wantNilConf: assert.NotNil, wantNilConf: assert.NotNil,
}, {
name: "client_dhcp",
cliID: "",
cliAddr: dhcpCliIP,
wantNilConf: assert.NotNil,
}, { }, {
name: "non_existing_client_id", name: "non_existing_client_id",
cliID: nonExistingClientID, cliID: nonExistingClientID,
@@ -1380,4 +1293,193 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
assert.NotEqual(t, conf, updConf) 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
} }

View File

@@ -138,6 +138,7 @@ func (m *upstreamManager) customUpstreamConfig(uid UID) (proxyConf *proxy.Custom
proxyConf = newCustomUpstreamConfig(cliConf, m.commonConf) proxyConf = newCustomUpstreamConfig(cliConf, m.commonConf)
cliConf.proxyConf = proxyConf cliConf.proxyConf = proxyConf
cliConf.commonConfUpdate = m.confUpdate
cliConf.isChanged = false cliConf.isChanged = false
return proxyConf return proxyConf

View File

@@ -1,13 +1,11 @@
package dhcpsvc_test package dhcpsvc_test
import ( import (
"net"
"net/netip" "net/netip"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/stretchr/testify/require"
) )
// testLocalTLD is a common local TLD for tests. // testLocalTLD is a common local TLD for tests.
@@ -56,11 +54,3 @@ 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
}

View File

@@ -2,6 +2,7 @@ package dhcpsvc_test
import ( import (
"io/fs" "io/fs"
"net"
"net/netip" "net/netip"
"os" "os"
"path" "path"
@@ -11,6 +12,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -176,9 +178,9 @@ func TestDHCPServer_AddLease(t *testing.T) {
newIP = netip.MustParseAddr("192.168.0.3") newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2") newIPv6 = netip.MustParseAddr("2001:db8::2")
existMAC = mustParseMAC(t, "01:02:03:04:05:06") existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
newMAC = mustParseMAC(t, "06:05:04:03:02:01") newMAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07") ipv6MAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
) )
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{ require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
@@ -291,9 +293,9 @@ func TestDHCPServer_index(t *testing.T) {
ip3 = netip.MustParseAddr("172.16.0.3") ip3 = netip.MustParseAddr("172.16.0.3")
ip4 = netip.MustParseAddr("172.16.0.4") ip4 = netip.MustParseAddr("172.16.0.4")
mac1 = mustParseMAC(t, "01:02:03:04:05:06") mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
mac2 = mustParseMAC(t, "06:05:04:03:02:01") mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
mac3 = mustParseMAC(t, "02:03:04:05:06:07") mac3 = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
) )
t.Run("ip_idx", func(t *testing.T) { t.Run("ip_idx", func(t *testing.T) {
@@ -349,9 +351,9 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
ip3 = netip.MustParseAddr("192.168.0.4") ip3 = netip.MustParseAddr("192.168.0.4")
ip4 = netip.MustParseAddr("2001:db8::3") ip4 = netip.MustParseAddr("2001:db8::3")
mac1 = mustParseMAC(t, "01:02:03:04:05:06") mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
mac2 = mustParseMAC(t, "06:05:04:03:02:01") mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
mac3 = mustParseMAC(t, "06:05:04:03:02:02") mac3 = errors.Must(net.ParseMAC("06:05:04:03:02:02"))
) )
testCases := []struct { testCases := []struct {
@@ -452,9 +454,9 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
newIP = netip.MustParseAddr("192.168.0.3") newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2") newIPv6 = netip.MustParseAddr("2001:db8::2")
existMAC = mustParseMAC(t, "01:02:03:04:05:06") existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
newMAC = mustParseMAC(t, "02:03:04:05:06:07") newMAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01") ipv6MAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
) )
testCases := []struct { testCases := []struct {
@@ -559,13 +561,13 @@ func TestServer_Leases(t *testing.T) {
Expiry: expiry, Expiry: expiry,
IP: netip.MustParseAddr("192.168.0.3"), IP: netip.MustParseAddr("192.168.0.3"),
Hostname: "example.host", Hostname: "example.host",
HWAddr: mustParseMAC(t, "AA:AA:AA:AA:AA:AA"), HWAddr: errors.Must(net.ParseMAC("AA:AA:AA:AA:AA:AA")),
IsStatic: false, IsStatic: false,
}, { }, {
Expiry: time.Time{}, Expiry: time.Time{},
IP: netip.MustParseAddr("192.168.0.4"), IP: netip.MustParseAddr("192.168.0.4"),
Hostname: "example.static.host", Hostname: "example.static.host",
HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"), HWAddr: errors.Must(net.ParseMAC("BB:BB:BB:BB:BB:BB")),
IsStatic: true, IsStatic: true,
}} }}
assert.ElementsMatch(t, wantLeases, srv.Leases()) assert.ElementsMatch(t, wantLeases, srv.Leases())

View File

@@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/golibs/container" "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
@@ -51,7 +52,7 @@ func processAccessClients(
} else if ipnet, err = netip.ParsePrefix(s); err == nil { } else if ipnet, err = netip.ParsePrefix(s); err == nil {
*nets = append(*nets, ipnet) *nets = append(*nets, ipnet)
} else { } else {
err = ValidateClientID(s) err = client.ValidateClientID(s)
if err != nil { if err != nil {
return fmt.Errorf("value %q at index %d: bad ip, cidr, or clientid", s, i) return fmt.Errorf("value %q at index %d: bad ip, cidr, or clientid", s, i)
} }

View File

@@ -7,26 +7,13 @@ import (
"path" "path"
"strings" "strings"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/quic-go/quic-go" "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 // clientIDFromClientServerName extracts and validates a ClientID. hostSrvName
// is the server name of the host. cliSrvName is the server name as sent by the // 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, // client. When strict is true, and client and host server name don't match,
@@ -53,7 +40,7 @@ func clientIDFromClientServerName(
} }
clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1] clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1]
err = ValidateClientID(clientID) err = client.ValidateClientID(clientID)
if err != nil { if err != nil {
// Don't wrap the error, because it's informative enough as is. // Don't wrap the error, because it's informative enough as is.
return "", err return "", err
@@ -93,7 +80,7 @@ func clientIDFromDNSContextHTTPS(pctx *proxy.DNSContext) (clientID string, err e
return "", fmt.Errorf("clientid check: invalid path %q: extra parts", origPath) return "", fmt.Errorf("clientid check: invalid path %q: extra parts", origPath)
} }
err = ValidateClientID(clientID) err = client.ValidateClientID(clientID)
if err != nil { if err != nil {
return "", fmt.Errorf("clientid check: %w", err) return "", fmt.Errorf("clientid check: %w", err)
} }

View File

@@ -1,131 +1,317 @@
package home package home
import ( import (
"context" "crypto/rand"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghuser" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil" "go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// webUser represents a user of the Web UI. // sessionTokenSize is the length of session token in bytes.
type webUser struct { const sessionTokenSize = 16
// Name represents the login name of the web user.
Name string `yaml:"name"`
// PasswordHash is the hashed representation of the web user password. type session struct {
PasswordHash string `yaml:"password"` userName string
// expire is the expiration time, in seconds.
// UserID is the unique identifier of the web user. expire uint32
//
// TODO(s.chzhen): !! Use this.
UserID aghuser.UserID `yaml:"-"`
} }
// toUser returns the new properly initialized *aghuser.User using stored func (s *session) serialize() []byte {
// properties. It panics if there is an error generating the user ID. const (
func (wu *webUser) toUser() (u *aghuser.User) { expireLen = 4
uid := wu.UserID nameLen = 2
if uid == (aghuser.UserID{}) { )
uid = aghuser.MustNewUserID() data := make([]byte, expireLen+nameLen+len(s.userName))
} binary.BigEndian.PutUint32(data[0:4], s.expire)
binary.BigEndian.PutUint16(data[4:6], uint16(len(s.userName)))
copy(data[6:], []byte(s.userName))
return data
}
return &aghuser.User{ func (s *session) deserialize(data []byte) bool {
Password: aghuser.NewDefaultPassword(wu.PasswordHash), if len(data) < 4+2 {
Login: aghuser.Login(wu.Name), return false
ID: uid,
} }
s.expire = binary.BigEndian.Uint32(data[0:4])
nameLen := binary.BigEndian.Uint16(data[4:6])
data = data[6:]
if len(data) < int(nameLen) {
return false
}
s.userName = string(data)
return true
} }
// Auth is the global authentication object. // Auth is the global authentication object.
type Auth struct { type Auth struct {
logger *slog.Logger
rateLimiter *authRateLimiter
sessions aghuser.SessionStorage
trustedProxies netutil.SubnetSet trustedProxies netutil.SubnetSet
users aghuser.DB db *bbolt.DB
rateLimiter *authRateLimiter
sessions map[string]*session
users []webUser
lock sync.Mutex
sessionTTL uint32
} }
// InitAuth initializes the global authentication object. baseLogger, // webUser represents a user of the Web UI.
// rateLimiter, trustedProxies must not be nil. dbFilename and sessionTTL //
// should not be empty. // TODO(s.chzhen): Improve naming.
type webUser struct {
Name string `yaml:"name"`
PasswordHash string `yaml:"password"`
}
// InitAuth initializes the global authentication object.
func InitAuth( func InitAuth(
ctx context.Context,
baseLogger *slog.Logger,
dbFilename string, dbFilename string,
users []webUser, users []webUser,
sessionTTL time.Duration, sessionTTL uint32,
rateLimiter *authRateLimiter, rateLimiter *authRateLimiter,
trustedProxies netutil.SubnetSet, trustedProxies netutil.SubnetSet,
) (a *Auth, err error) { ) (a *Auth) {
userDB := aghuser.NewDefaultDB() log.Info("Initializing auth module: %s", dbFilename)
for i, u := range users {
err = userDB.Create(ctx, u.toUser())
if err != nil {
return nil, fmt.Errorf("users: at index %d: %w", i, err)
}
}
s, err := aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{ a = &Auth{
Logger: baseLogger.With(slogutil.KeyPrefix, "session_storage"), sessionTTL: sessionTTL,
Clock: timeutil.SystemClock{},
UserDB: aghuser.NewDefaultDB(),
DBPath: dbFilename,
SessionTTL: sessionTTL,
})
if err != nil {
return nil, fmt.Errorf("creating session storage: %w", err)
}
return &Auth{
logger: baseLogger.With(slogutil.KeyPrefix, "auth"),
rateLimiter: rateLimiter, rateLimiter: rateLimiter,
sessions: make(map[string]*session),
users: users,
trustedProxies: trustedProxies, trustedProxies: trustedProxies,
sessions: s, }
users: userDB, var err error
}, nil
a.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, nil)
if err != nil {
log.Error("auth: open DB: %s: %s", dbFilename, err)
if err.Error() == "invalid argument" {
log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations")
}
return nil
}
a.loadSessions()
log.Info("auth: initialized. users:%d sessions:%d", len(a.users), len(a.sessions))
return a
} }
// Close closes the authentication database. // Close closes the authentication database.
func (a *Auth) Close(ctx context.Context) { func (a *Auth) Close() {
err := a.sessions.Close() _ = a.db.Close()
}
func bucketName() []byte {
return []byte("sessions-2")
}
// loadSessions loads sessions from the database file and removes expired
// sessions.
func (a *Auth) loadSessions() {
tx, err := a.db.Begin(true)
if err != nil { if err != nil {
a.logger.ErrorContext(ctx, "closing session storage", slogutil.KeyError, err) log.Error("auth: bbolt.Begin: %s", err)
return
}
defer func() {
_ = tx.Rollback()
}()
bkt := tx.Bucket(bucketName())
if bkt == nil {
return
}
removed := 0
if tx.Bucket([]byte("sessions")) != nil {
_ = tx.DeleteBucket([]byte("sessions"))
removed = 1
}
now := uint32(time.Now().UTC().Unix())
forEach := func(k, v []byte) error {
s := session{}
if !s.deserialize(v) || s.expire <= now {
err = bkt.Delete(k)
if err != nil {
log.Error("auth: bbolt.Delete: %s", err)
} else {
removed++
}
return nil
}
a.sessions[hex.EncodeToString(k)] = &s
return nil
}
_ = bkt.ForEach(forEach)
if removed != 0 {
err = tx.Commit()
if err != nil {
log.Error("bolt.Commit(): %s", err)
}
}
log.Debug("auth: loaded %d sessions from DB (removed %d expired)", len(a.sessions), removed)
}
// addSession adds a new session to the list of sessions and saves it in the
// database file.
func (a *Auth) addSession(data []byte, s *session) {
name := hex.EncodeToString(data)
a.lock.Lock()
a.sessions[name] = s
a.lock.Unlock()
if a.storeSession(data, s) {
log.Debug("auth: created session %s: expire=%d", name, s.expire)
} }
} }
// isValidSession returns true if the session is valid. // storeSession saves a session in the database file.
func (a *Auth) isValidSession(ctx context.Context, cookieSess string) (ok bool) { func (a *Auth) storeSession(data []byte, s *session) bool {
sess, err := hex.DecodeString(cookieSess) tx, err := a.db.Begin(true)
if err != nil { if err != nil {
a.logger.ErrorContext(ctx, "checking session: decoding cookie", slogutil.KeyError, err) log.Error("auth: bbolt.Begin: %s", err)
return false
}
defer func() {
_ = tx.Rollback()
}()
bkt, err := tx.CreateBucketIfNotExists(bucketName())
if err != nil {
log.Error("auth: bbolt.CreateBucketIfNotExists: %s", err)
return false return false
} }
var t aghuser.SessionToken err = bkt.Put(data, s.serialize())
copy(t[:], sess)
s, err := a.sessions.FindByToken(ctx, t)
if err != nil { if err != nil {
a.logger.ErrorContext(ctx, "checking session", slogutil.KeyError, err) log.Error("auth: bbolt.Put: %s", err)
return false return false
} }
return s != nil err = tx.Commit()
if err != nil {
log.Error("auth: bbolt.Commit: %s", err)
return false
}
return true
} }
// addUser adds a new user with the given password. u must not be nil. // removeSessionFromFile removes a stored session from the DB file on disk.
func (a *Auth) addUser(ctx context.Context, u *webUser, password string) (err error) { func (a *Auth) removeSessionFromFile(sess []byte) {
tx, err := a.db.Begin(true)
if err != nil {
log.Error("auth: bbolt.Begin: %s", err)
return
}
defer func() {
_ = tx.Rollback()
}()
bkt := tx.Bucket(bucketName())
if bkt == nil {
log.Error("auth: bbolt.Bucket")
return
}
err = bkt.Delete(sess)
if err != nil {
log.Error("auth: bbolt.Put: %s", err)
return
}
err = tx.Commit()
if err != nil {
log.Error("auth: bbolt.Commit: %s", err)
return
}
log.Debug("auth: removed session from DB")
}
// checkSessionResult is the result of checking a session.
type checkSessionResult int
// checkSessionResult constants.
const (
checkSessionOK checkSessionResult = 0
checkSessionNotFound checkSessionResult = -1
checkSessionExpired checkSessionResult = 1
)
// checkSession checks if the session is valid.
func (a *Auth) checkSession(sess string) (res checkSessionResult) {
now := uint32(time.Now().UTC().Unix())
update := false
a.lock.Lock()
defer a.lock.Unlock()
s, ok := a.sessions[sess]
if !ok {
return checkSessionNotFound
}
if s.expire <= now {
delete(a.sessions, sess)
key, _ := hex.DecodeString(sess)
a.removeSessionFromFile(key)
return checkSessionExpired
}
newExpire := now + a.sessionTTL
if s.expire/(24*60*60) != newExpire/(24*60*60) {
// update expiration time once a day
update = true
s.expire = newExpire
}
if update {
key, _ := hex.DecodeString(sess)
if a.storeSession(key, s) {
log.Debug("auth: updated session %s: expire=%d", sess, s.expire)
}
}
return checkSessionOK
}
// removeSession removes the session from the active sessions and the disk.
func (a *Auth) removeSession(sess string) {
key, _ := hex.DecodeString(sess)
a.lock.Lock()
delete(a.sessions, sess)
a.lock.Unlock()
a.removeSessionFromFile(key)
}
// addUser adds a new user with the given password.
func (a *Auth) addUser(u *webUser, password string) (err error) {
if len(password) == 0 { if len(password) == 0 {
return errors.Error("empty password") return errors.Error("empty password")
} }
@@ -137,129 +323,97 @@ func (a *Auth) addUser(ctx context.Context, u *webUser, password string) (err er
u.PasswordHash = string(hash) u.PasswordHash = string(hash)
err = a.users.Create(ctx, u.toUser()) a.lock.Lock()
if err != nil { defer a.lock.Unlock()
// Should not happen.
panic(err)
}
a.logger.DebugContext(ctx, "added user", "login", u.Name) a.users = append(a.users, *u)
log.Debug("auth: added user with login %q", u.Name)
return nil return nil
} }
// findUser returns a user if one exists with the provided login and the // findUser returns a user if there is one.
// password matches. func (a *Auth) findUser(login, password string) (u webUser, ok bool) {
func (a *Auth) findUser(ctx context.Context, login, password string) (user *aghuser.User) { a.lock.Lock()
user, err := a.users.ByLogin(ctx, aghuser.Login(login)) defer a.lock.Unlock()
if err != nil {
return nil for _, u = range a.users {
if u.Name == login &&
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
return u, true
}
} }
ok := user.Password.Authenticate(ctx, password) return webUser{}, false
if !ok {
return nil
}
return user
} }
// getCurrentUser searches for a user using a cookie or credentials from basic // getCurrentUser returns the current user. It returns an empty User if the
// authentication. // user is not found.
func (a *Auth) getCurrentUser(r *http.Request) (user *aghuser.User) { func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
ctx := r.Context()
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if err != nil { if err != nil {
// There's no Cookie, check Basic authentication. // There's no Cookie, check Basic authentication.
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if ok { if ok {
return a.findUser(ctx, user, pass) u, _ = globalContext.auth.findUser(user, pass)
return u
} }
return nil return webUser{}
} }
sess, err := hex.DecodeString(cookie.Value) a.lock.Lock()
if err != nil { defer a.lock.Unlock()
a.logger.ErrorContext(
ctx,
"searching for user: decoding cookie value",
slogutil.KeyError, err,
)
return nil s, ok := a.sessions[cookie.Value]
if !ok {
return webUser{}
} }
var t aghuser.SessionToken for _, u = range a.users {
copy(t[:], sess) if u.Name == s.userName {
return u
s, err := a.sessions.FindByToken(ctx, t) }
if err != nil {
a.logger.ErrorContext(ctx, "searching for user", slogutil.KeyError, err)
return nil
} }
if s == nil { return webUser{}
return nil
}
return &aghuser.User{
Login: s.UserLogin,
ID: s.UserID,
}
}
// removeSession deletes the session from the active sessions and the disk. It
// also logs any occurring errors.
func (a *Auth) removeSession(ctx context.Context, cookieSess string) {
sess, err := hex.DecodeString(cookieSess)
if err != nil {
a.logger.ErrorContext(ctx, "removing session: decoding cookie", slogutil.KeyError, err)
return
}
var t aghuser.SessionToken
copy(t[:], sess)
err = a.sessions.DeleteByToken(ctx, t)
if err != nil {
a.logger.ErrorContext(ctx, "removing session by token", slogutil.KeyError, err)
}
} }
// usersList returns a copy of a users list. // usersList returns a copy of a users list.
func (a *Auth) usersList(ctx context.Context) (webUsers []webUser) { func (a *Auth) usersList() (users []webUser) {
users, err := a.users.All(ctx) a.lock.Lock()
if err != nil { defer a.lock.Unlock()
// Should not happen.
panic(err)
}
webUsers = make([]webUser, 0, len(users)) users = make([]webUser, len(a.users))
for _, u := range users { copy(users, a.users)
webUsers = append(webUsers, webUser{
Name: string(u.Login),
PasswordHash: string(u.Password.Hash()),
UserID: u.ID,
})
}
return webUsers return users
} }
// authRequired returns true if a authentication is required. // authRequired returns true if a authentication is required.
func (a *Auth) authRequired(ctx context.Context) (ok bool) { func (a *Auth) authRequired() bool {
if GLMode { if GLMode {
return true return true
} }
users, err := a.users.All(ctx) a.lock.Lock()
if err != nil { defer a.lock.Unlock()
// Should not happen.
panic(err)
}
return len(users) != 0 return len(a.users) != 0
}
// newSessionToken returns cryptographically secure randomly generated slice of
// bytes of sessionTokenSize length.
//
// TODO(e.burkov): Think about using byte array instead of byte slice.
func newSessionToken() (data []byte) {
randData := make([]byte, sessionTokenSize)
// Since Go 1.24, crypto/rand.Read doesn't return an error and crashes
// unrecoverably instead.
_, _ = rand.Read(randData)
return randData
} }

View File

@@ -0,0 +1,69 @@
package home
import (
"encoding/hex"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuth(t *testing.T) {
dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db")
users := []webUser{{
Name: "name",
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}}
a := InitAuth(fn, nil, 60, nil, nil)
s := session{}
user := webUser{Name: "name"}
err := a.addUser(&user, "password")
require.NoError(t, err)
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
a.removeSession("notfound")
sess := newSessionToken()
sessStr := hex.EncodeToString(sess)
now := time.Now().UTC().Unix()
// check expiration
s.expire = uint32(now)
a.addSession(sess, &s)
assert.Equal(t, checkSessionExpired, a.checkSession(sessStr))
// add session with TTL = 2 sec
s = session{}
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.addSession(sess, &s)
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
a.Close()
// load saved session
a = InitAuth(fn, users, 60, nil, nil)
// the session is still alive
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
// reset our expiration time because checkSession() has just updated it
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.storeSession(sess, &s)
a.Close()
u, ok := a.findUser("name", "password")
assert.True(t, ok)
assert.NotEmpty(t, u.Name)
time.Sleep(3 * time.Second)
// load and remove expired sessions
a = InitAuth(fn, users, 60, nil, nil)
assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
a.Close()
}

View File

@@ -1,7 +1,6 @@
package home package home
import ( import (
"context"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -33,14 +32,10 @@ type loginJSON struct {
} }
// newCookie creates a new authentication cookie. // newCookie creates a new authentication cookie.
func (a *Auth) newCookie( func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) {
ctx context.Context,
req loginJSON,
addr string,
) (c *http.Cookie, err error) {
rateLimiter := a.rateLimiter rateLimiter := a.rateLimiter
u := a.findUser(ctx, req.Name, req.Password) u, ok := a.findUser(req.Name, req.Password)
if u == nil { if !ok {
if rateLimiter != nil { if rateLimiter != nil {
rateLimiter.inc(addr) rateLimiter.inc(addr)
} }
@@ -52,16 +47,19 @@ func (a *Auth) newCookie(
rateLimiter.remove(addr) rateLimiter.remove(addr)
} }
s, err := a.sessions.New(ctx, u) sess := newSessionToken()
if err != nil { now := time.Now().UTC()
return nil, fmt.Errorf("creating session: %w", err)
} a.addSession(sess, &session{
userName: u.Name,
expire: uint32(now.Unix()) + a.sessionTTL,
})
return &http.Cookie{ return &http.Cookie{
Name: sessionCookieName, Name: sessionCookieName,
Value: hex.EncodeToString(s.Token[:]), Value: hex.EncodeToString(sess),
Path: "/", Path: "/",
Expires: time.Now().Add(cookieTTL), Expires: now.Add(cookieTTL),
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
}, nil }, nil
@@ -174,7 +172,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err) log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
} }
cookie, err := globalContext.auth.newCookie(r.Context(), req, remoteIP) cookie, err := globalContext.auth.newCookie(req, remoteIP)
if err != nil { if err != nil {
logIP := remoteIP logIP := remoteIP
if globalContext.auth.trustedProxies.Contains(ip.Unmap()) { if globalContext.auth.trustedProxies.Contains(ip.Unmap()) {
@@ -211,7 +209,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
return return
} }
globalContext.auth.removeSession(r.Context(), c.Value) globalContext.auth.removeSession(c.Value)
c = &http.Cookie{ c = &http.Cookie{
Name: sessionCookieName, Name: sessionCookieName,
@@ -244,7 +242,28 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
return false return false
} }
if u := globalContext.auth.getCurrentUser(r); u != nil { // redirect to login page if not authenticated
isAuthenticated := false
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// Check Basic authentication.
user, pass, hasBasic := r.BasicAuth()
if hasBasic {
_, isAuthenticated = globalContext.auth.findUser(user, pass)
if !isAuthenticated {
log.Info("%s: invalid basic authorization value", pref)
}
}
} else {
res := globalContext.auth.checkSession(cookie.Value)
isAuthenticated = res == checkSessionOK
if !isAuthenticated {
log.Debug("%s: invalid cookie value: %q", pref, cookie)
}
}
if isAuthenticated {
return false return false
} }
@@ -270,14 +289,14 @@ func optionalAuth(
h func(http.ResponseWriter, *http.Request), h func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) { ) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
p := r.URL.Path p := r.URL.Path
authRequired := globalContext.auth != nil && globalContext.auth.authRequired(ctx) authRequired := globalContext.auth != nil && globalContext.auth.authRequired()
if p == "/login.html" { if p == "/login.html" {
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil { if authRequired && err == nil {
// Redirect to the dashboard if already authenticated. // Redirect to the dashboard if already authenticated.
if globalContext.auth.isValidSession(ctx, cookie.Value) { res := globalContext.auth.checkSession(cookie.Value)
if res == checkSessionOK {
http.Redirect(w, r, "", http.StatusFound) http.Redirect(w, r, "", http.StatusFound)
return return

View File

@@ -1,21 +1,355 @@
package home package home
import ( import (
"bytes"
"encoding/json"
"net/http" "net/http"
"net/http/httptest"
"net/netip" "net/netip"
"net/textproto" "net/textproto"
"net/url" "net/url"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/httphdr" "github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
) )
// TODO(s.chzhen): !! Add more tests.
func TestAuth_ServeHTTP_first_run(t *testing.T) {
storeGlobals(t)
globalContext.firstRun = true
mux := http.NewServeMux()
globalContext.mux = mux
var (
logger = slogutil.NewDiscardLogger()
ctx = testutil.ContextWithTimeout(t, testTimeout)
err error
)
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
globalContext.web = web
testCases := []struct {
url string
method string
code int
}{{
url: "/",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/apple/doh.mobileconfig",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/apple/dot.mobileconfig",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/i18n/change_language",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/i18n/current_language",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/install/check_config",
method: http.MethodPost,
code: http.StatusBadRequest,
}, {
url: "/control/install/configure",
method: http.MethodPost,
code: http.StatusBadRequest,
}, {
url: "/control/install/get_addresses",
method: http.MethodGet,
code: http.StatusOK,
}, {
url: "/control/login",
method: http.MethodPost,
code: http.StatusFound,
}, {
url: "/control/logout",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/profile",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/profile/update",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/status",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/update",
method: http.MethodGet,
code: http.StatusFound,
}, {
url: "/control/version.json",
method: http.MethodGet,
code: http.StatusFound,
}}
for _, tc := range testCases {
t.Run(tc.url, func(t *testing.T) {
r := httptest.NewRequest(tc.method, tc.url, nil)
h, pattern := mux.Handler(r)
require.NotEmpty(t, pattern)
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
assert.Equal(t, tc.code, w.Code)
})
}
}
func TestAuth_ServeHTTP(t *testing.T) {
storeGlobals(t)
const (
authNone = iota
authBasic
authCookie
)
const (
testTTL = 60
userName = "name"
userPassword = "password"
)
var (
logger = slogutil.NewDiscardLogger()
ctx = testutil.ContextWithTimeout(t, testTimeout)
err error
)
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
require.NoError(t, err)
sessionsDB := filepath.Join(t.TempDir(), "sessions.db")
users := []webUser{{
Name: userName,
PasswordHash: string(passwordHash),
}}
auth := InitAuth(sessionsDB, users, testTTL, nil, nil)
globalContext.auth = auth
mux := http.NewServeMux()
globalContext.mux = mux
tlsMgr, err := newTLSManager(ctx, &tlsManagerConfig{
logger: logger,
configModified: func() {},
})
require.NoError(t, err)
web, err := initWeb(ctx, options{}, nil, nil, logger, tlsMgr, false)
require.NoError(t, err)
globalContext.web = web
creds, err := json.Marshal(&loginJSON{Name: userName, Password: userPassword})
require.NoError(t, err)
r := httptest.NewRequest(http.MethodPost, "/control/login", bytes.NewReader(creds))
r.Header.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)
w := httptest.NewRecorder()
mux.ServeHTTP(w, r)
var loginCookie *http.Cookie
for _, c := range w.Result().Cookies() {
if c.Name == sessionCookieName {
loginCookie = c
}
}
require.NotNil(t, loginCookie)
testCases := []struct {
url string
method string
authMethod int
wantCode int
}{{
url: "/",
method: http.MethodGet,
authMethod: authNone,
wantCode: http.StatusFound,
}, {
url: "/control/i18n/change_language",
method: http.MethodPost,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/i18n/change_language",
method: http.MethodPost,
authMethod: authBasic,
wantCode: http.StatusInternalServerError,
}, {
url: "/control/i18n/change_language",
method: http.MethodPost,
authMethod: authCookie,
wantCode: http.StatusInternalServerError,
}, {
url: "/control/i18n/current_language",
method: http.MethodGet,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/i18n/current_language",
method: http.MethodGet,
authMethod: authBasic,
wantCode: http.StatusOK,
}, {
url: "/control/i18n/current_language",
method: http.MethodGet,
authMethod: authCookie,
wantCode: http.StatusOK,
}, {
url: "/control/logout",
method: http.MethodGet,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/logout",
method: http.MethodGet,
authMethod: authBasic,
wantCode: http.StatusFound,
}, {
url: "/control/profile",
method: http.MethodGet,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/profile",
method: http.MethodGet,
authMethod: authBasic,
wantCode: http.StatusOK,
}, {
url: "/control/profile",
method: http.MethodGet,
authMethod: authCookie,
wantCode: http.StatusOK,
}, {
url: "/control/profile/update",
method: http.MethodPut,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/profile/update",
method: http.MethodPut,
authMethod: authBasic,
wantCode: http.StatusBadRequest,
}, {
url: "/control/profile/update",
method: http.MethodPut,
authMethod: authCookie,
wantCode: http.StatusBadRequest,
}, {
url: "/control/status",
method: http.MethodGet,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/status",
method: http.MethodGet,
authMethod: authBasic,
wantCode: http.StatusOK,
}, {
url: "/control/status",
method: http.MethodGet,
authMethod: authCookie,
wantCode: http.StatusOK,
}, {
url: "/control/update",
method: http.MethodPost,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/version.json",
method: http.MethodGet,
authMethod: authNone,
wantCode: http.StatusForbidden,
}, {
url: "/control/version.json",
method: http.MethodGet,
authMethod: authBasic,
wantCode: http.StatusOK,
}, {
url: "/control/version.json",
method: http.MethodGet,
authMethod: authCookie,
wantCode: http.StatusOK,
}}
for _, tc := range testCases {
t.Run(tc.url, func(t *testing.T) {
r = httptest.NewRequest(tc.method, tc.url, nil)
switch tc.authMethod {
case authNone:
// Go on.
case authBasic:
r.SetBasicAuth(userName, userPassword)
case authCookie:
r.AddCookie(loginCookie)
default:
panic("unrecognized auth method")
}
h, pattern := mux.Handler(r)
require.NotEmpty(t, pattern)
w = httptest.NewRecorder()
h.ServeHTTP(w, r)
assert.Equal(t, tc.wantCode, w.Code)
})
}
t.Run("logout", func(t *testing.T) {
r = httptest.NewRequest(http.MethodGet, "/control/status", nil)
r.AddCookie(loginCookie)
w = httptest.NewRecorder()
mux.ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
r = httptest.NewRequest(http.MethodGet, "/control/logout", nil)
r.AddCookie(loginCookie)
w = httptest.NewRecorder()
mux.ServeHTTP(w, r)
assert.Equal(t, http.StatusFound, w.Code)
r = httptest.NewRequest(http.MethodGet, "/control/status", nil)
r.AddCookie(loginCookie)
w = httptest.NewRecorder()
mux.ServeHTTP(w, r)
assert.Equal(t, http.StatusForbidden, w.Code)
})
}
// implements http.ResponseWriter // implements http.ResponseWriter
type testResponseWriter struct { type testResponseWriter struct {
hdr http.Header hdr http.Header
@@ -35,20 +369,13 @@ func (w *testResponseWriter) WriteHeader(statusCode int) {
} }
func TestAuthHTTP(t *testing.T) { func TestAuthHTTP(t *testing.T) {
var (
ctx = testutil.ContextWithTimeout(t, testTimeout)
logger = slogutil.NewDiscardLogger()
err error
)
dir := t.TempDir() dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db") fn := filepath.Join(dir, "sessions.db")
users := []webUser{ users := []webUser{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
globalContext.auth, err = InitAuth(ctx, logger, fn, users, time.Minute, nil, nil) globalContext.auth = InitAuth(fn, users, 60, nil, nil)
require.NoError(t, err)
handlerCalled := false handlerCalled := false
handler := func(_ http.ResponseWriter, _ *http.Request) { handler := func(_ http.ResponseWriter, _ *http.Request) {
@@ -77,11 +404,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
// perform login // perform login
cookie, err := globalContext.auth.newCookie( cookie, err := globalContext.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "")
ctx,
loginJSON{Name: "name", Password: "password"},
"",
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, cookie) require.NotNil(t, cookie)
@@ -127,7 +450,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del(httphdr.Cookie) r.Header.Del(httphdr.Cookie)
globalContext.auth.Close(ctx) globalContext.auth.Close()
} }
func TestRealIP(t *testing.T) { func TestRealIP(t *testing.T) {

View File

@@ -28,6 +28,10 @@ type clientsContainer struct {
// filter. It must not be nil. // filter. It must not be nil.
baseLogger *slog.Logger 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 stores information about persistent clients.
storage *client.Storage storage *client.Storage
@@ -58,6 +62,7 @@ type clientsContainer struct {
// BlockedClientChecker checks if a client is blocked by the current access // BlockedClientChecker checks if a client is blocked by the current access
// settings. // settings.
type BlockedClientChecker interface { type BlockedClientChecker interface {
// TODO(s.chzhen): Accept [client.FindParams].
IsBlockedClient(ip netip.Addr, clientID string) (blocked bool, rule string) IsBlockedClient(ip netip.Addr, clientID string) (blocked bool, rule string)
} }
@@ -80,6 +85,7 @@ func (clients *clientsContainer) Init(
} }
clients.baseLogger = baseLogger clients.baseLogger = baseLogger
clients.logger = baseLogger.With(slogutil.KeyPrefix, "client_container")
clients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize clients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize
clients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime) clients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime)
@@ -269,7 +275,7 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
BlockedServices: cli.BlockedServices.Clone(), BlockedServices: cli.BlockedServices.Clone(),
IDs: cli.IDs(), IDs: cli.Identifiers(),
Tags: slices.Clone(cli.Tags), Tags: slices.Clone(cli.Tags),
Upstreams: slices.Clone(cli.Upstreams), Upstreams: slices.Clone(cli.Upstreams),
@@ -356,15 +362,27 @@ func (clients *clientsContainer) clientOrArtificial(
}, true }, true
} }
// shouldCountClient is a wrapper around [clientsContainer.find] to make it a // shouldCountClient is a wrapper around [client.Storage.Find] to make it a
// valid client information finder for the statistics. If no information about // valid client information finder for the statistics. If no information about
// the client is found, it returns true. // 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].
func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) { func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {
clients.lock.Lock() clients.lock.Lock()
defer clients.lock.Unlock() defer clients.lock.Unlock()
params := &client.FindParams{}
for _, id := range ids { for _, id := range ids {
client, ok := clients.storage.Find(id) 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)
if ok { if ok {
return !client.IgnoreStatistics return !client.IgnoreStatistics
} }

View File

@@ -300,7 +300,7 @@ func clientToJSON(c *client.Persistent) (cj *clientJSON) {
return &clientJSON{ return &clientJSON{
Name: c.Name, Name: c.Name,
IDs: c.IDs(), IDs: c.Identifiers(),
Tags: c.Tags, Tags: c.Tags,
UseGlobalSettings: !c.UseOwnSettings, UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled, FilteringEnabled: c.FilteringEnabled,
@@ -428,32 +428,53 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
// Deprecated: Remove it when migration to the new API is over. // Deprecated: Remove it when migration to the new API is over.
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query() q := r.URL.Query()
data := []map[string]*clientJSON{} data := make([]map[string]*clientJSON, 0, len(q))
params := &client.FindParams{}
var err error
for i := range len(q) { for i := range len(q) {
idStr := q.Get(fmt.Sprintf("ip%d", i)) idStr := q.Get(fmt.Sprintf("ip%d", i))
if idStr == "" { if idStr == "" {
break 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{ data = append(data, map[string]*clientJSON{
idStr: clients.findClient(idStr), idStr: clients.findClient(idStr, params),
}) })
} }
aghhttp.WriteJSONResponseOK(w, r, data) aghhttp.WriteJSONResponseOK(w, r, data)
} }
// findClient returns available information about a client by idStr from the // findClient returns available information about a client by params from the
// client's storage or access settings. cj is guaranteed to be non-nil. // client's storage or access settings. idStr is the string representation of
func (clients *clientsContainer) findClient(idStr string) (cj *clientJSON) { // typed params. params must not be nil. cj is guaranteed to be non-nil.
ip, _ := netip.ParseAddr(idStr) func (clients *clientsContainer) findClient(
c, ok := clients.storage.Find(idStr) idStr string,
params *client.FindParams,
) (cj *clientJSON) {
c, ok := clients.storage.Find(params)
if !ok { if !ok {
return clients.findRuntime(ip, idStr) return clients.findRuntime(idStr, params)
} }
cj = clientToJSON(c) cj = clientToJSON(c)
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr) disallowed, rule := clients.clientChecker.IsBlockedClient(
params.RemoteIP,
string(params.ClientID),
)
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
return cj return cj
@@ -472,7 +493,8 @@ type searchClientJSON struct {
ID string `json:"id"` 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) { func (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *http.Request) {
q := searchQueryJSON{} q := searchQueryJSON{}
err := json.NewDecoder(r.Body).Decode(&q) err := json.NewDecoder(r.Body).Decode(&q)
@@ -482,11 +504,25 @@ func (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *ht
return return
} }
data := []map[string]*clientJSON{} data := make([]map[string]*clientJSON, 0, len(q.Clients))
params := &client.FindParams{}
for _, c := range q.Clients { for _, c := range q.Clients {
idStr := c.ID 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{ data = append(data, map[string]*clientJSON{
idStr: clients.findClient(idStr), idStr: clients.findClient(idStr, params),
}) })
} }
@@ -494,38 +530,37 @@ func (clients *clientsContainer) handleSearchClient(w http.ResponseWriter, r *ht
} }
// findRuntime looks up the IP in runtime and temporary storages, like // findRuntime looks up the IP in runtime and temporary storages, like
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be // /etc/hosts tables, DHCP leases, or blocklists. params must not be nil. cj
// non-nil. // is guaranteed to be non-nil.
func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *clientJSON) { func (clients *clientsContainer) findRuntime(
idStr string,
params *client.FindParams,
) (cj *clientJSON) {
var host string
whois := &whois.Info{}
ip := params.RemoteIP
rc := clients.storage.ClientRuntime(ip) rc := clients.storage.ClientRuntime(ip)
if rc == nil { if rc != nil {
// It is still possible that the IP used to be in the runtime clients _, host = rc.Info()
// list, but then the server was reloaded. So, check the DNS server's whois = whoisOrEmpty(rc)
// 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
} }
_, host := rc.Info() // Check the DNS server's blocked IP list regardless of whether a runtime
cj = &clientJSON{ // client was found or not. This is because it's still possible that the
Name: host, // runtime client associated with the IP address was stored previously, but
IDs: []string{idStr}, // then the server was reloaded.
WHOIS: whoisOrEmpty(rc), //
// 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,
} }
disallowed, rule := clients.clientChecker.IsBlockedClient(ip, idStr)
cj.Disallowed, cj.DisallowedRule = &disallowed, &rule
return cj
} }
// RegisterClientsHandlers registers HTTP handlers // RegisterClientsHandlers registers HTTP handlers

View File

@@ -153,7 +153,7 @@ func TestClientsContainer_HandleAddClient(t *testing.T) {
clientTwo := newPersistentClientWithIDs(t, "client2", []string{testClientIP2}) clientTwo := newPersistentClientWithIDs(t, "client2", []string{testClientIP2})
clientEmptyID := newPersistentClient("empty_client_id") clientEmptyID := newPersistentClient("empty_client_id")
clientEmptyID.ClientIDs = []string{""} clientEmptyID.ClientIDs = []client.ClientID{""}
testCases := []struct { testCases := []struct {
name string name string
@@ -278,7 +278,7 @@ func TestClientsContainer_HandleUpdateClient(t *testing.T) {
clientModified := newPersistentClientWithIDs(t, "client2", []string{testClientIP2}) clientModified := newPersistentClientWithIDs(t, "client2", []string{testClientIP2})
clientEmptyID := newPersistentClient("empty_client_id") clientEmptyID := newPersistentClient("empty_client_id")
clientEmptyID.ClientIDs = []string{""} clientEmptyID.ClientIDs = []client.ClientID{""}
testCases := []struct { testCases := []struct {
name string name string

View File

@@ -2,7 +2,6 @@ package home
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"net/netip" "net/netip"
"os" "os"
@@ -749,8 +748,7 @@ func (c *configuration) write(tlsMgr *tlsManager) (err error) {
defer c.Unlock() defer c.Unlock()
if globalContext.auth != nil { if globalContext.auth != nil {
// TODO(s.chzhen): Pass context. config.Users = globalContext.auth.usersList()
config.Users = globalContext.auth.usersList(context.TODO())
} }
if tlsMgr != nil { if tlsMgr != nil {

View File

@@ -392,8 +392,6 @@ const PasswordMinRunes = 8
// Apply new configuration, start DNS server, restart Web server // Apply new configuration, start DNS server, restart Web server
func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, restartHTTP, err := decodeApplyConfigReq(r.Body) req, restartHTTP, err := decodeApplyConfigReq(r.Body)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -441,7 +439,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
u := &webUser{ u := &webUser{
Name: req.Username, Name: req.Username,
} }
err = globalContext.auth.addUser(ctx, u, req.Password) err = globalContext.auth.addUser(u, req.Password)
if err != nil { if err != nil {
globalContext.firstRun = true globalContext.firstRun = true
copyInstallSettings(config, curConfig) copyInstallSettings(config, curConfig)
@@ -454,7 +452,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
// moment we'll allow setting up TLS in the initial configuration or the // moment we'll allow setting up TLS in the initial configuration or the
// configuration itself will use HTTPS protocol, because the underlying // configuration itself will use HTTPS protocol, because the underlying
// functions potentially restart the HTTPS server. // functions potentially restart the HTTPS server.
err = startMods(ctx, web.baseLogger, web.tlsManager) err = startMods(r.Context(), web.baseLogger, web.tlsManager)
if err != nil { if err != nil {
globalContext.firstRun = true globalContext.firstRun = true
copyInstallSettings(config, curConfig) copyInstallSettings(config, curConfig)
@@ -490,11 +488,11 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
// and with its own context, because it waits until all requests are handled // and with its own context, because it waits until all requests are handled
// and will be blocked by it's own caller. // and will be blocked by it's own caller.
go func(timeout time.Duration) { go func(timeout time.Duration) {
shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer slogutil.RecoverAndLog(shutdownCtx, web.logger) defer slogutil.RecoverAndLog(ctx, web.logger)
defer cancel() defer cancel()
shutdownSrv(shutdownCtx, web.logger, web.httpServer) shutdownSrv(ctx, web.logger, web.httpServer)
}(shutdownTimeout) }(shutdownTimeout)
} }

View File

@@ -347,13 +347,6 @@ func newDNSTLSConfig(
return nil, fmt.Errorf(format, err) return nil, fmt.Errorf(format, err)
} }
// Unencrypted DoH is managed by AdGuard Home itself, not by dnsproxy.
// Therefore, avoid setting the certificate property to prevent dnsproxy
// from starting encrypted listeners. See [dnsforward.Server.prepareTLS].
if conf.AllowUnencryptedDoH {
return dnsConf, nil
}
dnsConf.Cert = &cert dnsConf.Cert = &cert
return dnsConf, nil return dnsConf, nil

View File

@@ -668,7 +668,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
GLMode = opts.glinetMode GLMode = opts.glinetMode
// Init auth module. // Init auth module.
globalContext.auth, err = initUsers(ctx, slogLogger) globalContext.auth, err = initUsers()
fatalOnError(err) fatalOnError(err)
web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL) web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
@@ -786,8 +786,7 @@ func checkPermissions(
} }
// initUsers initializes context auth module. Clears config users field. // initUsers initializes context auth module. Clears config users field.
// baseLogger must not be nil. func initUsers() (auth *Auth, err error) {
func initUsers(ctx context.Context, baseLogger *slog.Logger) (auth *Auth, err error) {
sessFilename := filepath.Join(globalContext.getDataDir(), "sessions.db") sessFilename := filepath.Join(globalContext.getDataDir(), "sessions.db")
var rateLimiter *authRateLimiter var rateLimiter *authRateLimiter
@@ -800,17 +799,10 @@ func initUsers(ctx context.Context, baseLogger *slog.Logger) (auth *Auth, err er
trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies)) trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies))
auth, err = InitAuth( sessionTTL := time.Duration(config.HTTPConfig.SessionTTL).Seconds()
ctx, auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter, trustedProxies)
baseLogger, if auth == nil {
sessFilename, return nil, errors.Error("initializing auth module failed")
config.Users,
time.Duration(config.HTTPConfig.SessionTTL),
rateLimiter,
trustedProxies,
)
if err != nil {
return nil, fmt.Errorf("initializing auth module: %w", err)
} }
config.Users = nil config.Users = nil
@@ -924,7 +916,7 @@ func cleanup(ctx context.Context) {
globalContext.web = nil globalContext.web = nil
} }
if globalContext.auth != nil { if globalContext.auth != nil {
globalContext.auth.Close(ctx) globalContext.auth.Close()
globalContext.auth = nil globalContext.auth = nil
} }

View File

@@ -8,7 +8,7 @@ import (
"net/url" "net/url"
"path" "path"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr" "github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
@@ -151,7 +151,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
clientID := q.Get("client_id") clientID := q.Get("client_id")
if clientID != "" { if clientID != "" {
err = dnsforward.ValidateClientID(clientID) err = client.ValidateClientID(clientID)
if err != nil { if err != nil {
respondJSONError(w, http.StatusBadRequest, err.Error()) respondJSONError(w, http.StatusBadRequest, err.Error())

View File

@@ -47,11 +47,7 @@ type profileJSON struct {
// handleGetProfile is the handler for GET /control/profile endpoint. // handleGetProfile is the handler for GET /control/profile endpoint.
func handleGetProfile(w http.ResponseWriter, r *http.Request) { func handleGetProfile(w http.ResponseWriter, r *http.Request) {
name := ""
u := globalContext.auth.getCurrentUser(r) u := globalContext.auth.getCurrentUser(r)
if u != nil {
name = string(u.Login)
}
var resp profileJSON var resp profileJSON
func() { func() {
@@ -59,7 +55,7 @@ func handleGetProfile(w http.ResponseWriter, r *http.Request) {
defer config.RUnlock() defer config.RUnlock()
resp = profileJSON{ resp = profileJSON{
Name: name, Name: u.Name,
Language: config.Language, Language: config.Language,
Theme: config.Theme, Theme: config.Theme,
} }

View File

@@ -113,10 +113,13 @@ func TestValidateCertificates(t *testing.T) {
// restores them once the test is complete. // restores them once the test is complete.
// //
// The global variables are: // The global variables are:
// - [configuration.dns] // - [configuration]
// - [homeContext.auth]
// - [homeContext.clients.storage] // - [homeContext.clients.storage]
// - [homeContext.dnsServer] // - [homeContext.dnsServer]
// - [homeContext.firstRun]
// - [homeContext.mux] // - [homeContext.mux]
// - [homeContext.web]
// //
// TODO(s.chzhen): Remove this once the TLS manager no longer accesses global // TODO(s.chzhen): Remove this once the TLS manager no longer accesses global
// variables. Make tests that use this helper concurrent. // variables. Make tests that use this helper concurrent.
@@ -124,15 +127,21 @@ func storeGlobals(tb testing.TB) {
tb.Helper() tb.Helper()
prevConfig := config prevConfig := config
auth := globalContext.auth
storage := globalContext.clients.storage storage := globalContext.clients.storage
dnsServer := globalContext.dnsServer dnsServer := globalContext.dnsServer
firstRun := globalContext.firstRun
mux := globalContext.mux mux := globalContext.mux
web := globalContext.web
tb.Cleanup(func() { tb.Cleanup(func() {
config = prevConfig config = prevConfig
globalContext.auth = auth
globalContext.clients.storage = storage globalContext.clients.storage = storage
globalContext.dnsServer = dnsServer globalContext.dnsServer = dnsServer
globalContext.firstRun = firstRun
globalContext.mux = mux globalContext.mux = mux
globalContext.web = web
}) })
} }
@@ -204,6 +213,8 @@ func assertCertSerialNumber(tb testing.TB, conf *tlsConfigSettings, wantSN int64
func TestTLSManager_Reload(t *testing.T) { func TestTLSManager_Reload(t *testing.T) {
storeGlobals(t) storeGlobals(t)
config.DNS.Port = 0
var ( var (
logger = slogutil.NewDiscardLogger() logger = slogutil.NewDiscardLogger()
ctx = testutil.ContextWithTimeout(t, testTimeout) ctx = testutil.ContextWithTimeout(t, testTimeout)
@@ -260,6 +271,10 @@ func TestTLSManager_Reload(t *testing.T) {
m.reload(ctx) 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() conf = m.config()
assertCertSerialNumber(t, conf, snAfter) assertCertSerialNumber(t, conf, snAfter)
} }

View File

@@ -980,7 +980,8 @@
- 'clients' - 'clients'
'operationId': 'clientsSearch' 'operationId': 'clientsSearch'
'summary': > 'summary': >
Get information about clients by their IP addresses, CIDRs, MAC addresses, or ClientIDs. Retrieve information about clients by performing an exact match search
using IP addresses, CIDRs, MAC addresses, or ClientIDs.
'requestBody': 'requestBody':
'content': 'content':
'application/json': 'application/json':