Compare commits

...

11 Commits

Author SHA1 Message Date
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
Eugene Burkov
c7c62ad3b6 Pull request 2395: Update all
Merge in DNS/adguard-home from upd-all to master

Squashed commit of the following:

commit c4bba4531813bdeb79536f7f601ade80a16a9163
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Apr 21 18:17:06 2025 +0300

    client: upd filters
2025-04-21 18:46:47 +03:00
Stanislav Chzhen
003e7ce0d5 Pull request 2393: 7773-fix-unencrypted_doh
Updates #7773.

Squashed commit of the following:

commit d9ca09c1d9b251998107fc87bd6daeb5999ea803
Merge: b67a71a7a a8fdf1c55
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 21 15:56:57 2025 +0300

    Merge branch 'master' into 7773-fix-unencrypted_doh

commit b67a71a7a9686d36cbf64a3f7561886bff7d9c5c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 18 16:01:49 2025 +0300

    home: imp docs

commit dab9b0582ff1ebc4637d5ec1ea3bc81190ed4066
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 18 15:09:36 2025 +0300

    home: fix unencrypted doh
2025-04-21 16:05:16 +03:00
Stanislav Chzhen
a8fdf1c553 Pull request 2386: AGDNS-2743-aghuser-session
Merge in DNS/adguard-home from AGDNS-2743-aghuser-session to master

Squashed commit of the following:

commit 74fd4bc11eaf784880855fa2c710a747428db146
Merge: 844e865f6 7d479baba
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 18 18:14:36 2025 +0300

    Merge branch 'master' into AGDNS-2743-aghuser-session

commit 844e865f647efb4de7f057c392894c8f65bab422
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 18 15:18:44 2025 +0300

    aghuser: imp fmt

commit 584288e0a3ddbe6d7ae31c80c22b8f397cfd0cae
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 17 20:16:54 2025 +0300

    aghuser: imp tests

commit ea4c8735585f6d30d6dedf2a40a8dd6b07609d07
Merge: c3fd8fe5e 3521e8ed9
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 17 20:10:06 2025 +0300

    Merge branch 'master' into AGDNS-2743-aghuser-session

commit c3fd8fe5eabaf2022a971197c018e140c254006d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 17 15:23:45 2025 +0300

    aghuser: imp tests

commit dfd9aba337227a8d3edc6f5a68f3f039afd1ca0b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 16 21:40:14 2025 +0300

    aghuser: imp code

commit b6e75223bf7960f3a2e94c1a3ed7cc33539b9806
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 14 21:49:20 2025 +0300

    aghuser: imp code

commit 56d6f9d478eec399c376992ffb0f1ca5b797986d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 11 16:58:11 2025 +0300

    aghuser: user db

commit 6fdc2f60bf7f93e72d917abb12af8e4867143b6d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 14:11:22 2025 +0300

    all: upd scripts

commit 575946756f3f622360c5feafe3e721eee010e230
Merge: 7e1fac4ec 1cc6c00e4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 14:05:46 2025 +0300

    Merge branch 'master' into AGDNS-2743-aghuser-session

commit 7e1fac4ecb1bde0013bca3f6b64e82d81a78c9c3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 10 14:05:35 2025 +0300

    aghuser: session storage

commit acfb040f0bdff501c7304ea100b9faf1c07291ae
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 8 15:54:24 2025 +0300

    aghuser: session
2025-04-18 18:34:10 +03:00
46 changed files with 1528 additions and 436 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.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.
-->
### 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
- 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.
[mr-xiang-li]: https://lixiang521.com/
### Fixed
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
- Searching for persistent clients using an exact match for CIDR in the `POST /clients/search HTTP API`.
[mr-xiang-li]: https://lixiang521.com/
[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1
## [v0.107.60] - 2025-04-14
@@ -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].
### 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]).
- 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
<!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...HEAD
[v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.62...HEAD
[v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...v0.107.62
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...HEAD
[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.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

View File

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

View File

@@ -656,7 +656,7 @@
"blocklist": "Zakázaný",
"milliseconds_abbreviation": "ms",
"cache_size": "Velikost mezipaměti",
"cache_size_desc": "Velikost mezipaměti DNS (v bajtech). Chcete-li ukládání do mezipaměti zakázat, 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_max_override": "Přepsat maximální hodnotu TTL",
"enter_cache_size": "Zadejte velikost mezipaměti (v bajtech)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Sortliste",
"milliseconds_abbreviation": "ms",
"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_max_override": "Tilsidesæt maksimal TTL",
"enter_cache_size": "Angiv cache-størrelse (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Sperrliste",
"milliseconds_abbreviation": "ms",
"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_max_override": "TTL-Höchstwert überschreiben",
"enter_cache_size": "Größe des Cache (Bytes) eingeben",

View File

@@ -656,7 +656,7 @@
"blocklist": "Blocklist",
"milliseconds_abbreviation": "ms",
"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_max_override": "Override maximum TTL",
"enter_cache_size": "Enter cache size (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista de bloqueo",
"milliseconds_abbreviation": "ms",
"cache_size": "Tamaño de la caché",
"cache_size_desc": "Tamaño de la caché DNS (en bytes). Para 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_max_override": "Anular TTL máximo",
"enter_cache_size": "Ingresa el tamaño de la caché (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Liste de blocage",
"milliseconds_abbreviation": "ms",
"cache_size": "Taille du cache",
"cache_size_desc": "Taille du cache DNS (en octets). Pour désactiver la mise en cache, 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_max_override": "Remplacer le TTL maximum",
"enter_cache_size": "Entrer la taille du cache (octets)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista nera",
"milliseconds_abbreviation": "ms",
"cache_size": "Dimensioni cache",
"cache_size_desc": "Dimensione della cache DNS (in byte). Per disabilitare la 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_max_override": "Sovrascrivi TTL massimo",
"enter_cache_size": "Immetti dimensioni cache (in byte)",

View File

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

View File

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

View File

@@ -656,7 +656,7 @@
"blocklist": "Blokkeerlijst",
"milliseconds_abbreviation": "ms",
"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_max_override": "Maximale TTL overschrijven",
"enter_cache_size": "Cache grootte invoeren (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista de bloqueio",
"milliseconds_abbreviation": "ms",
"cache_size": "Tamanho do cache",
"cache_size_desc": "Tamanho do cache do DNS (em bytes). Para desativar o cache, 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_max_override": "Sobrepor o TTL máximo",
"enter_cache_size": "Digite o tamanho do cache (bytes)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Lista de bloqueio",
"milliseconds_abbreviation": "ms",
"cache_size": "Tamanho do cache",
"cache_size_desc": "Tamanho do cache DNS (em bytes). Para desativar o cache, 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_max_override": "Sobrepor o TTL máximo",
"enter_cache_size": "Digite o tamanho do cache (bytes)",

View File

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

View File

@@ -656,7 +656,7 @@
"blocklist": "Zoznam blokovaní",
"milliseconds_abbreviation": "ms",
"cache_size": "Veľkosť cache",
"cache_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete 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_max_override": "Prepísať maximálne TTL",
"enter_cache_size": "Zadať veľkosť cache (v bajtoch)",

View File

@@ -656,7 +656,7 @@
"blocklist": "Engel listesi",
"milliseconds_abbreviation": "ms",
"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_max_override": "Maksimum kullanım süresini geçersiz kıl",
"enter_cache_size": "Önbellek boyutunu girin (bayt)",

View File

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

View File

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

View File

@@ -28,6 +28,12 @@ export default {
"homepage": "https://badmojr.github.io/1Hosts/",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
},
"1hosts_pro": {
"name": "1Hosts (Pro)",
"categoryId": "general",
"homepage": "https://badmojr.github.io/1Hosts/",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_64.txt"
},
"CHN_adrules": {
"name": "CHN: AdRules DNS List",
"categoryId": "regional",

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.24.2
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/urlfilter v0.20.0
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/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
github.com/AdguardTeam/dnsproxy v0.75.3 h1:pxlMNO+cP1A3px40PY/old6SAE82pkdLPUA2P3KY8u0=
github.com/AdguardTeam/dnsproxy v0.75.3/go.mod h1:50OyTHao+uQzUJiXay08hgfvWQ3o2Q2WV99W8u8ypDE=
github.com/AdguardTeam/dnsproxy v0.75.4 h1:hTnHh9HoTYKKhKqePpIxCzfecl7dAXykZTw2gcj0I5U=
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/go.mod h1:McV1QFFlKLElKa306V4OL/T2kr7564PhsayfvTWYBVs=
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=

View File

@@ -10,7 +10,8 @@ import (
// Login is the type for web user logins.
type Login string
// NewLogin returns a web user login.
// NewLogin returns a web user login. The length of s must not be greater than
// [math.MaxUint16].
//
// TODO(s.chzhen): Add more constraints as needed.
func NewLogin(s string) (l Login, err error) {

View File

@@ -0,0 +1,35 @@
package aghuser
import (
"crypto/rand"
"time"
)
// SessionToken is the type for the web user session token.
type SessionToken [16]byte
// NewSessionToken returns a cryptographically secure randomly generated web
// user session token. If an error occurs during random generation, it will
// cause the program to crash.
func NewSessionToken() (t SessionToken) {
_, _ = rand.Read(t[:])
return t
}
// Session represents a web user session.
type Session struct {
// Expire indicates when the session will expire.
Expire time.Time
// UserLogin is the login of the web user associated with the session.
//
// TODO(s.chzhen): Remove this field and associate the user by UserID.
UserLogin Login
// Token is the session token.
Token SessionToken
// UserID is the identifier of the web user associated with the session.
UserID UserID
}

View File

@@ -0,0 +1,449 @@
package aghuser
import (
"context"
"encoding/binary"
"fmt"
"log/slog"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
"go.etcd.io/bbolt"
berrors "go.etcd.io/bbolt/errors"
)
// SessionStorage is an interface that defines methods for handling web user
// sessions. All methods must be safe for concurrent use.
//
// TODO(s.chzhen): Add DeleteAll method.
type SessionStorage interface {
// New creates a new session for the web user.
New(ctx context.Context, u *User) (s *Session, err error)
// FindByToken returns the stored session for the web user based on the session
// token.
//
// TODO(s.chzhen): Consider function signature change to reflect the
// in-memory implementation, as it currently always returns nil for error.
FindByToken(ctx context.Context, t SessionToken) (s *Session, err error)
// DeleteByToken removes a stored web user session by the provided token.
DeleteByToken(ctx context.Context, t SessionToken) (err error)
// Close releases the web user sessions database resources.
Close() (err error)
}
// DefaultSessionStorageConfig represents the web user session storage
// configuration structure.
type DefaultSessionStorageConfig struct {
// Logger is used for logging the operation of the session storage. It must
// not be nil.
Logger *slog.Logger
// Clock is used to get the current time. It must not be nil.
Clock timeutil.Clock
// UserDB contains the web user information such as ID, login, and password.
// It must not be nil.
UserDB DB
// DBPath is the path to the database file where session data is stored. It
// must not be empty.
DBPath string
// SessionTTL is the default Time-To-Live duration for web user sessions.
// It specifies how long a session should last and is a required field.
SessionTTL time.Duration
}
// DefaultSessionStorage is the default bbolt database implementation of the
// [SessionStorage] interface.
type DefaultSessionStorage struct {
// db is an instance of the bbolt database where web user sessions are
// stored by [SessionToken] in the [bucketNameSessions] bucket.
db *bbolt.DB
// logger is used for logging the operation of the session storage.
logger *slog.Logger
// mu protects sessions.
mu *sync.Mutex
// clock is used to get the current time.
clock timeutil.Clock
// userDB contains the web user information such as ID, login, and password.
userDB DB
// sessions maps a session token to a web user session.
sessions map[SessionToken]*Session
// sessionTTL is the default Time-To-Live value for web user sessions.
sessionTTL time.Duration
}
// NewDefaultSessionStorage returns the new properly initialized
// *DefaultSessionStorage.
func NewDefaultSessionStorage(
ctx context.Context,
conf *DefaultSessionStorageConfig,
) (ds *DefaultSessionStorage, err error) {
ds = &DefaultSessionStorage{
clock: conf.Clock,
userDB: conf.UserDB,
logger: conf.Logger,
mu: &sync.Mutex{},
sessions: map[SessionToken]*Session{},
sessionTTL: conf.SessionTTL,
}
dbFilename := conf.DBPath
// TODO(s.chzhen): Pass logger with options.
ds.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, nil)
if err != nil {
ds.logger.ErrorContext(ctx, "opening db %q: %w", dbFilename, err)
if errors.Is(err, berrors.ErrInvalid) {
const s = "AdGuard Home cannot be initialized due to an incompatible file system.\n" +
"Please read the explanation here: https://adguard-dns.io/kb/adguard-home/getting-started/#limitations"
slogutil.PrintLines(ctx, ds.logger, slog.LevelError, "", s)
}
return nil, err
}
err = ds.loadSessions(ctx)
if err != nil {
return nil, fmt.Errorf("loading sessions: %w", err)
}
return ds, nil
}
// loadSessions loads web user sessions from the bbolt database.
func (ds *DefaultSessionStorage) loadSessions(ctx context.Context) (err error) {
tx, err := ds.db.Begin(true)
if err != nil {
return fmt.Errorf("starting transaction: %w", err)
}
needRollback := true
defer func() {
if needRollback {
err = errors.WithDeferred(err, tx.Rollback())
}
}()
bkt := tx.Bucket([]byte(bboltBucketSessions))
if bkt == nil {
return nil
}
removed, err := ds.processSessions(ctx, bkt)
if err != nil {
return fmt.Errorf("processing sessions: %w", err)
}
if removed == 0 {
ds.logger.DebugContext(ctx, "loading sessions from db", "stored", len(ds.sessions))
return nil
}
needRollback = false
err = tx.Commit()
if err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
ds.logger.DebugContext(
ctx,
"loading sessions from db",
"stored", len(ds.sessions),
"removed", removed,
)
return nil
}
// processSessions iterates over the sessions bucket and loads or removes
// sessions as needed.
func (ds *DefaultSessionStorage) processSessions(
ctx context.Context,
bkt *bbolt.Bucket,
) (removed int, err error) {
invalidSessions := [][]byte{}
err = bkt.ForEach(ds.bboltSessionHandler(ctx, &invalidSessions))
if err != nil {
return 0, fmt.Errorf("iterating over sessions: %w", err)
}
var errs []error
for _, s := range invalidSessions {
if err = bkt.Delete(s); err != nil {
errs = append(errs, err)
}
}
if err = errors.Join(errs...); err != nil {
return 0, fmt.Errorf("deleting sessions: %w", err)
}
return len(invalidSessions), nil
}
// bboltSessionHandler returns a function for [bbolt.Bucket.ForEach] that
// iterates over stored sessions, deserializes them, and logs any errors
// encountered. The returned error is always nil, as these errors are
// considered non-critical to stop the iteration process.
func (ds *DefaultSessionStorage) bboltSessionHandler(
ctx context.Context,
invalidSessions *[][]byte,
) (fn func(k, v []byte) (err error)) {
now := ds.clock.Now()
return func(k, v []byte) (err error) {
s, err := bboltDecode(v)
if err != nil {
*invalidSessions = append(*invalidSessions, k)
ds.logger.DebugContext(ctx, "deserializing session", slogutil.KeyError, err)
return nil
}
if now.After(s.Expire) {
*invalidSessions = append(*invalidSessions, k)
return nil
}
u, err := ds.userDB.ByLogin(ctx, s.UserLogin)
if err != nil {
// Should not happen, as it currently always returns nil for error.
panic(err)
}
if u == nil {
*invalidSessions = append(*invalidSessions, k)
ds.logger.DebugContext(ctx, "no saved user by name", "name", s.UserLogin)
return nil
}
t := SessionToken(k)
s.Token = t
s.UserID = u.ID
ds.sessions[t] = s
return nil
}
}
// bboltBucketSessions is the name of the bucket storing web user sessions in
// the bbolt database.
const bboltBucketSessions = "sessions-2"
const (
// bboltSessionExpireLen is the length of the expire field in the binary
// entry stored in bbolt.
bboltSessionExpireLen = 4
// bboltSessionNameLen is the length of the name field in the binary entry
// stored in bbolt.
bboltSessionNameLen = 2
)
// bboltDecode deserializes decodes a binary data into a session.
func bboltDecode(data []byte) (s *Session, err error) {
if len(data) < bboltSessionExpireLen+bboltSessionNameLen {
return nil, fmt.Errorf("length of the data is less than expected: got %d", len(data))
}
expireData := data[:bboltSessionExpireLen]
nameLenData := data[bboltSessionExpireLen : bboltSessionExpireLen+bboltSessionNameLen]
nameData := data[bboltSessionExpireLen+bboltSessionNameLen:]
nameLen := binary.BigEndian.Uint16(nameLenData)
if len(nameData) != int(nameLen) {
return nil, fmt.Errorf("login: expected length %d, got %d", nameLen, len(nameData))
}
expire := binary.BigEndian.Uint32(expireData)
return &Session{
Expire: time.Unix(int64(expire), 0),
UserLogin: Login(nameData),
}, nil
}
// bboltEncode serializes a session properties into a binary data.
func bboltEncode(s *Session) (data []byte) {
data = make([]byte, bboltSessionExpireLen+bboltSessionNameLen+len(s.UserLogin))
expireData := data[:bboltSessionExpireLen]
nameLenData := data[bboltSessionExpireLen : bboltSessionExpireLen+bboltSessionNameLen]
nameData := data[bboltSessionExpireLen+bboltSessionNameLen:]
expire := uint32(s.Expire.Unix())
binary.BigEndian.PutUint32(expireData, expire)
binary.BigEndian.PutUint16(nameLenData, uint16(len(s.UserLogin)))
copy(nameData, []byte(s.UserLogin))
return data
}
// type check
var _ SessionStorage = (*DefaultSessionStorage)(nil)
// New implements the [SessionStorage] interface for *DefaultSessionStorage.
func (ds *DefaultSessionStorage) New(ctx context.Context, u *User) (s *Session, err error) {
s = &Session{
Token: NewSessionToken(),
UserID: u.ID,
UserLogin: u.Login,
Expire: ds.clock.Now().Add(ds.sessionTTL),
}
err = ds.store(s)
if err != nil {
return nil, fmt.Errorf("storing session: %w", err)
}
ds.mu.Lock()
defer ds.mu.Unlock()
ds.sessions[s.Token] = s
return s, nil
}
// store saves a web user session in the bbolt database.
func (ds *DefaultSessionStorage) store(s *Session) (err error) {
tx, err := ds.db.Begin(true)
if err != nil {
return fmt.Errorf("starting transaction: %w", err)
}
needRollback := true
defer func() {
if needRollback {
err = errors.WithDeferred(err, tx.Rollback())
}
}()
bkt, err := tx.CreateBucketIfNotExists([]byte(bboltBucketSessions))
if err != nil {
return fmt.Errorf("creating bucket: %w", err)
}
err = bkt.Put(s.Token[:], bboltEncode(s))
if err != nil {
return fmt.Errorf("putting data: %w", err)
}
needRollback = false
err = tx.Commit()
if err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
return nil
}
// FindByToken implements the [SessionStorage] interface for *DefaultSessionStorage.
func (ds *DefaultSessionStorage) FindByToken(ctx context.Context, t SessionToken) (s *Session, err error) {
ds.mu.Lock()
defer ds.mu.Unlock()
s, ok := ds.sessions[t]
if !ok {
return nil, nil
}
now := ds.clock.Now()
if now.After(s.Expire) {
err = ds.deleteByToken(ctx, t)
if err != nil {
return nil, fmt.Errorf("expired session: %w", err)
}
return nil, nil
}
return s, nil
}
// DeleteByToken implements the [SessionStorage] interface for
// *DefaultSessionStorage.
func (ds *DefaultSessionStorage) DeleteByToken(ctx context.Context, t SessionToken) (err error) {
ds.mu.Lock()
defer ds.mu.Unlock()
// Don't wrap the error because it's informative enough as is.
return ds.deleteByToken(ctx, t)
}
// deleteByToken removes stored session by token. ds.mu is expected to be
// locked.
func (ds *DefaultSessionStorage) deleteByToken(ctx context.Context, t SessionToken) (err error) {
err = ds.remove(ctx, t)
if err != nil {
ds.logger.ErrorContext(ctx, "deleting session", slogutil.KeyError, err)
return err
}
delete(ds.sessions, t)
return nil
}
// remove deletes a web user session from the bbolt database.
func (ds *DefaultSessionStorage) remove(ctx context.Context, t SessionToken) (err error) {
tx, err := ds.db.Begin(true)
if err != nil {
return fmt.Errorf("starting transaction: %w", err)
}
needRollback := true
defer func() {
if needRollback {
err = errors.WithDeferred(err, tx.Rollback())
}
}()
bkt := tx.Bucket([]byte(bboltBucketSessions))
if bkt == nil {
return errors.Error("no bucket")
}
err = bkt.Delete(t[:])
if err != nil {
return fmt.Errorf("removing data: %w", err)
}
needRollback = false
err = tx.Commit()
if err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
ds.logger.DebugContext(ctx, "removed session from db")
return err
}
// Close implements the [SessionStorage] interface for *DefaultSessionStorage.
func (ds *DefaultSessionStorage) Close() (err error) {
err = ds.db.Close()
if err != nil {
return fmt.Errorf("closing db: %w", err)
}
return nil
}

View File

@@ -0,0 +1,162 @@
package aghuser_test
import (
"context"
"os"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghuser"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/faketime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// addSession is a helper function that saves and returns a session for a newly
// generated [aghuser.User] by login.
func addSession(
tb testing.TB,
ctx context.Context,
ds aghuser.SessionStorage,
login aghuser.Login,
) (s *aghuser.Session) {
tb.Helper()
s, err := ds.New(ctx, &aghuser.User{
ID: aghuser.MustNewUserID(),
Login: login,
})
require.NoError(tb, err)
require.NotNil(tb, s)
var got *aghuser.Session
got, err = ds.FindByToken(ctx, s.Token)
require.NoError(tb, err)
require.NotNil(tb, got)
assert.Equal(tb, login, got.UserLogin)
return s
}
func TestDefaultSessionStorage(t *testing.T) {
const (
userLoginFirst aghuser.Login = "user_one"
userLoginSecond aghuser.Login = "user_two"
)
var (
ctx = testutil.ContextWithTimeout(t, testTimeout)
logger = slogutil.NewDiscardLogger()
)
const (
sessionTTL = time.Minute
timeStep = time.Second
)
// Set up a mock clock to test expired sessions. Each call to [clock.Now]
// will return the [date] incremented by [timeStep].
date := time.Now()
clock := &faketime.Clock{
OnNow: func() (now time.Time) {
date = date.Add(timeStep)
return date
},
}
dbFile, err := os.CreateTemp(t.TempDir(), "sessions.db")
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, dbFile.Close)
userDB := aghuser.NewDefaultDB()
err = userDB.Create(ctx, &aghuser.User{
Login: userLoginFirst,
ID: aghuser.MustNewUserID(),
})
require.NoError(t, err)
err = userDB.Create(ctx, &aghuser.User{
Login: userLoginSecond,
ID: aghuser.MustNewUserID(),
})
require.NoError(t, err)
var (
ds *aghuser.DefaultSessionStorage
sessionFirst *aghuser.Session
sessionSecond *aghuser.Session
)
require.True(t, t.Run("prepare_session_storage", func(t *testing.T) {
ds, err = aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{
Clock: clock,
UserDB: userDB,
Logger: logger,
DBPath: dbFile.Name(),
SessionTTL: sessionTTL,
})
require.NoError(t, err)
sessionFirst = addSession(t, ctx, ds, userLoginFirst)
// Advance time to ensure the first session expires before creating the
// second session.
date = date.Add(time.Hour)
sessionSecond = addSession(t, ctx, ds, userLoginSecond)
err = ds.Close()
require.NoError(t, err)
}))
require.True(t, t.Run("load_sessions", func(t *testing.T) {
ds, err = aghuser.NewDefaultSessionStorage(ctx, &aghuser.DefaultSessionStorageConfig{
Clock: clock,
UserDB: userDB,
Logger: logger,
DBPath: dbFile.Name(),
SessionTTL: sessionTTL,
})
require.NoError(t, err)
var got *aghuser.Session
got, err = ds.FindByToken(ctx, sessionFirst.Token)
require.NoError(t, err)
assert.Nil(t, got)
got, err = ds.FindByToken(ctx, sessionSecond.Token)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, userLoginSecond, got.UserLogin)
err = ds.DeleteByToken(ctx, sessionSecond.Token)
require.NoError(t, err)
got, err = ds.FindByToken(ctx, sessionSecond.Token)
require.NoError(t, err)
assert.Nil(t, got)
}))
require.True(t, t.Run("expired_session", func(t *testing.T) {
testutil.CleanupAndRequireSuccess(t, ds.Close)
sessionFirst = addSession(t, ctx, ds, userLoginFirst)
date = date.Add(time.Hour)
var got *aghuser.Session
got, err = ds.FindByToken(ctx, sessionFirst.Token)
require.NoError(t, err)
assert.Nil(t, got)
}))
}

View File

@@ -32,13 +32,13 @@ func MustNewUserID() (uid UserID) {
// User represents a web user.
type User struct {
// ID is the unique identifier for the web user. It must not be empty.
ID UserID
// Password stores the password information for the web user. It must not
// be nil.
Password Password
// Login is the login name of the web user. It must not be empty.
Login Login
// Password stores the password information for the web user. It must not
// be nil.
Password Password
// ID is the unique identifier for the web user. It must not be empty.
ID UserID
}

View File

@@ -11,8 +11,34 @@ import (
"slices"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
)
// ClientID is a unique identifier for a persistent client used in
// DNS-over-HTTPS, DNS-over-TLS, and DNS-over-QUIC queries.
//
// TODO(s.chzhen): Use everywhere.
type ClientID string
// ValidateClientID returns an error if id is not a valid ClientID.
//
// TODO(s.chzhen): Consider implementing [validate.Interface] for ClientID.
func ValidateClientID(id string) (err error) {
err = netutil.ValidateHostnameLabel(id)
if err != nil {
// Replace the domain name label wrapper with our own.
return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
}
return nil
}
// isValidClientID returns false if id is not a valid ClientID.
func isValidClientID(id string) (ok bool) {
return netutil.IsValidHostnameLabel(id)
}
// Source represents the source from which the information about the client has
// been obtained.
type Source uint8

View File

@@ -35,7 +35,7 @@ type index struct {
nameToUID map[string]UID
// clientIDToUID maps ClientID to UID.
clientIDToUID map[string]UID
clientIDToUID map[ClientID]UID
// ipToUID maps IP address to UID.
ipToUID map[netip.Addr]UID
@@ -54,7 +54,7 @@ type index struct {
func newIndex() (ci *index) {
return &index{
nameToUID: map[string]UID{},
clientIDToUID: map[string]UID{},
clientIDToUID: map[ClientID]UID{},
ipToUID: map[netip.Addr]UID{},
subnetToUID: aghalg.NewSortedMap[netip.Prefix, UID](subnetCompare),
macToUID: map[macKey]UID{},
@@ -207,7 +207,7 @@ func (ci *index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr)
// find finds persistent client by string representation of the ClientID, IP
// address, or MAC.
func (ci *index) find(id string) (c *Persistent, ok bool) {
c, ok = ci.findByClientID(id)
c, ok = ci.findByClientID(ClientID(id))
if ok {
return c, true
}
@@ -230,7 +230,7 @@ func (ci *index) find(id string) (c *Persistent, ok bool) {
}
// findByClientID finds persistent client by ClientID.
func (ci *index) findByClientID(clientID string) (c *Persistent, ok bool) {
func (ci *index) findByClientID(clientID ClientID) (c *Persistent, ok bool) {
uid, ok := ci.clientIDToUID[clientID]
if ok {
return ci.uidToClient[uid], true
@@ -275,6 +275,26 @@ func (ci *index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
return nil, false
}
// findByCIDR searches for a persistent client with the provided subnet as an
// identifier. Note that this function looks for an exact match of subnets,
// rather than checking if one subnet contains another.
func (ci *index) findByCIDR(subnet netip.Prefix) (c *Persistent, ok bool) {
var uid UID
for pref, id := range ci.subnetToUID.Range {
if subnet == pref {
uid, ok = id, true
break
}
}
if ok {
return ci.uidToClient[uid], true
}
return nil, false
}
// findByMAC finds persistent client by MAC.
func (ci *index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
k := macToKey(mac)

View File

@@ -5,6 +5,7 @@ import (
"net/netip"
"testing"
"github.com/AdguardTeam/golibs/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -58,12 +59,12 @@ func TestClientIndex_Find(t *testing.T) {
clientWithMAC = &Persistent{
Name: "client_with_mac",
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
}
clientWithID = &Persistent{
Name: "client_with_id",
ClientIDs: []string{cliID},
ClientIDs: []ClientID{cliID},
}
clientLinkLocal = &Persistent{
@@ -141,10 +142,10 @@ func TestClientIndex_Clashes(t *testing.T) {
Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
}, {
Name: "client_with_mac",
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
}, {
Name: "client_with_id",
ClientIDs: []string{cliID},
ClientIDs: []ClientID{cliID},
}}
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) {
testCases := []struct {
want any
@@ -200,44 +190,44 @@ func TestMACToKey(t *testing.T) {
}{{
name: "column6",
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",
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",
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",
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",
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",
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",
in: "0000.5e00.5301",
want: [6]byte(mustParseMAC("0000.5e00.5301")),
want: [6]byte(errors.Must(net.ParseMAC("0000.5e00.5301"))),
}, {
name: "dot8",
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",
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 {
t.Run(tc.name, func(t *testing.T) {
mac := mustParseMAC(tc.in)
mac := errors.Must(net.ParseMAC(tc.in))
key := macToKey(mac)
assert.Equal(t, tc.want, key)
@@ -302,19 +292,19 @@ func TestIndex_FindByIPWithoutZone(t *testing.T) {
func TestClientIndex_RangeByName(t *testing.T) {
sortedClients := []*Persistent{{
Name: "clientA",
ClientIDs: []string{"A"},
ClientIDs: []ClientID{"A"},
}, {
Name: "clientB",
ClientIDs: []string{"B"},
ClientIDs: []ClientID{"B"},
}, {
Name: "clientC",
ClientIDs: []string{"C"},
ClientIDs: []ClientID{"C"},
}, {
Name: "clientD",
ClientIDs: []string{"D"},
ClientIDs: []ClientID{"D"},
}, {
Name: "clientE",
ClientIDs: []string{"E"},
ClientIDs: []ClientID{"E"},
}}
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/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/google/uuid"
)
@@ -71,7 +70,9 @@ type Persistent struct {
// Tags is a list of client tags that categorize the client.
Tags []string
// Upstreams is a list of custom upstream DNS servers for the client.
// 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
// 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
// (IP, subnet, MAC, or ClientID).
ClientIDs []string
ClientIDs []ClientID
// UID is the unique identifier of the persistent client.
UID UID
// UpstreamsCacheSize is the cache size for custom upstreams.
// UpstreamsCacheSize defines the size of the custom upstream cache.
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
// 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 {
case c.Name == "":
return errors.Error("empty name")
case c.IDsLen() == 0:
case c.idendifiersLen() == 0:
return errors.Error("id required")
case c.UID == UID{}:
return errors.Error("uid required")
@@ -237,28 +239,15 @@ func (c *Persistent) setID(id string) (err error) {
return err
}
c.ClientIDs = append(c.ClientIDs, strings.ToLower(id))
c.ClientIDs = append(c.ClientIDs, ClientID(strings.ToLower(id)))
return nil
}
// ValidateClientID returns an error if id is not a valid ClientID.
//
// TODO(s.chzhen): It's an exact copy of the [dnsforward.ValidateClientID] to
// avoid the import cycle. Remove it.
func ValidateClientID(id string) (err error) {
err = netutil.ValidateHostnameLabel(id)
if err != nil {
// Replace the domain name label wrapper with our own.
return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
}
return nil
}
// IDs returns a list of ClientIDs containing at least one element.
func (c *Persistent) IDs() (ids []string) {
ids = make([]string, 0, c.IDsLen())
// Identifiers returns a list of client identifiers containing at least one
// element.
func (c *Persistent) Identifiers() (ids []string) {
ids = make([]string, 0, c.idendifiersLen())
for _, ip := range c.IPs {
ids = append(ids, ip.String())
@@ -272,11 +261,15 @@ func (c *Persistent) IDs() (ids []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.
func (c *Persistent) IDsLen() (n int) {
// identifiersLen returns the number of client identifiers.
func (c *Persistent) idendifiersLen() (n int) {
return len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs)
}

View File

@@ -7,6 +7,7 @@ import (
"net"
"net/netip"
"slices"
"strings"
"sync"
"time"
@@ -18,6 +19,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
)
@@ -433,48 +435,186 @@ func (s *Storage) Add(ctx context.Context, p *Persistent) (err error) {
ctx,
"client added",
"name", p.Name,
"ids", p.IDs(),
"ids", p.Identifiers(),
"clients_count", s.index.size(),
)
return nil
}
// FindByName finds persistent client by name. And returns its shallow copy.
func (s *Storage) FindByName(name string) (p *Persistent, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
// FindParams represents the parameters for searching a client. At least one
// field must be non-empty.
type FindParams struct {
// ClientID is a unique identifier for the client used in DoH, DoT, and DoQ
// DNS queries.
ClientID ClientID
p, ok = s.index.findByName(name)
if ok {
return p.ShallowClone(), ok
}
// RemoteIP is the IP address used as a client search parameter.
RemoteIP netip.Addr
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
// address, or MAC. And returns its shallow copy.
// ErrBadIdentifier is returned by [FindParams.Set] when it cannot parse the
// provided client identifier.
const ErrBadIdentifier errors.Error = "bad client identifier"
// Set clears the stored search parameters and parses the string representation
// of the search parameter into typed parameter, storing it. In some cases, it
// may result in storing both an IP address and a MAC address because they might
// have identical string representations. It returns [ErrBadIdentifier] if id
// cannot be parsed.
//
// TODO(s.chzhen): Accept ClientIDData structure instead, which will contain
// the parsed IP address, if any.
func (s *Storage) Find(id string) (p *Persistent, ok bool) {
// TODO(s.chzhen): Add support for UID.
func (p *FindParams) Set(id string) (err error) {
*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()
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 {
return p.ShallowClone(), ok
return p, true
}
ip, err := netip.ParseAddr(id)
if err != nil {
return nil, false
}
foundMAC := s.dhcp.MACByIP(ip)
foundMAC := s.dhcp.MACByIP(addr)
if foundMAC != nil {
return s.FindByMAC(foundMAC)
return s.index.findByMAC(foundMAC)
}
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.
// Therefore, the result of this method is indeterminate.
//
// TODO(s.chzhen): Consider accepting [FindParams].
func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -498,7 +640,7 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil {
return s.FindByMAC(foundMAC)
return s.index.findByMAC(foundMAC)
}
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
}
// FindByMAC finds persistent client by MAC and returns its shallow copy. s.mu
// is expected to be locked.
func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) {
p, ok = s.index.findByMAC(mac)
if ok {
return p.ShallowClone(), ok
}
return nil, false
}
// RemoveByName removes persistent client information. ok is false if no such
// client exists by that name.
func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) {
@@ -648,9 +779,9 @@ func (s *Storage) CustomUpstreamConfig(
s.mu.Lock()
defer s.mu.Unlock()
c, ok := s.index.findByClientID(id)
c, ok := s.index.findByClientID(ClientID(id))
if !ok {
c, ok = s.index.findByIP(addr)
c, ok = s.findByIP(addr)
}
if !ok {
@@ -682,7 +813,7 @@ func (s *Storage) ClearUpstreamCache() {
// ClientID or client IP address, and applies it to the filtering settings.
// setts must not be nil.
func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filtering.Settings) {
c, ok := s.index.findByClientID(id)
c, ok := s.index.findByClientID(ClientID(id))
if !ok {
c, ok = s.index.findByIP(addr)
}
@@ -690,7 +821,7 @@ func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filter
if !ok {
foundMAC := s.dhcp.MACByIP(addr)
if foundMAC != nil {
c, ok = s.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/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
@@ -350,15 +351,15 @@ func TestClientsDHCP(t *testing.T) {
cliName1 = "one.dhcp"
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"
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"
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"
otherARPCliName = "other.arp"
@@ -519,7 +520,11 @@ func TestClientsDHCP(t *testing.T) {
})
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)
assert.Equal(t, prsCliName, prsCli.Name)
@@ -663,17 +668,6 @@ func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
return s
}
// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
// error.
func mustParseMAC(s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
if err != nil {
panic(err)
}
return mac
}
func TestStorage_Add(t *testing.T) {
const (
existingName = "existing_name"
@@ -693,7 +687,7 @@ func TestStorage_Add(t *testing.T) {
Name: existingName,
IPs: []netip.Addr{existingIP},
Subnets: []netip.Prefix{existingSubnet},
ClientIDs: []string{existingClientID},
ClientIDs: []client.ClientID{existingClientID},
UID: existingClientUID,
}
@@ -761,7 +755,7 @@ func TestStorage_Add(t *testing.T) {
name: "duplicate_client_id",
cli: &client.Persistent{
Name: "duplicate_client_id",
ClientIDs: []string{existingClientID},
ClientIDs: []client.ClientID{existingClientID},
UID: client.MustNewUID(),
},
wantErrMsg: `adding client: another client "existing_name" ` +
@@ -898,12 +892,12 @@ func TestStorage_Find(t *testing.T) {
clientWithMAC = &client.Persistent{
Name: "client_with_mac",
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
}
clientWithID = &client.Persistent{
Name: "client_with_id",
ClientIDs: []string{cliID},
ClientIDs: []client.ClientID{cliID},
}
clientLinkLocal = &client.Persistent{
@@ -950,7 +944,11 @@ func TestStorage_Find(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
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)
assert.Equal(t, tc.want, c)
@@ -959,7 +957,11 @@ func TestStorage_Find(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)
})
}
@@ -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) {
const (
clientName = "client_name"
@@ -1162,7 +1043,7 @@ func TestStorage_Update(t *testing.T) {
Name: obstructingName,
IPs: []netip.Addr{obstructingIP},
Subnets: []netip.Prefix{obstructingSubnet},
ClientIDs: []string{obstructingClientID},
ClientIDs: []client.ClientID{obstructingClientID},
}
clientToUpdate := &client.Persistent{
@@ -1211,7 +1092,7 @@ func TestStorage_Update(t *testing.T) {
name: "duplicate_client_id",
cli: &client.Persistent{
Name: "duplicate_client_id",
ClientIDs: []string{obstructingClientID},
ClientIDs: []client.ClientID{obstructingClientID},
UID: client.MustNewUID(),
},
wantErrMsg: `updating client: another client "obstructing_name" ` +
@@ -1238,19 +1119,19 @@ func TestStorage_Update(t *testing.T) {
func TestStorage_RangeByName(t *testing.T) {
sortedClients := []*client.Persistent{{
Name: "clientA",
ClientIDs: []string{"A"},
ClientIDs: []client.ClientID{"A"},
}, {
Name: "clientB",
ClientIDs: []string{"B"},
ClientIDs: []client.ClientID{"B"},
}, {
Name: "clientC",
ClientIDs: []string{"C"},
ClientIDs: []client.ClientID{"C"},
}, {
Name: "clientD",
ClientIDs: []string{"D"},
ClientIDs: []client.ClientID{"D"},
}, {
Name: "clientE",
ClientIDs: []string{"E"},
ClientIDs: []client.ClientID{"E"},
}}
testCases := []struct {
@@ -1288,29 +1169,20 @@ func TestStorage_RangeByName(t *testing.T) {
func TestStorage_CustomUpstreamConfig(t *testing.T) {
const (
existingName = "existing_name"
existingClientID = "existing_client_id"
existingClientID = "existing_client_id"
nonExistingClientID = "non_existing_client_id"
)
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")
dhcpCliIP = netip.MustParseAddr("192.0.2.2")
dhcpCliMAC = errors.Must(net.ParseMAC("02:00:00:00:00:00"))
testUpstreamTimeout = time.Second
)
existingClient := &client.Persistent{
Name: existingName,
IPs: []netip.Addr{existingIP},
ClientIDs: []string{existingClientID},
UID: existingClientUID,
Upstreams: []string{"192.0.2.0"},
}
date := time.Now()
clock := &faketime.Clock{
OnNow: func() (now time.Time) {
@@ -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{
UpstreamTimeout: testUpstreamTimeout,
})
@@ -1329,8 +1224,21 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
return s.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := s.Add(ctx, existingClient)
err = s.Add(ctx, &client.Persistent{
Name: "client_first",
IPs: []netip.Addr{existingIP},
ClientIDs: []client.ClientID{existingClientID},
UID: client.MustNewUID(),
Upstreams: []string{"192.0.2.0"},
})
require.NoError(t, err)
err = s.Add(ctx, &client.Persistent{
Name: "client_second",
MACs: []net.HardwareAddr{dhcpCliMAC},
UID: client.MustNewUID(),
Upstreams: []string{"192.0.2.0"},
})
require.NoError(t, err)
testCases := []struct {
@@ -1348,6 +1256,11 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
cliID: "",
cliAddr: existingIP,
wantNilConf: assert.NotNil,
}, {
name: "client_dhcp",
cliID: "",
cliAddr: dhcpCliIP,
wantNilConf: assert.NotNil,
}, {
name: "non_existing_client_id",
cliID: nonExistingClientID,
@@ -1380,4 +1293,193 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
assert.NotEqual(t, conf, updConf)
})
t.Run("same_custom_config", func(t *testing.T) {
firstConf := s.CustomUpstreamConfig(existingClientID, existingIP)
require.NotNil(t, firstConf)
secondConf := s.CustomUpstreamConfig(existingClientID, existingIP)
require.NotNil(t, secondConf)
assert.Same(t, firstConf, secondConf)
})
}
func BenchmarkFindParams_Set(b *testing.B) {
const (
testIPStr = "192.0.2.1"
testCIDRStr = "192.0.2.0/24"
testMACStr = "02:00:00:00:00:00"
testClientID = "clientid"
)
benchCases := []struct {
wantErr error
params *client.FindParams
name string
id string
}{{
wantErr: nil,
params: &client.FindParams{
ClientID: testClientID,
},
name: "client_id",
id: testClientID,
}, {
wantErr: nil,
params: &client.FindParams{
RemoteIP: netip.MustParseAddr(testIPStr),
},
name: "ip_address",
id: testIPStr,
}, {
wantErr: nil,
params: &client.FindParams{
Subnet: netip.MustParsePrefix(testCIDRStr),
},
name: "subnet",
id: testCIDRStr,
}, {
wantErr: nil,
params: &client.FindParams{
MAC: errors.Must(net.ParseMAC(testMACStr)),
},
name: "mac_address",
id: testMACStr,
}, {
wantErr: client.ErrBadIdentifier,
params: &client.FindParams{},
name: "bad_id",
id: "!@#$%^&*()_+",
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
params := &client.FindParams{}
var err error
b.ReportAllocs()
for b.Loop() {
err = params.Set(bc.id)
}
assert.ErrorIs(b, err, bc.wantErr)
assert.Equal(b, bc.params, params)
})
}
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardHome/internal/client
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkFindParams_Set/client_id-8 49463488 24.27 ns/op 0 B/op 0 allocs/op
// BenchmarkFindParams_Set/ip_address-8 18740977 62.22 ns/op 0 B/op 0 allocs/op
// BenchmarkFindParams_Set/subnet-8 10848192 110.0 ns/op 0 B/op 0 allocs/op
// BenchmarkFindParams_Set/mac_address-8 8148494 133.2 ns/op 8 B/op 1 allocs/op
// BenchmarkFindParams_Set/bad_id-8 73894278 16.29 ns/op 0 B/op 0 allocs/op
}
func BenchmarkStorage_Find(b *testing.B) {
const (
cliID = "cid"
cliMAC = "02:00:00:00:00:00"
)
const (
cliNameWithID = "client_with_id"
cliNameWithIP = "client_with_ip"
cliNameWithCIDR = "client_with_cidr"
cliNameWithMAC = "client_with_mac"
)
var (
cliIP = netip.MustParseAddr("192.0.2.1")
cliCIDR = netip.MustParsePrefix("192.0.2.0/24")
)
var (
clientWithID = &client.Persistent{
Name: cliNameWithID,
ClientIDs: []client.ClientID{cliID},
}
clientWithIP = &client.Persistent{
Name: cliNameWithIP,
IPs: []netip.Addr{cliIP},
}
clientWithCIDR = &client.Persistent{
Name: cliNameWithCIDR,
Subnets: []netip.Prefix{cliCIDR},
}
clientWithMAC = &client.Persistent{
Name: cliNameWithMAC,
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
}
)
clients := []*client.Persistent{
clientWithID,
clientWithIP,
clientWithCIDR,
clientWithMAC,
}
s := newStorage(b, clients)
benchCases := []struct {
params *client.FindParams
name string
wantName string
}{{
params: &client.FindParams{
ClientID: cliID,
},
name: "client_id",
wantName: cliNameWithID,
}, {
params: &client.FindParams{
RemoteIP: cliIP,
},
name: "ip_address",
wantName: cliNameWithIP,
}, {
params: &client.FindParams{
Subnet: cliCIDR,
},
name: "subnet",
wantName: cliNameWithCIDR,
}, {
params: &client.FindParams{
MAC: errors.Must(net.ParseMAC(cliMAC)),
},
name: "mac_address",
wantName: cliNameWithMAC,
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
var p *client.Persistent
var ok bool
b.ReportAllocs()
for b.Loop() {
p, ok = s.Find(bc.params)
}
assert.True(b, ok)
assert.NotNil(b, p)
assert.Equal(b, bc.wantName, p.Name)
})
}
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardHome/internal/client
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkStorage_Find/client_id-8 7070107 154.4 ns/op 240 B/op 2 allocs/op
// BenchmarkStorage_Find/ip_address-8 6831823 168.6 ns/op 248 B/op 2 allocs/op
// BenchmarkStorage_Find/subnet-8 7209050 167.5 ns/op 256 B/op 2 allocs/op
// BenchmarkStorage_Find/mac_address-8 5776131 199.7 ns/op 256 B/op 3 allocs/op
}

View File

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

View File

@@ -1,13 +1,11 @@
package dhcpsvc_test
import (
"net"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/stretchr/testify/require"
)
// testLocalTLD is a common local TLD for tests.
@@ -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 (
"io/fs"
"net"
"net/netip"
"os"
"path"
@@ -11,6 +12,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -176,9 +178,9 @@ func TestDHCPServer_AddLease(t *testing.T) {
newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
newMAC = mustParseMAC(t, "06:05:04:03:02:01")
ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07")
existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
newMAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
ipv6MAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
)
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")
ip4 = netip.MustParseAddr("172.16.0.4")
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
mac3 = mustParseMAC(t, "02:03:04:05:06:07")
mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
mac3 = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
)
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")
ip4 = netip.MustParseAddr("2001:db8::3")
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
mac3 = mustParseMAC(t, "06:05:04:03:02:02")
mac1 = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
mac2 = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
mac3 = errors.Must(net.ParseMAC("06:05:04:03:02:02"))
)
testCases := []struct {
@@ -452,9 +454,9 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
newMAC = mustParseMAC(t, "02:03:04:05:06:07")
ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01")
existMAC = errors.Must(net.ParseMAC("01:02:03:04:05:06"))
newMAC = errors.Must(net.ParseMAC("02:03:04:05:06:07"))
ipv6MAC = errors.Must(net.ParseMAC("06:05:04:03:02:01"))
)
testCases := []struct {
@@ -559,13 +561,13 @@ func TestServer_Leases(t *testing.T) {
Expiry: expiry,
IP: netip.MustParseAddr("192.168.0.3"),
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,
}, {
Expiry: time.Time{},
IP: netip.MustParseAddr("192.168.0.4"),
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,
}}
assert.ElementsMatch(t, wantLeases, srv.Leases())

View File

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

View File

@@ -7,26 +7,13 @@ import (
"path"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/quic-go/quic-go"
)
// ValidateClientID returns an error if id is not a valid ClientID.
//
// Keep in sync with [client.ValidateClientID].
func ValidateClientID(id string) (err error) {
err = netutil.ValidateHostnameLabel(id)
if err != nil {
// Replace the domain name label wrapper with our own.
return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
}
return nil
}
// clientIDFromClientServerName extracts and validates a ClientID. hostSrvName
// is the server name of the host. cliSrvName is the server name as sent by the
// client. When strict is true, and client and host server name don't match,
@@ -53,7 +40,7 @@ func clientIDFromClientServerName(
}
clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1]
err = ValidateClientID(clientID)
err = client.ValidateClientID(clientID)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
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)
}
err = ValidateClientID(clientID)
err = client.ValidateClientID(clientID)
if err != nil {
return "", fmt.Errorf("clientid check: %w", err)
}

View File

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

View File

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

View File

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

View File

@@ -317,13 +317,7 @@ func newDNSTLSConfig(
return &dnsforward.TLSConfig{}, nil
}
cert, err := tls.X509KeyPair(conf.CertificateChainData, conf.PrivateKeyData)
if err != nil {
return nil, fmt.Errorf("parsing tls key pair: %w", err)
}
dnsConf = &dnsforward.TLSConfig{
Cert: &cert,
ServerName: conf.ServerName,
StrictSNICheck: conf.StrictSNICheck,
}
@@ -340,6 +334,21 @@ func newDNSTLSConfig(
dnsConf.QUICListenAddrs = ipsToUDPAddrs(addrs, conf.PortDNSOverQUIC)
}
cert, err := tls.X509KeyPair(conf.CertificateChainData, conf.PrivateKeyData)
if err != nil {
const format = "parsing tls key pair: %w"
if conf.AllowUnencryptedDoH {
// TODO(s.chzhen): Use [slog.Logger].
log.Info("warning: %s: %s", format, err)
return dnsConf, nil
}
return nil, fmt.Errorf(format, err)
}
dnsConf.Cert = &cert
return dnsConf, nil
}

View File

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

View File

@@ -204,6 +204,8 @@ func assertCertSerialNumber(tb testing.TB, conf *tlsConfigSettings, wantSN int64
func TestTLSManager_Reload(t *testing.T) {
storeGlobals(t)
config.DNS.Port = 0
var (
logger = slogutil.NewDiscardLogger()
ctx = testutil.ContextWithTimeout(t, testTimeout)
@@ -260,6 +262,10 @@ func TestTLSManager_Reload(t *testing.T) {
m.reload(ctx)
// The [tlsManager.reload] method will start the DNS server and it should be
// stopped after the test ends.
testutil.CleanupAndRequireSuccess(t, globalContext.dnsServer.Stop)
conf = m.config()
assertCertSerialNumber(t, conf, snAfter)
}

View File

@@ -980,7 +980,8 @@
- 'clients'
'operationId': 'clientsSearch'
'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':
'content':
'application/json':

View File

@@ -199,6 +199,7 @@ run_linter gocognit --over='10' \
./internal/aghhttp/ \
./internal/aghrenameio/ \
./internal/aghtest/ \
./internal/aghuser/ \
./internal/arpdb/ \
./internal/client/ \
./internal/configmigrate/ \
@@ -250,6 +251,7 @@ run_linter fieldalignment \
./internal/aghrenameio/ \
./internal/aghtest/ \
./internal/aghtls/ \
./internal/aghuser/ \
./internal/arpdb/ \
./internal/client/ \
./internal/configmigrate/ \
@@ -280,6 +282,7 @@ run_linter gosec --exclude G115 --quiet \
./internal/aghos/ \
./internal/aghrenameio/ \
./internal/aghtest/ \
./internal/aghuser/ \
./internal/arpdb/ \
./internal/client/ \
./internal/configmigrate/ \