Compare commits

..

1 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

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

4
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -7,8 +7,10 @@ import (
"net/url"
"path/filepath"
"testing"
"time"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -33,13 +35,20 @@ func (w *testResponseWriter) WriteHeader(statusCode int) {
}
func TestAuthHTTP(t *testing.T) {
var (
ctx = testutil.ContextWithTimeout(t, testTimeout)
logger = slogutil.NewDiscardLogger()
err error
)
dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db")
users := []webUser{
{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
handler := func(_ http.ResponseWriter, _ *http.Request) {
@@ -68,7 +77,11 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled)
// 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.NotNil(t, cookie)
@@ -114,7 +127,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled)
r.Header.Del(httphdr.Cookie)
globalContext.auth.Close()
globalContext.auth.Close(ctx)
}
func TestRealIP(t *testing.T) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -392,6 +392,8 @@ const PasswordMinRunes = 8
// Apply new configuration, start DNS server, restart Web server
func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, restartHTTP, err := decodeApplyConfigReq(r.Body)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -439,7 +441,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
u := &webUser{
Name: req.Username,
}
err = globalContext.auth.addUser(u, req.Password)
err = globalContext.auth.addUser(ctx, u, req.Password)
if err != nil {
globalContext.firstRun = true
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
// configuration itself will use HTTPS protocol, because the underlying
// functions potentially restart the HTTPS server.
err = startMods(r.Context(), web.baseLogger, web.tlsManager)
err = startMods(ctx, web.baseLogger, web.tlsManager)
if err != nil {
globalContext.firstRun = true
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 will be blocked by it's own caller.
go func(timeout time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer slogutil.RecoverAndLog(ctx, web.logger)
shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer slogutil.RecoverAndLog(shutdownCtx, web.logger)
defer cancel()
shutdownSrv(ctx, web.logger, web.httpServer)
shutdownSrv(shutdownCtx, web.logger, web.httpServer)
}(shutdownTimeout)
}

View File

@@ -347,6 +347,13 @@ func newDNSTLSConfig(
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
return dnsConf, nil

View File

@@ -668,7 +668,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
GLMode = opts.glinetMode
// Init auth module.
globalContext.auth, err = initUsers()
globalContext.auth, err = initUsers(ctx, slogLogger)
fatalOnError(err)
web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
@@ -786,7 +786,8 @@ func checkPermissions(
}
// 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")
var rateLimiter *authRateLimiter
@@ -799,10 +800,17 @@ func initUsers() (auth *Auth, err error) {
trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies))
sessionTTL := time.Duration(config.HTTPConfig.SessionTTL).Seconds()
auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter, trustedProxies)
if auth == nil {
return nil, errors.Error("initializing auth module failed")
auth, err = InitAuth(
ctx,
baseLogger,
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
@@ -916,7 +924,7 @@ func cleanup(ctx context.Context) {
globalContext.web = nil
}
if globalContext.auth != nil {
globalContext.auth.Close()
globalContext.auth.Close(ctx)
globalContext.auth = nil
}

View File

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

View File

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

View File

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

View File

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