Compare commits

..

1 Commits

Author SHA1 Message Date
Stanislav Chzhen
53cb84efc0 all: session storage usage 2025-04-22 15:42:12 +03:00
55 changed files with 817 additions and 1520 deletions

View File

@@ -9,45 +9,26 @@ The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/
<!-- <!--
## [v0.108.0] TBA ## [v0.108.0] TBA
## [v0.107.62] - 2025-04-30 (APPROX.) ## [v0.107.61] - 2025-04-22 (APPROX.)
See also the [v0.107.62 GitHub milestone][ms-v0.107.62]. See also the [v0.107.61 GitHub milestone][ms-v0.107.61].
[ms-v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/milestone/97?closed=1 [ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Fixed
- Command line option `--update` when the `dns.serve_plain_dns` configuration property was disabled ([7801]).
- DNS cache not working for custom upstream configurations.
- Validation process for the DNS-over-TLS, DNS-over-QUIC, and HTTPS ports on the *Encryption Settings* page.
[#7801]: https://github.com/AdguardTeam/AdGuardHome/issues/7801
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.61] - 2025-04-22
See also the [v0.107.61 GitHub milestone][ms-v0.107.61].
### Security ### Security
- Any simultaneous requests that are considered duplicates will now only result in a single request to upstreams, reducing the chance of a cache poisoning attack succeeding. This is controlled by the new configuration object `pending_requests`, which has a single `enabled` property, set to `true` by default. - Any simultaneous requests that are considered duplicates will now only result in a single request to upstreams, reducing the chance of a cache poisoning attack succeeding. This is controlled by the new configuration object `pending_requests`, which has a single `enabled` property, set to `true` by default.
**NOTE:** We thank [Xiang Li][mr-xiang-li] for reporting this security issue. It's strongly recommended to leave it enabled, otherwise AdGuard Home will be vulnerable to untrusted clients. **NOTE:** We thank [Xiang Li][mr-xiang-li] for reporting this security issue. It's strongly recommended to leave it enabled, otherwise AdGuard Home will be vulnerable to untrusted clients.
### Fixed
- Searching for persistent clients using an exact match for CIDR in the `POST /clients/search HTTP API`.
[mr-xiang-li]: https://lixiang521.com/ [mr-xiang-li]: https://lixiang521.com/
[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.60] - 2025-04-14 ## [v0.107.60] - 2025-04-14
@@ -90,6 +71,10 @@ See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
See also the [v0.107.59 GitHub milestone][ms-v0.107.59]. See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
### Fixed
- Validation process for the DNS-over-TLS, DNS-over-QUIC, and HTTPS ports on the *Encryption Settings* page.
- Rules with the `client` modifier not working ([#7708]). - Rules with the `client` modifier not working ([#7708]).
- The search form not working in the query log ([#7704]). - The search form not working in the query log ([#7704]).
@@ -3130,12 +3115,11 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
[ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1 [ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.62...HEAD
[v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...v0.107.62
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...HEAD [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.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...HEAD
[v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60 [v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60
[v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59 [v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59
[v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58 [v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -656,7 +656,7 @@
"blocklist": "Zoznam blokovaní", "blocklist": "Zoznam blokovaní",
"milliseconds_abbreviation": "ms", "milliseconds_abbreviation": "ms",
"cache_size": "Veľkosť cache", "cache_size": "Veľkosť cache",
"cache_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete vypnúť ukladanie do vyrovnávacej pamäte, nastavte hodnotu 0.", "cache_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete zakázať ukladanie do vyrovnávacej pamäte, ponechajte pole prázdne.",
"cache_ttl_min_override": "Prepísať minimálne TTL", "cache_ttl_min_override": "Prepísať minimálne TTL",
"cache_ttl_max_override": "Prepísať maximálne TTL", "cache_ttl_max_override": "Prepísať maximálne TTL",
"enter_cache_size": "Zadať veľkosť cache (v bajtoch)", "enter_cache_size": "Zadať veľkosť cache (v bajtoch)",

View File

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

View File

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

View File

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

12
go.mod
View File

@@ -3,8 +3,8 @@ module github.com/AdguardTeam/AdGuardHome
go 1.24.2 go 1.24.2
require ( require (
github.com/AdguardTeam/dnsproxy v0.75.5 github.com/AdguardTeam/dnsproxy v0.75.3
github.com/AdguardTeam/golibs v0.32.9 github.com/AdguardTeam/golibs v0.32.8
github.com/AdguardTeam/urlfilter v0.20.0 github.com/AdguardTeam/urlfilter v0.20.0
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.4.0 github.com/ameshkov/dnscrypt/v2 v2.4.0
@@ -36,7 +36,7 @@ require (
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.37.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/net v0.39.0 golang.org/x/net v0.39.0
golang.org/x/sys v0.33.0 golang.org/x/sys v0.32.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.1 howett.net/plist v1.0.1
@@ -61,7 +61,7 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golangci/misspell v0.6.0 // indirect github.com/golangci/misspell v0.6.0 // indirect
github.com/google/generative-ai-go v0.19.0 // indirect github.com/google/generative-ai-go v0.19.0 // indirect
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect
@@ -89,11 +89,11 @@ require (
go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect go.uber.org/mock v0.5.1 // indirect
golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.24.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.14.0 // indirect golang.org/x/sync v0.13.0 // indirect
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 // indirect golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 // indirect
golang.org/x/term v0.31.0 // indirect golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.24.0 // indirect

24
go.sum
View File

@@ -10,10 +10,10 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
github.com/AdguardTeam/dnsproxy v0.75.5 h1:/P7+Ku4bjl+sVC/FW3PbT7pabgCjKTcrAOHqsZe2e60= github.com/AdguardTeam/dnsproxy v0.75.3 h1:pxlMNO+cP1A3px40PY/old6SAE82pkdLPUA2P3KY8u0=
github.com/AdguardTeam/dnsproxy v0.75.5/go.mod h1:fdwtHhrDkTueDagDCasYKZbXdppkkBXW7RGPBNH+pis= github.com/AdguardTeam/dnsproxy v0.75.3/go.mod h1:50OyTHao+uQzUJiXay08hgfvWQ3o2Q2WV99W8u8ypDE=
github.com/AdguardTeam/golibs v0.32.9 h1:/6luT0aMOn05/s9eh1yA4lbcHgl0d1iEEvEBbIMMUk0= github.com/AdguardTeam/golibs v0.32.8 h1:O3mc3kYcPkW3kbmd+gqzFNgUka13a+iBgFLThwOYSQE=
github.com/AdguardTeam/golibs v0.32.9/go.mod h1:McV1QFFlKLElKa306V4OL/T2kr7564PhsayfvTWYBVs= github.com/AdguardTeam/golibs v0.32.8/go.mod h1:McV1QFFlKLElKa306V4OL/T2kr7564PhsayfvTWYBVs=
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs= github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=
github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk= github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
@@ -72,8 +72,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
@@ -199,8 +199,8 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
@@ -227,8 +227,8 @@ golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -241,8 +241,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 h1:RXY2+rSHXvxO2Y+gKrPjYVaEoGOqh3VEXFhnWAt1Irg= golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 h1:RXY2+rSHXvxO2Y+gKrPjYVaEoGOqh3VEXFhnWAt1Irg=
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3/go.mod h1:RoaXAWDwS90j6FxVKwJdBV+0HCU+llrKUGgJaxiKl6M= golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3/go.mod h1:RoaXAWDwS90j6FxVKwJdBV+0HCU+llrKUGgJaxiKl6M=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import (
"net/netip" "net/netip"
"testing" "testing"
"github.com/AdguardTeam/golibs/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -59,12 +58,12 @@ func TestClientIndex_Find(t *testing.T) {
clientWithMAC = &Persistent{ clientWithMAC = &Persistent{
Name: "client_with_mac", Name: "client_with_mac",
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))}, MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
} }
clientWithID = &Persistent{ clientWithID = &Persistent{
Name: "client_with_id", Name: "client_with_id",
ClientIDs: []ClientID{cliID}, ClientIDs: []string{cliID},
} }
clientLinkLocal = &Persistent{ clientLinkLocal = &Persistent{
@@ -142,10 +141,10 @@ func TestClientIndex_Clashes(t *testing.T) {
Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)}, Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
}, { }, {
Name: "client_with_mac", Name: "client_with_mac",
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))}, MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
}, { }, {
Name: "client_with_id", Name: "client_with_id",
ClientIDs: []ClientID{cliID}, ClientIDs: []string{cliID},
}} }}
ci := newIDIndex(clients) ci := newIDIndex(clients)
@@ -182,6 +181,17 @@ func TestClientIndex_Clashes(t *testing.T) {
} }
} }
// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
// error.
func mustParseMAC(s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
if err != nil {
panic(err)
}
return mac
}
func TestMACToKey(t *testing.T) { func TestMACToKey(t *testing.T) {
testCases := []struct { testCases := []struct {
want any want any
@@ -190,44 +200,44 @@ func TestMACToKey(t *testing.T) {
}{{ }{{
name: "column6", name: "column6",
in: "00:00:5e:00:53:01", in: "00:00:5e:00:53:01",
want: [6]byte(errors.Must(net.ParseMAC("00:00:5e:00:53:01"))), want: [6]byte(mustParseMAC("00:00:5e:00:53:01")),
}, { }, {
name: "column8", name: "column8",
in: "02:00:5e:10:00:00:00:01", in: "02:00:5e:10:00:00:00:01",
want: [8]byte(errors.Must(net.ParseMAC("02:00:5e:10:00:00:00:01"))), want: [8]byte(mustParseMAC("02:00:5e:10:00:00:00:01")),
}, { }, {
name: "column20", name: "column20",
in: "00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01", in: "00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01",
want: [20]byte(errors.Must(net.ParseMAC("00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01"))), want: [20]byte(mustParseMAC("00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01")),
}, { }, {
name: "hyphen6", name: "hyphen6",
in: "00-00-5e-00-53-01", in: "00-00-5e-00-53-01",
want: [6]byte(errors.Must(net.ParseMAC("00-00-5e-00-53-01"))), want: [6]byte(mustParseMAC("00-00-5e-00-53-01")),
}, { }, {
name: "hyphen8", name: "hyphen8",
in: "02-00-5e-10-00-00-00-01", in: "02-00-5e-10-00-00-00-01",
want: [8]byte(errors.Must(net.ParseMAC("02-00-5e-10-00-00-00-01"))), want: [8]byte(mustParseMAC("02-00-5e-10-00-00-00-01")),
}, { }, {
name: "hyphen20", name: "hyphen20",
in: "00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01", in: "00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01",
want: [20]byte(errors.Must(net.ParseMAC("00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01"))), want: [20]byte(mustParseMAC("00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01")),
}, { }, {
name: "dot6", name: "dot6",
in: "0000.5e00.5301", in: "0000.5e00.5301",
want: [6]byte(errors.Must(net.ParseMAC("0000.5e00.5301"))), want: [6]byte(mustParseMAC("0000.5e00.5301")),
}, { }, {
name: "dot8", name: "dot8",
in: "0200.5e10.0000.0001", in: "0200.5e10.0000.0001",
want: [8]byte(errors.Must(net.ParseMAC("0200.5e10.0000.0001"))), want: [8]byte(mustParseMAC("0200.5e10.0000.0001")),
}, { }, {
name: "dot20", name: "dot20",
in: "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001", in: "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001",
want: [20]byte(errors.Must(net.ParseMAC("0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001"))), want: [20]byte(mustParseMAC("0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001")),
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
mac := errors.Must(net.ParseMAC(tc.in)) mac := mustParseMAC(tc.in)
key := macToKey(mac) key := macToKey(mac)
assert.Equal(t, tc.want, key) assert.Equal(t, tc.want, key)
@@ -292,19 +302,19 @@ func TestIndex_FindByIPWithoutZone(t *testing.T) {
func TestClientIndex_RangeByName(t *testing.T) { func TestClientIndex_RangeByName(t *testing.T) {
sortedClients := []*Persistent{{ sortedClients := []*Persistent{{
Name: "clientA", Name: "clientA",
ClientIDs: []ClientID{"A"}, ClientIDs: []string{"A"},
}, { }, {
Name: "clientB", Name: "clientB",
ClientIDs: []ClientID{"B"}, ClientIDs: []string{"B"},
}, { }, {
Name: "clientC", Name: "clientC",
ClientIDs: []ClientID{"C"}, ClientIDs: []string{"C"},
}, { }, {
Name: "clientD", Name: "clientD",
ClientIDs: []ClientID{"D"}, ClientIDs: []string{"D"},
}, { }, {
Name: "clientE", Name: "clientE",
ClientIDs: []ClientID{"E"}, ClientIDs: []string{"E"},
}} }}
testCases := []struct { testCases := []struct {
@@ -339,115 +349,3 @@ func TestClientIndex_RangeByName(t *testing.T) {
}) })
} }
} }
func TestIndex_FindByName(t *testing.T) {
const (
clientExistingName = "client_existing"
clientAnotherExistingName = "client_another_existing"
nonExistingClientName = "client_non_existing"
)
var (
clientExisting = &Persistent{
Name: clientExistingName,
IPs: []netip.Addr{netip.MustParseAddr("192.0.2.1")},
}
clientAnotherExisting = &Persistent{
Name: clientAnotherExistingName,
IPs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
}
)
clients := []*Persistent{
clientExisting,
clientAnotherExisting,
}
ci := newIDIndex(clients)
testCases := []struct {
want *Persistent
found assert.BoolAssertionFunc
name string
clientName string
}{{
want: clientExisting,
found: assert.True,
name: "existing",
clientName: clientExistingName,
}, {
want: clientAnotherExisting,
found: assert.True,
name: "another_existing",
clientName: clientAnotherExistingName,
}, {
want: nil,
found: assert.False,
name: "non_existing",
clientName: nonExistingClientName,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := ci.findByName(tc.clientName)
assert.Equal(t, tc.want, c)
tc.found(t, ok)
})
}
}
func TestIndex_FindByMAC(t *testing.T) {
var (
cliMAC = errors.Must(net.ParseMAC("11:11:11:11:11:11"))
cliAnotherMAC = errors.Must(net.ParseMAC("22:22:22:22:22:22"))
nonExistingClientMAC = errors.Must(net.ParseMAC("33:33:33:33:33:33"))
)
var (
clientExisting = &Persistent{
Name: "client",
MACs: []net.HardwareAddr{cliMAC},
}
clientAnotherExisting = &Persistent{
Name: "another_client",
MACs: []net.HardwareAddr{cliAnotherMAC},
}
)
clients := []*Persistent{
clientExisting,
clientAnotherExisting,
}
ci := newIDIndex(clients)
testCases := []struct {
want *Persistent
found assert.BoolAssertionFunc
name string
clientMAC net.HardwareAddr
}{{
want: clientExisting,
found: assert.True,
name: "existing",
clientMAC: cliMAC,
}, {
want: clientAnotherExisting,
found: assert.True,
name: "another_existing",
clientMAC: cliAnotherMAC,
}, {
want: nil,
found: assert.False,
name: "non_existing",
clientMAC: nonExistingClientMAC,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := ci.findByMAC(tc.clientMAC)
assert.Equal(t, tc.want, c)
tc.found(t, ok)
})
}
}

View File

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

View File

@@ -18,7 +18,6 @@ import (
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile" "github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
@@ -434,138 +433,48 @@ func (s *Storage) Add(ctx context.Context, p *Persistent) (err error) {
ctx, ctx,
"client added", "client added",
"name", p.Name, "name", p.Name,
"ids", p.Identifiers(), "ids", p.IDs(),
"clients_count", s.index.size(), "clients_count", s.index.size(),
) )
return nil return nil
} }
// FindParams represents the parameters for searching a client. At least one // FindByName finds persistent client by name. And returns its shallow copy.
// field must be non-empty. func (s *Storage) FindByName(name string) (p *Persistent, ok bool) {
type FindParams struct {
// ClientID is a unique identifier for the client used in DoH, DoT, and DoQ
// DNS queries.
ClientID ClientID
// RemoteIP is the IP address used as a client search parameter.
RemoteIP netip.Addr
// Subnet is the CIDR used as a client search parameter.
Subnet netip.Prefix
// MAC is the physical hardware address used as a client search parameter.
MAC net.HardwareAddr
// UID is the unique ID of persistent client used as a search parameter.
//
// TODO(s.chzhen): Use this.
UID UID
}
// ErrBadIdentifier is returned by [FindParams.Set] when it cannot parse the
// provided client identifier.
const ErrBadIdentifier errors.Error = "bad client identifier"
// Set clears the stored search parameters and parses the string representation
// of the search parameter into typed parameter, storing it. In some cases, it
// may result in storing both an IP address and a MAC address because they might
// have identical string representations. It returns [ErrBadIdentifier] if id
// cannot be parsed.
//
// TODO(s.chzhen): Add support for UID.
func (p *FindParams) Set(id string) (err error) {
*p = FindParams{}
isFound := false
if netutil.IsValidIPString(id) {
// It is safe to use [netip.MustParseAddr] because it has already been
// validated that id contains the string representation of the IP
// address.
p.RemoteIP = netip.MustParseAddr(id)
// Even if id can be parsed as an IP address, it may be a MAC address.
// So do not return prematurely, continue parsing.
isFound = true
}
if netutil.IsValidMACString(id) {
p.MAC, err = net.ParseMAC(id)
if err != nil {
panic(fmt.Errorf("parsing mac from %q: %w", id, err))
}
isFound = true
}
if isFound {
return nil
}
if netutil.IsValidIPPrefixString(id) {
// It is safe to use [netip.MustParsePrefix] because it has already been
// validated that id contains the string representation of IP prefix.
p.Subnet = netip.MustParsePrefix(id)
return nil
}
if !isValidClientID(id) {
return ErrBadIdentifier
}
p.ClientID = ClientID(id)
return nil
}
// Find represents the parameters for searching a client. params must not be
// nil and must have at least one non-empty field.
func (s *Storage) Find(params *FindParams) (p *Persistent, ok bool) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
isClientID := params.ClientID != "" p, ok = s.index.findByName(name)
isRemoteIP := params.RemoteIP != (netip.Addr{}) if ok {
isSubnet := params.Subnet != (netip.Prefix{}) return p.ShallowClone(), ok
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
}
} }
return nil, false
} }
// findByIP finds persistent client by IP address. s.mu is expected to be // Find finds persistent client by string representation of the ClientID, IP
// locked. // address, or MAC. And returns its shallow copy.
func (s *Storage) findByIP(addr netip.Addr) (p *Persistent, ok bool) { //
p, ok = s.index.findByIP(addr) // TODO(s.chzhen): Accept ClientIDData structure instead, which will contain
// the parsed IP address, if any.
func (s *Storage) Find(id string) (p *Persistent, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok = s.index.find(id)
if ok { if ok {
return p, true return p.ShallowClone(), ok
} }
foundMAC := s.dhcp.MACByIP(addr) ip, err := netip.ParseAddr(id)
if err != nil {
return nil, false
}
foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil { if foundMAC != nil {
return s.index.findByMAC(foundMAC) return s.FindByMAC(foundMAC)
} }
return nil, false return nil, false
@@ -578,8 +487,6 @@ func (s *Storage) findByIP(addr netip.Addr) (p *Persistent, ok bool) {
// //
// Note that multiple clients can have the same IP address with different zones. // Note that multiple clients can have the same IP address with different zones.
// Therefore, the result of this method is indeterminate. // Therefore, the result of this method is indeterminate.
//
// TODO(s.chzhen): Consider accepting [FindParams].
func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) { func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@@ -591,7 +498,7 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
foundMAC := s.dhcp.MACByIP(ip) foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil { if foundMAC != nil {
return s.index.findByMAC(foundMAC) return s.FindByMAC(foundMAC)
} }
p = s.index.findByIPWithoutZone(ip) p = s.index.findByIPWithoutZone(ip)
@@ -602,6 +509,17 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
return nil, false return nil, false
} }
// FindByMAC finds persistent client by MAC and returns its shallow copy. s.mu
// is expected to be locked.
func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) {
p, ok = s.index.findByMAC(mac)
if ok {
return p.ShallowClone(), ok
}
return nil, false
}
// RemoveByName removes persistent client information. ok is false if no such // RemoveByName removes persistent client information. ok is false if no such
// client exists by that name. // client exists by that name.
func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) { func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) {
@@ -730,9 +648,9 @@ func (s *Storage) CustomUpstreamConfig(
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
c, ok := s.index.findByClientID(ClientID(id)) c, ok := s.index.findByClientID(id)
if !ok { if !ok {
c, ok = s.findByIP(addr) c, ok = s.index.findByIP(addr)
} }
if !ok { if !ok {
@@ -764,7 +682,7 @@ func (s *Storage) ClearUpstreamCache() {
// ClientID or client IP address, and applies it to the filtering settings. // ClientID or client IP address, and applies it to the filtering settings.
// setts must not be nil. // setts must not be nil.
func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filtering.Settings) { func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filtering.Settings) {
c, ok := s.index.findByClientID(ClientID(id)) c, ok := s.index.findByClientID(id)
if !ok { if !ok {
c, ok = s.index.findByIP(addr) c, ok = s.index.findByIP(addr)
} }
@@ -772,7 +690,7 @@ func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filter
if !ok { if !ok {
foundMAC := s.dhcp.MACByIP(addr) foundMAC := s.dhcp.MACByIP(addr)
if foundMAC != nil { if foundMAC != nil {
c, ok = s.index.findByMAC(foundMAC) c, ok = s.FindByMAC(foundMAC)
} }
} }

View File

@@ -15,7 +15,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/whois" "github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile" "github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
@@ -351,15 +350,15 @@ func TestClientsDHCP(t *testing.T) {
cliName1 = "one.dhcp" cliName1 = "one.dhcp"
cliIP2 = netip.MustParseAddr("2.2.2.2") cliIP2 = netip.MustParseAddr("2.2.2.2")
cliMAC2 = errors.Must(net.ParseMAC("22:22:22:22:22:22")) cliMAC2 = mustParseMAC("22:22:22:22:22:22")
cliName2 = "two.dhcp" cliName2 = "two.dhcp"
cliIP3 = netip.MustParseAddr("3.3.3.3") cliIP3 = netip.MustParseAddr("3.3.3.3")
cliMAC3 = errors.Must(net.ParseMAC("33:33:33:33:33:33")) cliMAC3 = mustParseMAC("33:33:33:33:33:33")
cliName3 = "three.dhcp" cliName3 = "three.dhcp"
prsCliIP = netip.MustParseAddr("4.3.2.1") prsCliIP = netip.MustParseAddr("4.3.2.1")
prsCliMAC = errors.Must(net.ParseMAC("AA:AA:AA:AA:AA:AA")) prsCliMAC = mustParseMAC("AA:AA:AA:AA:AA:AA")
prsCliName = "persistent.dhcp" prsCliName = "persistent.dhcp"
otherARPCliName = "other.arp" otherARPCliName = "other.arp"
@@ -520,11 +519,7 @@ func TestClientsDHCP(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
params := &client.FindParams{} prsCli, ok := storage.Find(prsCliIP.String())
err = params.Set(prsCliIP.String())
require.NoError(t, err)
prsCli, ok := storage.Find(params)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, prsCliName, prsCli.Name) assert.Equal(t, prsCliName, prsCli.Name)
@@ -668,6 +663,17 @@ func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
return s return s
} }
// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
// error.
func mustParseMAC(s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
if err != nil {
panic(err)
}
return mac
}
func TestStorage_Add(t *testing.T) { func TestStorage_Add(t *testing.T) {
const ( const (
existingName = "existing_name" existingName = "existing_name"
@@ -687,7 +693,7 @@ func TestStorage_Add(t *testing.T) {
Name: existingName, Name: existingName,
IPs: []netip.Addr{existingIP}, IPs: []netip.Addr{existingIP},
Subnets: []netip.Prefix{existingSubnet}, Subnets: []netip.Prefix{existingSubnet},
ClientIDs: []client.ClientID{existingClientID}, ClientIDs: []string{existingClientID},
UID: existingClientUID, UID: existingClientUID,
} }
@@ -755,7 +761,7 @@ func TestStorage_Add(t *testing.T) {
name: "duplicate_client_id", name: "duplicate_client_id",
cli: &client.Persistent{ cli: &client.Persistent{
Name: "duplicate_client_id", Name: "duplicate_client_id",
ClientIDs: []client.ClientID{existingClientID}, ClientIDs: []string{existingClientID},
UID: client.MustNewUID(), UID: client.MustNewUID(),
}, },
wantErrMsg: `adding client: another client "existing_name" ` + wantErrMsg: `adding client: another client "existing_name" ` +
@@ -892,12 +898,12 @@ func TestStorage_Find(t *testing.T) {
clientWithMAC = &client.Persistent{ clientWithMAC = &client.Persistent{
Name: "client_with_mac", Name: "client_with_mac",
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))}, MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
} }
clientWithID = &client.Persistent{ clientWithID = &client.Persistent{
Name: "client_with_id", Name: "client_with_id",
ClientIDs: []client.ClientID{cliID}, ClientIDs: []string{cliID},
} }
clientLinkLocal = &client.Persistent{ clientLinkLocal = &client.Persistent{
@@ -944,11 +950,7 @@ func TestStorage_Find(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
for _, id := range tc.ids { for _, id := range tc.ids {
params := &client.FindParams{} c, ok := s.Find(id)
err := params.Set(id)
require.NoError(t, err)
c, ok := s.Find(params)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, tc.want, c) assert.Equal(t, tc.want, c)
@@ -957,11 +959,7 @@ func TestStorage_Find(t *testing.T) {
} }
t.Run("not_found", func(t *testing.T) { t.Run("not_found", func(t *testing.T) {
params := &client.FindParams{} _, ok := s.Find(cliIPNone)
err := params.Set(cliIPNone)
require.NoError(t, err)
_, ok := s.Find(params)
assert.False(t, ok) assert.False(t, ok)
}) })
} }
@@ -1027,6 +1025,127 @@ func TestStorage_FindLoose(t *testing.T) {
} }
} }
func TestStorage_FindByName(t *testing.T) {
const (
cliIP1 = "1.1.1.1"
cliIP2 = "2.2.2.2"
)
const (
clientExistingName = "client_existing"
clientAnotherExistingName = "client_another_existing"
nonExistingClientName = "client_non_existing"
)
var (
clientExisting = &client.Persistent{
Name: clientExistingName,
IPs: []netip.Addr{netip.MustParseAddr(cliIP1)},
}
clientAnotherExisting = &client.Persistent{
Name: clientAnotherExistingName,
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
}
)
clients := []*client.Persistent{
clientExisting,
clientAnotherExisting,
}
s := newStorage(t, clients)
testCases := []struct {
want *client.Persistent
name string
clientName string
}{{
name: "existing",
clientName: clientExistingName,
want: clientExisting,
}, {
name: "another_existing",
clientName: clientAnotherExistingName,
want: clientAnotherExisting,
}, {
name: "non_existing",
clientName: nonExistingClientName,
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := s.FindByName(tc.clientName)
if tc.want == nil {
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.Equal(t, tc.want, c)
})
}
}
func TestStorage_FindByMAC(t *testing.T) {
var (
cliMAC = mustParseMAC("11:11:11:11:11:11")
cliAnotherMAC = mustParseMAC("22:22:22:22:22:22")
nonExistingClientMAC = mustParseMAC("33:33:33:33:33:33")
)
var (
clientExisting = &client.Persistent{
Name: "client",
MACs: []net.HardwareAddr{cliMAC},
}
clientAnotherExisting = &client.Persistent{
Name: "another_client",
MACs: []net.HardwareAddr{cliAnotherMAC},
}
)
clients := []*client.Persistent{
clientExisting,
clientAnotherExisting,
}
s := newStorage(t, clients)
testCases := []struct {
want *client.Persistent
name string
clientMAC net.HardwareAddr
}{{
name: "existing",
clientMAC: cliMAC,
want: clientExisting,
}, {
name: "another_existing",
clientMAC: cliAnotherMAC,
want: clientAnotherExisting,
}, {
name: "non_existing",
clientMAC: nonExistingClientMAC,
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := s.FindByMAC(tc.clientMAC)
if tc.want == nil {
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.Equal(t, tc.want, c)
})
}
}
func TestStorage_Update(t *testing.T) { func TestStorage_Update(t *testing.T) {
const ( const (
clientName = "client_name" clientName = "client_name"
@@ -1043,7 +1162,7 @@ func TestStorage_Update(t *testing.T) {
Name: obstructingName, Name: obstructingName,
IPs: []netip.Addr{obstructingIP}, IPs: []netip.Addr{obstructingIP},
Subnets: []netip.Prefix{obstructingSubnet}, Subnets: []netip.Prefix{obstructingSubnet},
ClientIDs: []client.ClientID{obstructingClientID}, ClientIDs: []string{obstructingClientID},
} }
clientToUpdate := &client.Persistent{ clientToUpdate := &client.Persistent{
@@ -1092,7 +1211,7 @@ func TestStorage_Update(t *testing.T) {
name: "duplicate_client_id", name: "duplicate_client_id",
cli: &client.Persistent{ cli: &client.Persistent{
Name: "duplicate_client_id", Name: "duplicate_client_id",
ClientIDs: []client.ClientID{obstructingClientID}, ClientIDs: []string{obstructingClientID},
UID: client.MustNewUID(), UID: client.MustNewUID(),
}, },
wantErrMsg: `updating client: another client "obstructing_name" ` + wantErrMsg: `updating client: another client "obstructing_name" ` +
@@ -1119,19 +1238,19 @@ func TestStorage_Update(t *testing.T) {
func TestStorage_RangeByName(t *testing.T) { func TestStorage_RangeByName(t *testing.T) {
sortedClients := []*client.Persistent{{ sortedClients := []*client.Persistent{{
Name: "clientA", Name: "clientA",
ClientIDs: []client.ClientID{"A"}, ClientIDs: []string{"A"},
}, { }, {
Name: "clientB", Name: "clientB",
ClientIDs: []client.ClientID{"B"}, ClientIDs: []string{"B"},
}, { }, {
Name: "clientC", Name: "clientC",
ClientIDs: []client.ClientID{"C"}, ClientIDs: []string{"C"},
}, { }, {
Name: "clientD", Name: "clientD",
ClientIDs: []client.ClientID{"D"}, ClientIDs: []string{"D"},
}, { }, {
Name: "clientE", Name: "clientE",
ClientIDs: []client.ClientID{"E"}, ClientIDs: []string{"E"},
}} }}
testCases := []struct { testCases := []struct {
@@ -1169,20 +1288,29 @@ func TestStorage_RangeByName(t *testing.T) {
func TestStorage_CustomUpstreamConfig(t *testing.T) { func TestStorage_CustomUpstreamConfig(t *testing.T) {
const ( const (
existingClientID = "existing_client_id" existingName = "existing_name"
existingClientID = "existing_client_id"
nonExistingClientID = "non_existing_client_id" nonExistingClientID = "non_existing_client_id"
) )
var ( var (
existingIP = netip.MustParseAddr("192.0.2.1") existingClientUID = client.MustNewUID()
nonExistingIP = netip.MustParseAddr("192.0.2.255") existingIP = netip.MustParseAddr("192.0.2.1")
dhcpCliIP = netip.MustParseAddr("192.0.2.2") nonExistingIP = netip.MustParseAddr("192.0.2.255")
dhcpCliMAC = errors.Must(net.ParseMAC("02:00:00:00:00:00"))
testUpstreamTimeout = time.Second testUpstreamTimeout = time.Second
) )
existingClient := &client.Persistent{
Name: existingName,
IPs: []netip.Addr{existingIP},
ClientIDs: []string{existingClientID},
UID: existingClientUID,
Upstreams: []string{"192.0.2.0"},
}
date := time.Now() date := time.Now()
clock := &faketime.Clock{ clock := &faketime.Clock{
OnNow: func() (now time.Time) { OnNow: func() (now time.Time) {
@@ -1192,30 +1320,7 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
}, },
} }
ipToMAC := map[netip.Addr]net.HardwareAddr{ s := newTestStorage(t, clock)
dhcpCliIP: dhcpCliMAC,
}
dhcp := &testDHCP{
OnLeases: func() (ls []*dhcpsvc.Lease) {
panic("not implemented")
},
OnHostBy: func(ip netip.Addr) (host string) {
panic("not implemented")
},
OnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) {
return ipToMAC[ip]
},
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
s, err := client.NewStorage(ctx, &client.StorageConfig{
Logger: slogutil.NewDiscardLogger(),
Clock: clock,
DHCP: dhcp,
})
require.NoError(t, err)
s.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{ s.UpdateCommonUpstreamConfig(&client.CommonUpstreamConfig{
UpstreamTimeout: testUpstreamTimeout, UpstreamTimeout: testUpstreamTimeout,
}) })
@@ -1224,21 +1329,8 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
return s.Shutdown(testutil.ContextWithTimeout(t, testTimeout)) return s.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
}) })
err = s.Add(ctx, &client.Persistent{ ctx := testutil.ContextWithTimeout(t, testTimeout)
Name: "client_first", err := s.Add(ctx, existingClient)
IPs: []netip.Addr{existingIP},
ClientIDs: []client.ClientID{existingClientID},
UID: client.MustNewUID(),
Upstreams: []string{"192.0.2.0"},
})
require.NoError(t, err)
err = s.Add(ctx, &client.Persistent{
Name: "client_second",
MACs: []net.HardwareAddr{dhcpCliMAC},
UID: client.MustNewUID(),
Upstreams: []string{"192.0.2.0"},
})
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@@ -1256,11 +1348,6 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
cliID: "", cliID: "",
cliAddr: existingIP, cliAddr: existingIP,
wantNilConf: assert.NotNil, wantNilConf: assert.NotNil,
}, {
name: "client_dhcp",
cliID: "",
cliAddr: dhcpCliIP,
wantNilConf: assert.NotNil,
}, { }, {
name: "non_existing_client_id", name: "non_existing_client_id",
cliID: nonExistingClientID, cliID: nonExistingClientID,
@@ -1293,193 +1380,4 @@ func TestStorage_CustomUpstreamConfig(t *testing.T) {
assert.NotEqual(t, conf, updConf) assert.NotEqual(t, conf, updConf)
}) })
t.Run("same_custom_config", func(t *testing.T) {
firstConf := s.CustomUpstreamConfig(existingClientID, existingIP)
require.NotNil(t, firstConf)
secondConf := s.CustomUpstreamConfig(existingClientID, existingIP)
require.NotNil(t, secondConf)
assert.Same(t, firstConf, secondConf)
})
}
func BenchmarkFindParams_Set(b *testing.B) {
const (
testIPStr = "192.0.2.1"
testCIDRStr = "192.0.2.0/24"
testMACStr = "02:00:00:00:00:00"
testClientID = "clientid"
)
benchCases := []struct {
wantErr error
params *client.FindParams
name string
id string
}{{
wantErr: nil,
params: &client.FindParams{
ClientID: testClientID,
},
name: "client_id",
id: testClientID,
}, {
wantErr: nil,
params: &client.FindParams{
RemoteIP: netip.MustParseAddr(testIPStr),
},
name: "ip_address",
id: testIPStr,
}, {
wantErr: nil,
params: &client.FindParams{
Subnet: netip.MustParsePrefix(testCIDRStr),
},
name: "subnet",
id: testCIDRStr,
}, {
wantErr: nil,
params: &client.FindParams{
MAC: errors.Must(net.ParseMAC(testMACStr)),
},
name: "mac_address",
id: testMACStr,
}, {
wantErr: client.ErrBadIdentifier,
params: &client.FindParams{},
name: "bad_id",
id: "!@#$%^&*()_+",
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
params := &client.FindParams{}
var err error
b.ReportAllocs()
for b.Loop() {
err = params.Set(bc.id)
}
assert.ErrorIs(b, err, bc.wantErr)
assert.Equal(b, bc.params, params)
})
}
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardHome/internal/client
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkFindParams_Set/client_id-8 49463488 24.27 ns/op 0 B/op 0 allocs/op
// BenchmarkFindParams_Set/ip_address-8 18740977 62.22 ns/op 0 B/op 0 allocs/op
// BenchmarkFindParams_Set/subnet-8 10848192 110.0 ns/op 0 B/op 0 allocs/op
// BenchmarkFindParams_Set/mac_address-8 8148494 133.2 ns/op 8 B/op 1 allocs/op
// BenchmarkFindParams_Set/bad_id-8 73894278 16.29 ns/op 0 B/op 0 allocs/op
}
func BenchmarkStorage_Find(b *testing.B) {
const (
cliID = "cid"
cliMAC = "02:00:00:00:00:00"
)
const (
cliNameWithID = "client_with_id"
cliNameWithIP = "client_with_ip"
cliNameWithCIDR = "client_with_cidr"
cliNameWithMAC = "client_with_mac"
)
var (
cliIP = netip.MustParseAddr("192.0.2.1")
cliCIDR = netip.MustParsePrefix("192.0.2.0/24")
)
var (
clientWithID = &client.Persistent{
Name: cliNameWithID,
ClientIDs: []client.ClientID{cliID},
}
clientWithIP = &client.Persistent{
Name: cliNameWithIP,
IPs: []netip.Addr{cliIP},
}
clientWithCIDR = &client.Persistent{
Name: cliNameWithCIDR,
Subnets: []netip.Prefix{cliCIDR},
}
clientWithMAC = &client.Persistent{
Name: cliNameWithMAC,
MACs: []net.HardwareAddr{errors.Must(net.ParseMAC(cliMAC))},
}
)
clients := []*client.Persistent{
clientWithID,
clientWithIP,
clientWithCIDR,
clientWithMAC,
}
s := newStorage(b, clients)
benchCases := []struct {
params *client.FindParams
name string
wantName string
}{{
params: &client.FindParams{
ClientID: cliID,
},
name: "client_id",
wantName: cliNameWithID,
}, {
params: &client.FindParams{
RemoteIP: cliIP,
},
name: "ip_address",
wantName: cliNameWithIP,
}, {
params: &client.FindParams{
Subnet: cliCIDR,
},
name: "subnet",
wantName: cliNameWithCIDR,
}, {
params: &client.FindParams{
MAC: errors.Must(net.ParseMAC(cliMAC)),
},
name: "mac_address",
wantName: cliNameWithMAC,
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
var p *client.Persistent
var ok bool
b.ReportAllocs()
for b.Loop() {
p, ok = s.Find(bc.params)
}
assert.True(b, ok)
assert.NotNil(b, p)
assert.Equal(b, bc.wantName, p.Name)
})
}
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardHome/internal/client
// cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
// BenchmarkStorage_Find/client_id-8 7070107 154.4 ns/op 240 B/op 2 allocs/op
// BenchmarkStorage_Find/ip_address-8 6831823 168.6 ns/op 248 B/op 2 allocs/op
// BenchmarkStorage_Find/subnet-8 7209050 167.5 ns/op 256 B/op 2 allocs/op
// BenchmarkStorage_Find/mac_address-8 5776131 199.7 ns/op 256 B/op 3 allocs/op
} }

View File

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

View File

@@ -1,11 +1,13 @@
package dhcpsvc_test package dhcpsvc_test
import ( import (
"net"
"net/netip" "net/netip"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/stretchr/testify/require"
) )
// testLocalTLD is a common local TLD for tests. // testLocalTLD is a common local TLD for tests.
@@ -54,3 +56,11 @@ var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
}, },
}, },
} }
// mustParseMAC parses a hardware address from s and requires no errors.
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
require.NoError(t, err)
return mac
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,10 @@ import (
"net/url" "net/url"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/AdguardTeam/golibs/httphdr" "github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -33,13 +35,20 @@ func (w *testResponseWriter) WriteHeader(statusCode int) {
} }
func TestAuthHTTP(t *testing.T) { func TestAuthHTTP(t *testing.T) {
var (
ctx = testutil.ContextWithTimeout(t, testTimeout)
logger = slogutil.NewDiscardLogger()
err error
)
dir := t.TempDir() dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db") fn := filepath.Join(dir, "sessions.db")
users := []webUser{ users := []webUser{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
globalContext.auth = InitAuth(fn, users, 60, nil, nil) globalContext.auth, err = InitAuth(ctx, logger, fn, users, time.Minute, nil, nil)
require.NoError(t, err)
handlerCalled := false handlerCalled := false
handler := func(_ http.ResponseWriter, _ *http.Request) { handler := func(_ http.ResponseWriter, _ *http.Request) {
@@ -68,7 +77,11 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
// perform login // perform login
cookie, err := globalContext.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "") cookie, err := globalContext.auth.newCookie(
ctx,
loginJSON{Name: "name", Password: "password"},
"",
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, cookie) require.NotNil(t, cookie)
@@ -114,7 +127,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del(httphdr.Cookie) r.Header.Del(httphdr.Cookie)
globalContext.auth.Close() globalContext.auth.Close(ctx)
} }
func TestRealIP(t *testing.T) { func TestRealIP(t *testing.T) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,7 +82,7 @@ func (web *webAPI) requestVersionInfo(
) (err error) { ) (err error) {
updater := web.conf.updater updater := web.conf.updater
for range 3 { for range 3 {
resp.VersionInfo, err = updater.VersionInfo(ctx, recheck) resp.VersionInfo, err = updater.VersionInfo(recheck)
if err == nil { if err == nil {
return nil return nil
} }
@@ -133,7 +133,7 @@ func (web *webAPI) handleUpdate(w http.ResponseWriter, r *http.Request) {
return return
} }
err = updater.Update(r.Context(), false) err = updater.Update(false)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err) aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)

View File

@@ -119,15 +119,16 @@ func initDNS(
globalContext.dhcpServer, globalContext.dhcpServer,
anonymizer, anonymizer,
httpRegister, httpRegister,
tlsMgr.config(),
tlsMgr, tlsMgr,
baseLogger, baseLogger,
) )
} }
// initDNSServer initializes the [context.dnsServer]. To only use the internal // initDNSServer initializes the [context.dnsServer]. To only use the internal
// proxy, none of the arguments are required, but tlsMgr and l still must not be // proxy, none of the arguments are required, but tlsConf, tlsMgr and l still
// nil, in other cases all the arguments also must not be nil. It also must not // must not be nil, in other cases all the arguments also must not be nil. It
// be called unless [config] and [globalContext] are initialized. // also must not be called unless [config] and [globalContext] are initialized.
// //
// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter. // TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.
func initDNSServer( func initDNSServer(
@@ -137,6 +138,7 @@ func initDNSServer(
dhcpSrv dnsforward.DHCP, dhcpSrv dnsforward.DHCP,
anonymizer *aghnet.IPMut, anonymizer *aghnet.IPMut,
httpReg aghhttp.RegisterFunc, httpReg aghhttp.RegisterFunc,
tlsConf *tlsConfigSettings,
tlsMgr *tlsManager, tlsMgr *tlsManager,
l *slog.Logger, l *slog.Logger,
) (err error) { ) (err error) {
@@ -165,7 +167,7 @@ func initDNSServer(
dnsConf, err := newServerConfig( dnsConf, err := newServerConfig(
&config.DNS, &config.DNS,
config.Clients.Sources, config.Clients.Sources,
tlsMgr.config(), tlsConf,
tlsMgr, tlsMgr,
httpReg, httpReg,
globalContext.clients.storage, globalContext.clients.storage,
@@ -345,6 +347,13 @@ func newDNSTLSConfig(
return nil, fmt.Errorf(format, err) return nil, fmt.Errorf(format, err)
} }
// Unencrypted DoH is managed by AdGuard Home itself, not by dnsproxy.
// Therefore, avoid setting the certificate property to prevent dnsproxy
// from starting encrypted listeners. See [dnsforward.Server.prepareTLS].
if conf.AllowUnencryptedDoH {
return dnsConf, nil
}
dnsConf.Cert = &cert dnsConf.Cert = &cert
return dnsConf, nil return dnsConf, nil

View File

@@ -487,14 +487,9 @@ func checkPorts() (err error) {
} }
// isUpdateEnabled returns true if the update is enabled for current // isUpdateEnabled returns true if the update is enabled for current
// configuration. It also logs the decision. isCustomURL should be true if the // configuration. It also logs the decision. customURL should be true if the
// updater is using a custom URL. // updater is using a custom URL.
func isUpdateEnabled( func isUpdateEnabled(ctx context.Context, l *slog.Logger, opts *options, customURL bool) (ok bool) {
ctx context.Context,
l *slog.Logger,
opts *options,
isCustomURL bool,
) (ok bool) {
if opts.disableUpdate { if opts.disableUpdate {
l.DebugContext(ctx, "updates are disabled by command-line option") l.DebugContext(ctx, "updates are disabled by command-line option")
@@ -505,13 +500,13 @@ func isUpdateEnabled(
case case
version.ChannelDevelopment, version.ChannelDevelopment,
version.ChannelCandidate: version.ChannelCandidate:
if isCustomURL { if customURL {
l.DebugContext(ctx, "updates are enabled because custom url is used") l.DebugContext(ctx, "updates are enabled because custom url is used")
} else { } else {
l.DebugContext(ctx, "updates are disabled for development and candidate builds") l.DebugContext(ctx, "updates are disabled for development and candidate builds")
} }
return isCustomURL return customURL
default: default:
l.DebugContext(ctx, "updates are enabled") l.DebugContext(ctx, "updates are enabled")
@@ -519,7 +514,7 @@ func isUpdateEnabled(
} }
} }
// initWeb initializes the web module. upd, baseLogger, and tlsMgr must not be // initWeb initializes the web module. upd, baseLogger, and tlsMgr must not be
// nil. // nil.
func initWeb( func initWeb(
ctx context.Context, ctx context.Context,
@@ -528,7 +523,7 @@ func initWeb(
upd *updater.Updater, upd *updater.Updater,
baseLogger *slog.Logger, baseLogger *slog.Logger,
tlsMgr *tlsManager, tlsMgr *tlsManager,
isCustomUpdURL bool, customURL bool,
) (web *webAPI, err error) { ) (web *webAPI, err error) {
logger := baseLogger.With(slogutil.KeyPrefix, "webapi") logger := baseLogger.With(slogutil.KeyPrefix, "webapi")
@@ -544,7 +539,7 @@ func initWeb(
} }
} }
disableUpdate := !isUpdateEnabled(ctx, baseLogger, &opts, isCustomUpdURL) disableUpdate := !isUpdateEnabled(ctx, baseLogger, &opts, customURL)
webConf := &webConfig{ webConf := &webConfig{
updater: upd, updater: upd,
@@ -650,12 +645,11 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
confPath := configFilePath() confPath := configFilePath()
updLogger := slogLogger.With(slogutil.KeyPrefix, "updater") upd, customURL := newUpdater(ctx, slogLogger, globalContext.workDir, confPath, execPath, config)
upd, isCustomURL := newUpdater(ctx, updLogger, config, globalContext.workDir, confPath, execPath)
// TODO(e.burkov): This could be made earlier, probably as the option's // TODO(e.burkov): This could be made earlier, probably as the option's
// effect. // effect.
cmdlineUpdate(ctx, updLogger, opts, upd, tlsMgr) cmdlineUpdate(ctx, slogLogger, opts, upd, tlsMgr)
if !globalContext.firstRun { if !globalContext.firstRun {
// Save the updated config. // Save the updated config.
@@ -674,10 +668,10 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
GLMode = opts.glinetMode GLMode = opts.glinetMode
// Init auth module. // Init auth module.
globalContext.auth, err = initUsers() globalContext.auth, err = initUsers(ctx, slogLogger)
fatalOnError(err) fatalOnError(err)
web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, isCustomURL) web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
fatalOnError(err) fatalOnError(err)
globalContext.web = web globalContext.web = web
@@ -720,17 +714,16 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
<-done <-done
} }
// newUpdater creates a new AdGuard Home updater. l and conf must not be nil. // newUpdater creates a new AdGuard Home updater. customURL is true if the user
// workDir, confPath, and execPath must not be empty. isCustomURL is true if // has specified a custom version announcement URL.
// the user has specified a custom version announcement URL.
func newUpdater( func newUpdater(
ctx context.Context, ctx context.Context,
l *slog.Logger, l *slog.Logger,
conf *configuration,
workDir string, workDir string,
confPath string, confPath string,
execPath string, execPath string,
) (upd *updater.Updater, isCustomURL bool) { config *configuration,
) (upd *updater.Updater, customURL bool) {
// envName is the name of the environment variable that can be used to // envName is the name of the environment variable that can be used to
// override the default version check URL. // override the default version check URL.
const envName = "ADGUARD_HOME_TEST_UPDATE_VERSION_URL" const envName = "ADGUARD_HOME_TEST_UPDATE_VERSION_URL"
@@ -742,14 +735,14 @@ func newUpdater(
case version.Channel() == version.ChannelRelease: case version.Channel() == version.ChannelRelease:
// Only enable custom version URL for development builds. // Only enable custom version URL for development builds.
l.DebugContext(ctx, "custom version url is disabled for release builds") l.DebugContext(ctx, "custom version url is disabled for release builds")
case !conf.UnsafeUseCustomUpdateIndexURL: case !config.UnsafeUseCustomUpdateIndexURL:
l.DebugContext(ctx, "custom version url is disabled in config") l.DebugContext(ctx, "custom version url is disabled in config")
default: default:
versionURL, _ = url.Parse(customURLStr) versionURL, _ = url.Parse(customURLStr)
} }
err := urlutil.ValidateHTTPURL(versionURL) err := urlutil.ValidateHTTPURL(versionURL)
if isCustomURL = err == nil; !isCustomURL { if customURL = err == nil; !customURL {
l.DebugContext(ctx, "parsing custom version url", slogutil.KeyError, err) l.DebugContext(ctx, "parsing custom version url", slogutil.KeyError, err)
versionURL = updater.DefaultVersionURL() versionURL = updater.DefaultVersionURL()
@@ -758,8 +751,7 @@ func newUpdater(
l.DebugContext(ctx, "creating updater", "config_path", confPath) l.DebugContext(ctx, "creating updater", "config_path", confPath)
return updater.NewUpdater(&updater.Config{ return updater.NewUpdater(&updater.Config{
Client: conf.Filtering.HTTPClient, Client: config.Filtering.HTTPClient,
Logger: l,
Version: version.Version(), Version: version.Version(),
Channel: version.Channel(), Channel: version.Channel(),
GOARCH: runtime.GOARCH, GOARCH: runtime.GOARCH,
@@ -770,7 +762,7 @@ func newUpdater(
ConfName: confPath, ConfName: confPath,
ExecPath: execPath, ExecPath: execPath,
VersionCheckURL: versionURL, VersionCheckURL: versionURL,
}), isCustomURL }), customURL
} }
// checkPermissions checks and migrates permissions of the files and directories // checkPermissions checks and migrates permissions of the files and directories
@@ -794,7 +786,8 @@ func checkPermissions(
} }
// initUsers initializes context auth module. Clears config users field. // initUsers initializes context auth module. Clears config users field.
func initUsers() (auth *Auth, err error) { // baseLogger must not be nil.
func initUsers(ctx context.Context, baseLogger *slog.Logger) (auth *Auth, err error) {
sessFilename := filepath.Join(globalContext.getDataDir(), "sessions.db") sessFilename := filepath.Join(globalContext.getDataDir(), "sessions.db")
var rateLimiter *authRateLimiter var rateLimiter *authRateLimiter
@@ -807,10 +800,17 @@ func initUsers() (auth *Auth, err error) {
trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies)) trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies))
sessionTTL := time.Duration(config.HTTPConfig.SessionTTL).Seconds() auth, err = InitAuth(
auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter, trustedProxies) ctx,
if auth == nil { baseLogger,
return nil, errors.Error("initializing auth module failed") sessFilename,
config.Users,
time.Duration(config.HTTPConfig.SessionTTL),
rateLimiter,
trustedProxies,
)
if err != nil {
return nil, fmt.Errorf("initializing auth module: %w", err)
} }
config.Users = nil config.Users = nil
@@ -924,7 +924,7 @@ func cleanup(ctx context.Context) {
globalContext.web = nil globalContext.web = nil
} }
if globalContext.auth != nil { if globalContext.auth != nil {
globalContext.auth.Close() globalContext.auth.Close(ctx)
globalContext.auth = nil globalContext.auth = nil
} }
@@ -1086,12 +1086,12 @@ func cmdlineUpdate(
// //
// TODO(e.burkov): We could probably initialize the internal resolver // TODO(e.burkov): We could probably initialize the internal resolver
// separately. // separately.
err := initDNSServer(nil, nil, nil, nil, nil, nil, tlsMgr, l) err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, tlsMgr, l)
fatalOnError(err) fatalOnError(err)
l.InfoContext(ctx, "performing update via cli") l.InfoContext(ctx, "performing update via cli")
info, err := upd.VersionInfo(ctx, true) info, err := upd.VersionInfo(true)
if err != nil { if err != nil {
l.ErrorContext(ctx, "getting version info", slogutil.KeyError, err) l.ErrorContext(ctx, "getting version info", slogutil.KeyError, err)
@@ -1104,7 +1104,7 @@ func cmdlineUpdate(
os.Exit(osutil.ExitCodeSuccess) os.Exit(osutil.ExitCodeSuccess)
} }
err = upd.Update(ctx, globalContext.firstRun) err = upd.Update(globalContext.firstRun)
fatalOnError(err) fatalOnError(err)
err = restartService() err = restartService()

View File

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

View File

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

View File

@@ -193,10 +193,7 @@ func (m *tlsManager) start(_ context.Context) {
m.web.tlsConfigChanged(context.Background(), m.conf) m.web.tlsConfigChanged(context.Background(), m.conf)
} }
// reload updates the configuration and restarts the TLS manager. It logs any // reload updates the configuration and restarts the TLS manager.
// encountered errors.
//
// TODO(s.chzhen): Consider returning an error.
func (m *tlsManager) reload(ctx context.Context) { func (m *tlsManager) reload(ctx context.Context) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()

View File

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

View File

@@ -1,7 +1,6 @@
package updater package updater
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -13,6 +12,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/ioutil" "github.com/AdguardTeam/golibs/ioutil"
"github.com/AdguardTeam/golibs/log"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
) )
@@ -35,7 +35,7 @@ const maxVersionRespSize datasize.ByteSize = 64 * datasize.KB
// VersionInfo downloads the latest version information. If forceRecheck is // VersionInfo downloads the latest version information. If forceRecheck is
// false and there are cached results, those results are returned. // false and there are cached results, those results are returned.
func (u *Updater) VersionInfo(ctx context.Context, forceRecheck bool) (vi VersionInfo, err error) { func (u *Updater) VersionInfo(forceRecheck bool) (vi VersionInfo, err error) {
u.mu.Lock() u.mu.Lock()
defer u.mu.Unlock() defer u.mu.Unlock()
@@ -45,17 +45,11 @@ func (u *Updater) VersionInfo(ctx context.Context, forceRecheck bool) (vi Versio
return u.prevCheckResult, u.prevCheckError return u.prevCheckResult, u.prevCheckError
} }
var resp *http.Response
vcu := u.versionCheckURL vcu := u.versionCheckURL
req, err := http.NewRequestWithContext(ctx, http.MethodGet, vcu, nil) resp, err = u.client.Get(vcu)
if err != nil { if err != nil {
return VersionInfo{}, fmt.Errorf("constructing request to %s: %w", vcu, err) return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
}
u.logger.DebugContext(ctx, "requesting version data", "url", vcu)
resp, err := u.client.Do(req)
if err != nil {
return VersionInfo{}, fmt.Errorf("requesting %s: %w", vcu, err)
} }
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }() defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
@@ -65,16 +59,16 @@ func (u *Updater) VersionInfo(ctx context.Context, forceRecheck bool) (vi Versio
// ReadCloser. // ReadCloser.
body, err := io.ReadAll(r) body, err := io.ReadAll(r)
if err != nil { if err != nil {
return VersionInfo{}, fmt.Errorf("reading response from %s: %w", vcu, err) return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
} }
u.prevCheckTime = now u.prevCheckTime = now
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(ctx, body) u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
return u.prevCheckResult, u.prevCheckError return u.prevCheckResult, u.prevCheckError
} }
func (u *Updater) parseVersionResponse(ctx context.Context, data []byte) (VersionInfo, error) { func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
info := VersionInfo{ info := VersionInfo{
CanAutoUpdate: aghalg.NBFalse, CanAutoUpdate: aghalg.NBFalse,
} }
@@ -98,7 +92,7 @@ func (u *Updater) parseVersionResponse(ctx context.Context, data []byte) (Versio
info.Announcement = versionJSON["announcement"] info.Announcement = versionJSON["announcement"]
info.AnnouncementURL = versionJSON["announcement_url"] info.AnnouncementURL = versionJSON["announcement_url"]
packageURL, key, found := u.downloadURL(ctx, versionJSON) packageURL, key, found := u.downloadURL(versionJSON)
if !found { if !found {
return info, fmt.Errorf("version.json: no package URL: key %q not found in object", key) return info, fmt.Errorf("version.json: no package URL: key %q not found in object", key)
} }
@@ -114,10 +108,7 @@ func (u *Updater) parseVersionResponse(ctx context.Context, data []byte) (Versio
// downloadURL returns the download URL for current build as well as its key in // downloadURL returns the download URL for current build as well as its key in
// versionObj. If the key is not found, it additionally prints an informative // versionObj. If the key is not found, it additionally prints an informative
// log message. // log message.
func (u *Updater) downloadURL( func (u *Updater) downloadURL(versionObj map[string]string) (dlURL, key string, ok bool) {
ctx context.Context,
versionObj map[string]string,
) (dlURL, key string, ok bool) {
if u.goarch == "arm" && u.goarm != "" { if u.goarch == "arm" && u.goarm != "" {
key = fmt.Sprintf("download_%s_%sv%s", u.goos, u.goarch, u.goarm) key = fmt.Sprintf("download_%s_%sv%s", u.goos, u.goarch, u.goarm)
} else if isMIPS(u.goarch) && u.gomips != "" { } else if isMIPS(u.goarch) && u.gomips != "" {
@@ -133,7 +124,7 @@ func (u *Updater) downloadURL(
keys := slices.Sorted(maps.Keys(versionObj)) keys := slices.Sorted(maps.Keys(versionObj))
u.logger.ErrorContext(ctx, "key not found", "missing", key, "got", keys) log.Error("updater: key %q not found; got keys %q", key, keys)
return "", key, false return "", key, false
} }

View File

@@ -10,7 +10,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/AdGuardHome/internal/updater"
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -59,7 +58,6 @@ func TestUpdater_VersionInfo(t *testing.T) {
u := updater.NewUpdater(&updater.Config{ u := updater.NewUpdater(&updater.Config{
Client: srv.Client(), Client: srv.Client(),
Logger: testLogger,
Version: "v0.103.0-beta.1", Version: "v0.103.0-beta.1",
Channel: version.ChannelBeta, Channel: version.ChannelBeta,
GOARCH: "arm", GOARCH: "arm",
@@ -67,8 +65,7 @@ func TestUpdater_VersionInfo(t *testing.T) {
VersionCheckURL: fakeURL, VersionCheckURL: fakeURL,
}) })
ctx := testutil.ContextWithTimeout(t, testTimeout) info, err := u.VersionInfo(false)
info, err := u.VersionInfo(ctx, false)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, counter, 1) assert.Equal(t, counter, 1)
@@ -78,14 +75,14 @@ func TestUpdater_VersionInfo(t *testing.T) {
assert.Equal(t, aghalg.NBTrue, info.CanAutoUpdate) assert.Equal(t, aghalg.NBTrue, info.CanAutoUpdate)
t.Run("cache_check", func(t *testing.T) { t.Run("cache_check", func(t *testing.T) {
_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), false) _, err = u.VersionInfo(false)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, counter, 1) assert.Equal(t, counter, 1)
}) })
t.Run("force_check", func(t *testing.T) { t.Run("force_check", func(t *testing.T) {
_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), true) _, err = u.VersionInfo(true)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, counter, 2) assert.Equal(t, counter, 2)
@@ -94,7 +91,7 @@ func TestUpdater_VersionInfo(t *testing.T) {
t.Run("api_fail", func(t *testing.T) { t.Run("api_fail", func(t *testing.T) {
srv.Close() srv.Close()
_, err = u.VersionInfo(testutil.ContextWithTimeout(t, testTimeout), true) _, err = u.VersionInfo(true)
var urlErr *url.Error var urlErr *url.Error
assert.ErrorAs(t, err, &urlErr) assert.ErrorAs(t, err, &urlErr)
}) })
@@ -133,7 +130,6 @@ func TestUpdater_VersionInfo_others(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
u := updater.NewUpdater(&updater.Config{ u := updater.NewUpdater(&updater.Config{
Client: fakeClient, Client: fakeClient,
Logger: testLogger,
Version: "v0.103.0-beta.1", Version: "v0.103.0-beta.1",
Channel: version.ChannelBeta, Channel: version.ChannelBeta,
GOOS: "linux", GOOS: "linux",
@@ -143,8 +139,7 @@ func TestUpdater_VersionInfo_others(t *testing.T) {
VersionCheckURL: fakeURL, VersionCheckURL: fakeURL,
}) })
ctx := testutil.ContextWithTimeout(t, testTimeout) info, err := u.VersionInfo(false)
info, err := u.VersionInfo(ctx, false)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "v0.103.0-beta.2", info.NewVersion) assert.Equal(t, "v0.103.0-beta.2", info.NewVersion)

View File

@@ -5,11 +5,9 @@ import (
"archive/tar" "archive/tar"
"archive/zip" "archive/zip"
"compress/gzip" "compress/gzip"
"context"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"log/slog"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@@ -24,14 +22,13 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/ioutil" "github.com/AdguardTeam/golibs/ioutil"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil/urlutil" "github.com/AdguardTeam/golibs/netutil/urlutil"
) )
// Updater is the AdGuard Home updater. // Updater is the AdGuard Home updater.
type Updater struct { type Updater struct {
client *http.Client client *http.Client
logger *slog.Logger
version string version string
channel string channel string
@@ -78,48 +75,27 @@ func DefaultVersionURL() *url.URL {
// Config is the AdGuard Home updater configuration. // Config is the AdGuard Home updater configuration.
type Config struct { type Config struct {
// Client is used to perform HTTP requests. It must not be nil.
Client *http.Client Client *http.Client
// Logger is used for logging the update process. It must not be nil.
Logger *slog.Logger
// VersionCheckURL is URL to the latest version announcement. It must not // VersionCheckURL is URL to the latest version announcement. It must not
// be nil, see [DefaultVersionURL]. // be nil, see [DefaultVersionURL].
VersionCheckURL *url.URL VersionCheckURL *url.URL
// Version is the current AdGuard Home version. It must not be empty.
Version string Version string
// Channel is the current AdGuard Home update channel. It must be a valid
// channel, see [version.ChannelBeta] and the related constants.
Channel string Channel string
GOARCH string
GOOS string
GOARM string
GOMIPS string
// GOARCH is the current CPU architecture. It must not be empty and must be // ConfName is the name of the current configuration file. Typically,
// one of the supported architectures. // "AdGuardHome.yaml".
GOARCH string
// GOOS is the current operating system. It must not be empty and must be
// one of the supported OSs.
GOOS string
// GOARM is the current ARM variant, if any. It must either be empty or be
// a valid and supported GOARM value.
GOARM string
// GOMIPS is the current MIPS variant, if any. It must either be empty or
// be a valid and supported GOMIPS value.
GOMIPS string
// ConfName is the name of the current configuration file. It must not be
// empty.
ConfName string ConfName string
// WorkDir is the working directory that is used for temporary files. It // WorkDir is the working directory that is used for temporary files.
// must not be empty.
WorkDir string WorkDir string
// ExecPath is path to the executable file. It must not be empty. // ExecPath is path to the executable file.
ExecPath string ExecPath string
} }
@@ -127,7 +103,6 @@ type Config struct {
func NewUpdater(conf *Config) *Updater { func NewUpdater(conf *Config) *Updater {
return &Updater{ return &Updater{
client: conf.Client, client: conf.Client,
logger: conf.Logger,
version: conf.Version, version: conf.Version,
channel: conf.Channel, channel: conf.Channel,
@@ -147,49 +122,49 @@ func NewUpdater(conf *Config) *Updater {
// Update performs the auto-update. It returns an error if the update failed. // Update performs the auto-update. It returns an error if the update failed.
// If firstRun is true, it assumes the configuration file doesn't exist. // If firstRun is true, it assumes the configuration file doesn't exist.
func (u *Updater) Update(ctx context.Context, firstRun bool) (err error) { func (u *Updater) Update(firstRun bool) (err error) {
u.mu.Lock() u.mu.Lock()
defer u.mu.Unlock() defer u.mu.Unlock()
u.logger.InfoContext(ctx, "staring update", "first_run", firstRun) log.Info("updater: updating")
defer func() { defer func() {
if err != nil { if err != nil {
u.logger.ErrorContext(ctx, "update failed", slogutil.KeyError, err) log.Info("updater: failed")
} else { } else {
u.logger.InfoContext(ctx, "update finished") log.Info("updater: finished successfully")
} }
}() }()
err = u.prepare(ctx) err = u.prepare()
if err != nil { if err != nil {
return fmt.Errorf("preparing: %w", err) return fmt.Errorf("preparing: %w", err)
} }
defer u.clean(ctx) defer u.clean()
err = u.downloadPackageFile(ctx) err = u.downloadPackageFile()
if err != nil { if err != nil {
return fmt.Errorf("downloading package file: %w", err) return fmt.Errorf("downloading package file: %w", err)
} }
err = u.unpack(ctx) err = u.unpack()
if err != nil { if err != nil {
return fmt.Errorf("unpacking: %w", err) return fmt.Errorf("unpacking: %w", err)
} }
if !firstRun { if !firstRun {
err = u.check(ctx) err = u.check()
if err != nil { if err != nil {
return fmt.Errorf("checking config: %w", err) return fmt.Errorf("checking config: %w", err)
} }
} }
err = u.backup(ctx, firstRun) err = u.backup(firstRun)
if err != nil { if err != nil {
return fmt.Errorf("making backup: %w", err) return fmt.Errorf("making backup: %w", err)
} }
err = u.replace(ctx) err = u.replace()
if err != nil { if err != nil {
return fmt.Errorf("replacing: %w", err) return fmt.Errorf("replacing: %w", err)
} }
@@ -206,7 +181,7 @@ func (u *Updater) NewVersion() (nv string) {
} }
// prepare fills all necessary fields in Updater object. // prepare fills all necessary fields in Updater object.
func (u *Updater) prepare(ctx context.Context) (err error) { func (u *Updater) prepare() (err error) {
u.updateDir = filepath.Join(u.workDir, fmt.Sprintf("agh-update-%s", u.newVersion)) u.updateDir = filepath.Join(u.workDir, fmt.Sprintf("agh-update-%s", u.newVersion))
_, pkgNameOnly := filepath.Split(u.packageURL) _, pkgNameOnly := filepath.Split(u.packageURL)
@@ -225,12 +200,11 @@ func (u *Updater) prepare(ctx context.Context) (err error) {
u.backupExeName = filepath.Join(u.backupDir, filepath.Base(u.execPath)) u.backupExeName = filepath.Join(u.backupDir, filepath.Base(u.execPath))
u.updateExeName = filepath.Join(u.updateDir, updateExeName) u.updateExeName = filepath.Join(u.updateDir, updateExeName)
u.logger.InfoContext( log.Debug(
ctx, "updater: updating from %s to %s using url: %s",
"updating", version.Version(),
"from", version.Version(), u.newVersion,
"to", u.newVersion, u.packageURL,
"package_url", u.packageURL,
) )
u.currentExeName = u.execPath u.currentExeName = u.execPath
@@ -243,20 +217,23 @@ func (u *Updater) prepare(ctx context.Context) (err error) {
} }
// unpack extracts the files from the downloaded archive. // unpack extracts the files from the downloaded archive.
func (u *Updater) unpack(ctx context.Context) (err error) { func (u *Updater) unpack() error {
var err error
_, pkgNameOnly := filepath.Split(u.packageURL) _, pkgNameOnly := filepath.Split(u.packageURL)
u.logger.InfoContext(ctx, "unpacking package", "package_name", pkgNameOnly) log.Debug("updater: unpacking package")
if strings.HasSuffix(pkgNameOnly, ".zip") { if strings.HasSuffix(pkgNameOnly, ".zip") {
u.unpackedFiles, err = u.unpackZip(ctx, u.packageName, u.updateDir) u.unpackedFiles, err = zipFileUnpack(u.packageName, u.updateDir)
if err != nil { if err != nil {
return fmt.Errorf(".zip unpack failed: %w", err) return fmt.Errorf(".zip unpack failed: %w", err)
} }
} else if strings.HasSuffix(pkgNameOnly, ".tar.gz") { } else if strings.HasSuffix(pkgNameOnly, ".tar.gz") {
u.unpackedFiles, err = u.unpackTarGz(ctx, u.packageName, u.updateDir) u.unpackedFiles, err = tarGzFileUnpack(u.packageName, u.updateDir)
if err != nil { if err != nil {
return fmt.Errorf(".tar.gz unpack failed: %w", err) return fmt.Errorf(".tar.gz unpack failed: %w", err)
} }
} else { } else {
return fmt.Errorf("unknown package extension") return fmt.Errorf("unknown package extension")
} }
@@ -266,8 +243,8 @@ func (u *Updater) unpack(ctx context.Context) (err error) {
// check returns an error if the configuration file couldn't be used with the // check returns an error if the configuration file couldn't be used with the
// version of AdGuard Home just downloaded. // version of AdGuard Home just downloaded.
func (u *Updater) check(ctx context.Context) (err error) { func (u *Updater) check() (err error) {
u.logger.InfoContext(ctx, "checking configuration") log.Debug("updater: checking configuration")
err = copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"), aghos.DefaultPermFile) err = copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"), aghos.DefaultPermFile)
if err != nil { if err != nil {
@@ -291,9 +268,8 @@ func (u *Updater) check(ctx context.Context) (err error) {
// backup makes a backup of the current configuration and supporting files. It // backup makes a backup of the current configuration and supporting files. It
// ignores the configuration file if firstRun is true. // ignores the configuration file if firstRun is true.
func (u *Updater) backup(ctx context.Context, firstRun bool) (err error) { func (u *Updater) backup(firstRun bool) (err error) {
u.logger.InfoContext(ctx, "backing up current configuration") log.Debug("updater: backing up current configuration")
_ = os.Mkdir(u.backupDir, aghos.DefaultPermDir) _ = os.Mkdir(u.backupDir, aghos.DefaultPermDir)
if !firstRun { if !firstRun {
err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"), aghos.DefaultPermFile) err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"), aghos.DefaultPermFile)
@@ -303,7 +279,7 @@ func (u *Updater) backup(ctx context.Context, firstRun bool) (err error) {
} }
wd := u.workDir wd := u.workDir
err = u.copySupportingFiles(ctx, u.unpackedFiles, wd, u.backupDir) err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
if err != nil { if err != nil {
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %w", wd, u.backupDir, err) return fmt.Errorf("copySupportingFiles(%s, %s) failed: %w", wd, u.backupDir, err)
} }
@@ -313,18 +289,13 @@ func (u *Updater) backup(ctx context.Context, firstRun bool) (err error) {
// replace moves the current executable with the updated one and also copies the // replace moves the current executable with the updated one and also copies the
// supporting files. // supporting files.
func (u *Updater) replace(ctx context.Context) (err error) { func (u *Updater) replace() error {
err = u.copySupportingFiles(ctx, u.unpackedFiles, u.updateDir, u.workDir) err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
if err != nil { if err != nil {
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %w", u.updateDir, u.workDir, err) return fmt.Errorf("copySupportingFiles(%s, %s) failed: %w", u.updateDir, u.workDir, err)
} }
u.logger.InfoContext( log.Debug("updater: renaming: %s to %s", u.currentExeName, u.backupExeName)
ctx,
"backing up current executable",
"from", u.currentExeName,
"to", u.backupExeName,
)
err = os.Rename(u.currentExeName, u.backupExeName) err = os.Rename(u.currentExeName, u.backupExeName)
if err != nil { if err != nil {
return err return err
@@ -340,22 +311,14 @@ func (u *Updater) replace(ctx context.Context) (err error) {
return err return err
} }
u.logger.InfoContext( log.Debug("updater: renamed: %s to %s", u.updateExeName, u.currentExeName)
ctx,
"replacing current executable",
"from", u.updateExeName,
"to", u.currentExeName,
)
return nil return nil
} }
// clean removes the temporary directory itself and all it's contents. // clean removes the temporary directory itself and all it's contents.
func (u *Updater) clean(ctx context.Context) { func (u *Updater) clean() {
err := os.RemoveAll(u.updateDir) _ = os.RemoveAll(u.updateDir)
if err != nil {
u.logger.WarnContext(ctx, "removing update dir", slogutil.KeyError, err)
}
} }
// MaxPackageFileSize is a maximum package file length in bytes. The largest // MaxPackageFileSize is a maximum package file length in bytes. The largest
@@ -364,52 +327,34 @@ func (u *Updater) clean(ctx context.Context) {
const MaxPackageFileSize = 32 * 1024 * 1024 const MaxPackageFileSize = 32 * 1024 * 1024
// Download package file and save it to disk // Download package file and save it to disk
func (u *Updater) downloadPackageFile(ctx context.Context) (err error) { func (u *Updater) downloadPackageFile() (err error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.packageURL, nil) var resp *http.Response
resp, err = u.client.Get(u.packageURL)
if err != nil { if err != nil {
return fmt.Errorf("constructing package request: %w", err) return fmt.Errorf("http request failed: %w", err)
}
resp, err := u.client.Do(req)
if err != nil {
return fmt.Errorf("requesting package: %w", err)
} }
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }() defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
r := ioutil.LimitReader(resp.Body, MaxPackageFileSize) r := ioutil.LimitReader(resp.Body, MaxPackageFileSize)
u.logger.InfoContext(ctx, "reading http body") log.Debug("updater: reading http body")
// This use of ReadAll is now safe, because we limited body's Reader. // This use of ReadAll is now safe, because we limited body's Reader.
body, err := io.ReadAll(r) body, err := io.ReadAll(r)
if err != nil { if err != nil {
return fmt.Errorf("io.ReadAll() failed: %w", err) return fmt.Errorf("io.ReadAll() failed: %w", err)
} }
err = os.Mkdir(u.updateDir, aghos.DefaultPermDir) _ = os.Mkdir(u.updateDir, aghos.DefaultPermDir)
if err != nil {
// TODO(a.garipov): Consider returning this error.
u.logger.WarnContext(ctx, "creating update dir", slogutil.KeyError, err)
}
u.logger.InfoContext(ctx, "saving package", "to", u.packageName)
log.Debug("updater: saving package to file")
err = os.WriteFile(u.packageName, body, aghos.DefaultPermFile) err = os.WriteFile(u.packageName, body, aghos.DefaultPermFile)
if err != nil { if err != nil {
return fmt.Errorf("writing package file: %w", err) return fmt.Errorf("writing package file: %w", err)
} }
return nil return nil
} }
// unpackTarGzFile unpacks one file from a .tar.gz archive into outDir. All func tarGzFileUnpackOne(outDir string, tr *tar.Reader, hdr *tar.Header) (name string, err error) {
// arguments must not be empty.
func (u *Updater) unpackTarGzFile(
ctx context.Context,
outDir string,
tr *tar.Reader,
hdr *tar.Header,
) (name string, err error) {
name = filepath.Base(hdr.Name) name = filepath.Base(hdr.Name)
if name == "" { if name == "" {
return "", nil return "", nil
@@ -432,18 +377,13 @@ func (u *Updater) unpackTarGzFile(
return "", fmt.Errorf("creating directory %q: %w", outName, err) return "", fmt.Errorf("creating directory %q: %w", outName, err)
} }
u.logger.InfoContext(ctx, "created directory", "name", outName) log.Debug("updater: created directory %q", outName)
return "", nil return "", nil
} }
if hdr.Typeflag != tar.TypeReg { if hdr.Typeflag != tar.TypeReg {
u.logger.WarnContext( log.Info("updater: %s: unknown file type %d, skipping", name, hdr.Typeflag)
ctx,
"unknown file type; skipping",
"file_name", name,
"type", hdr.Typeflag,
)
return "", nil return "", nil
} }
@@ -460,19 +400,16 @@ func (u *Updater) unpackTarGzFile(
return "", fmt.Errorf("io.Copy(): %w", err) return "", fmt.Errorf("io.Copy(): %w", err)
} }
u.logger.InfoContext(ctx, "created file", "name", outName) log.Debug("updater: created file %q", outName)
return name, nil return name, nil
} }
// unpackTarGz unpack all files from a .tar.gz archive to outDir. Existing // Unpack all files from .tar.gz file to the specified directory
// files are overwritten. All files are created inside outDir. files are the // Existing files are overwritten
// list of created files. // All files are created inside outDir, subdirectories are not created
func (u *Updater) unpackTarGz( // Return the list of files (not directories) written
ctx context.Context, func tarGzFileUnpack(tarfile, outDir string) (files []string, err error) {
tarfile string,
outDir string,
) (files []string, err error) {
f, err := os.Open(tarfile) f, err := os.Open(tarfile)
if err != nil { if err != nil {
return nil, fmt.Errorf("os.Open(): %w", err) return nil, fmt.Errorf("os.Open(): %w", err)
@@ -500,7 +437,7 @@ func (u *Updater) unpackTarGz(
} }
var name string var name string
name, err = u.unpackTarGzFile(ctx, outDir, tarReader, hdr) name, err = tarGzFileUnpackOne(outDir, tarReader, hdr)
if name != "" { if name != "" {
files = append(files, name) files = append(files, name)
@@ -510,13 +447,7 @@ func (u *Updater) unpackTarGz(
return files, err return files, err
} }
// unpackZipFile unpacks one file from a .zip archive into outDir. All func zipFileUnpackOne(outDir string, zf *zip.File) (name string, err error) {
// arguments must not be empty.
func (u *Updater) unpackZipFile(
ctx context.Context,
outDir string,
zf *zip.File,
) (name string, err error) {
var rc io.ReadCloser var rc io.ReadCloser
rc, err = zf.Open() rc, err = zf.Open()
if err != nil { if err != nil {
@@ -535,8 +466,7 @@ func (u *Updater) unpackZipFile(
if name == "AdGuardHome" { if name == "AdGuardHome" {
// Top-level AdGuardHome/. Skip it. // Top-level AdGuardHome/. Skip it.
// //
// TODO(a.garipov): See the similar TODO in // TODO(a.garipov): See the similar todo in tarGzFileUnpack.
// [Updater.unpackTarGzFile].
return "", nil return "", nil
} }
@@ -545,7 +475,7 @@ func (u *Updater) unpackZipFile(
return "", fmt.Errorf("creating directory %q: %w", outputName, err) return "", fmt.Errorf("creating directory %q: %w", outputName, err)
} }
u.logger.InfoContext(ctx, "created directory", "name", outputName) log.Debug("updater: created directory %q", outputName)
return "", nil return "", nil
} }
@@ -562,19 +492,16 @@ func (u *Updater) unpackZipFile(
return "", fmt.Errorf("io.Copy(): %w", err) return "", fmt.Errorf("io.Copy(): %w", err)
} }
u.logger.InfoContext(ctx, "created file", "name", outputName) log.Debug("updater: created file %q", outputName)
return name, nil return name, nil
} }
// unpackZip unpack all files from a .zip archive to outDir. Existing files are // Unpack all files from .zip file to the specified directory
// overwritten. All files are created inside outDir. files are the list of // Existing files are overwritten
// created files. // All files are created inside 'outDir', subdirectories are not created
func (u *Updater) unpackZip( // Return the list of files (not directories) written
ctx context.Context, func zipFileUnpack(zipfile, outDir string) (files []string, err error) {
zipfile string,
outDir string,
) (files []string, err error) {
zrc, err := zip.OpenReader(zipfile) zrc, err := zip.OpenReader(zipfile)
if err != nil { if err != nil {
return nil, fmt.Errorf("zip.OpenReader(): %w", err) return nil, fmt.Errorf("zip.OpenReader(): %w", err)
@@ -583,7 +510,7 @@ func (u *Updater) unpackZip(
for _, zf := range zrc.File { for _, zf := range zrc.File {
var name string var name string
name, err = u.unpackZipFile(ctx, outDir, zf) name, err = zipFileUnpackOne(outDir, zf)
if err != nil { if err != nil {
break break
} }
@@ -616,12 +543,7 @@ func copyFile(src, dst string, perm fs.FileMode) (err error) {
// copySupportingFiles copies each file specified in files from srcdir to // copySupportingFiles copies each file specified in files from srcdir to
// dstdir. If a file specified as a path, only the name of the file is used. // dstdir. If a file specified as a path, only the name of the file is used.
// It skips AdGuardHome, AdGuardHome.exe, and AdGuardHome.yaml. // It skips AdGuardHome, AdGuardHome.exe, and AdGuardHome.yaml.
func (u *Updater) copySupportingFiles( func copySupportingFiles(files []string, srcdir, dstdir string) error {
ctx context.Context,
files []string,
srcdir string,
dstdir string,
) (err error) {
for _, f := range files { for _, f := range files {
_, name := filepath.Split(f) _, name := filepath.Split(f)
if name == "AdGuardHome" || name == "AdGuardHome.exe" || name == "AdGuardHome.yaml" { if name == "AdGuardHome" || name == "AdGuardHome.exe" || name == "AdGuardHome.yaml" {
@@ -631,12 +553,12 @@ func (u *Updater) copySupportingFiles(
src := filepath.Join(srcdir, name) src := filepath.Join(srcdir, name)
dst := filepath.Join(dstdir, name) dst := filepath.Join(dstdir, name)
err = copyFile(src, dst, aghos.DefaultPermFile) err := copyFile(src, dst, aghos.DefaultPermFile)
if err != nil && !errors.Is(err, os.ErrNotExist) { if err != nil && !errors.Is(err, os.ErrNotExist) {
return err return err
} }
u.logger.InfoContext(ctx, "copied", "from", src, "to", dst) log.Debug("updater: copied: %q to %q", src, dst)
} }
return nil return nil

View File

@@ -1,16 +1,12 @@
package updater package updater
import ( import (
"context"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -59,7 +55,6 @@ func TestUpdater_internal(t *testing.T) {
u := NewUpdater(&Config{ u := NewUpdater(&Config{
Client: fakeClient, Client: fakeClient,
Logger: slogutil.NewDiscardLogger(),
GOOS: tc.os, GOOS: tc.os,
Version: "v0.103.0", Version: "v0.103.0",
ExecPath: exePath, ExecPath: exePath,
@@ -73,13 +68,13 @@ func TestUpdater_internal(t *testing.T) {
u.newVersion = "v0.103.1" u.newVersion = "v0.103.1"
u.packageURL = fakeURL.String() u.packageURL = fakeURL.String()
require.NoError(t, u.prepare(newCtx(t))) require.NoError(t, u.prepare())
require.NoError(t, u.downloadPackageFile(newCtx(t))) require.NoError(t, u.downloadPackageFile())
require.NoError(t, u.unpack(newCtx(t))) require.NoError(t, u.unpack())
require.NoError(t, u.backup(newCtx(t), false)) require.NoError(t, u.backup(false))
require.NoError(t, u.replace(newCtx(t))) require.NoError(t, u.replace())
u.clean(newCtx(t)) u.clean()
require.True(t, t.Run("backup", func(t *testing.T) { require.True(t, t.Run("backup", func(t *testing.T) {
var d []byte var d []byte
@@ -118,8 +113,3 @@ func TestUpdater_internal(t *testing.T) {
})) }))
} }
} }
// newCtx is a helper that returns a new context with a timeout.
func newCtx(tb testing.TB) (ctx context.Context) {
return testutil.ContextWithTimeout(tb, 1*time.Second)
}

View File

@@ -10,21 +10,17 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/AdGuardHome/internal/updater"
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// testTimeout is the common timeout for tests. func TestMain(m *testing.M) {
const testTimeout = 1 * time.Second testutil.DiscardLogOutput(m)
}
// testLogger is the common logger for tests.
var testLogger = slogutil.NewDiscardLogger()
func TestUpdater_Update(t *testing.T) { func TestUpdater_Update(t *testing.T) {
const jsonData = `{ const jsonData = `{
@@ -77,7 +73,6 @@ func TestUpdater_Update(t *testing.T) {
u := updater.NewUpdater(&updater.Config{ u := updater.NewUpdater(&updater.Config{
Client: srv.Client(), Client: srv.Client(),
Logger: testLogger,
GOARCH: "amd64", GOARCH: "amd64",
GOOS: "linux", GOOS: "linux",
Version: "v0.103.0", Version: "v0.103.0",
@@ -87,12 +82,10 @@ func TestUpdater_Update(t *testing.T) {
VersionCheckURL: versionCheckURL, VersionCheckURL: versionCheckURL,
}) })
ctx := testutil.ContextWithTimeout(t, testTimeout) _, err = u.VersionInfo(false)
_, err = u.VersionInfo(ctx, false)
require.NoError(t, err) require.NoError(t, err)
ctx = testutil.ContextWithTimeout(t, testTimeout) err = u.Update(true)
err = u.Update(ctx, true)
require.NoError(t, err) require.NoError(t, err)
// check backup files // check backup files
@@ -131,15 +124,14 @@ func TestUpdater_Update(t *testing.T) {
t.Skip("skipping config check test on windows") t.Skip("skipping config check test on windows")
} }
err = u.Update(testutil.ContextWithTimeout(t, testTimeout), false) err = u.Update(false)
assert.NoError(t, err) assert.NoError(t, err)
}) })
t.Run("api_fail", func(t *testing.T) { t.Run("api_fail", func(t *testing.T) {
srv.Close() srv.Close()
err = u.Update(testutil.ContextWithTimeout(t, testTimeout), true) err = u.Update(true)
var urlErr *url.Error var urlErr *url.Error
assert.ErrorAs(t, err, &urlErr) assert.ErrorAs(t, err, &urlErr)
}) })

View File

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