diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index d969343d..d1010108 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -1,7 +1,8 @@ Before submitting a PR please make sure that: 1. You have discussed your solution in an issue and have got an - approval from a maintainer. + approval from a maintainer. See our + [contribution guide](https://github.com/AdguardTeam/AdGuardHome/blob/master/CONTRIBUTING.md). 2. This isn't a localization fix; please send those to our [CrowdIn](https://crowdin.com/project/adguard-applications/en#/adguard-home) @@ -13,8 +14,8 @@ Before submitting a PR please make sure that: Add a short description here. The description should include: 1. Which issue this PR closes (`Closes #NNNN.`) or updates (`Updates - #NNNN.`). + #NNNN.`). Please do not open PRs without filing an issue first. 2. A short description of how the change achieves that. -Do not forget to remove these instructions. +Do not forget to remove these instructions! diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1c35913..8a3af2fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ 'name': 'build' 'env': - 'GO_VERSION': '1.20.11' + 'GO_VERSION': '1.20.12' 'NODE_VERSION': '16' 'on': diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 144cc3f4..1ba27d28 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,7 @@ 'name': 'lint' 'env': - 'GO_VERSION': '1.20.11' + 'GO_VERSION': '1.20.12' 'on': 'push': diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b79829..52129c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,11 @@ and this project adheres to @@ -29,6 +29,52 @@ NOTE: Add new changes ABOVE THIS COMMENT. +## [v0.107.42] - 2023-12-07 + +See also the [v0.107.42 GitHub milestone][ms-v0.107.42]. + +### Security + +- Go version has been updated to prevent the possibility of exploiting the + CVE-2023-39326, CVE-2023-45283, and CVE-2023-45285 Go vulnerabilities fixed in + [Go 1.20.12][go-1.20.12]. + +### Added + +- Ability to set client's custom DNS cache ([#6362]). +- Ability to disable plain-DNS serving through configuration file if an + encrypted protocol is already enabled ([#1660]). +- Ability to specify rate limiting settings in the Web UI ([#6369]). + +### Changed + +#### Configuration changes + +- The new property `dns.serve_plain_dns` has been added to the configuration + file ([#1660]). +- The property `dns.bogus_nxdomain` is now validated more strictly. +- Added new properties `clients.persistent.*.upstreams_cache_enabled` and + `clients.persistent.*.upstreams_cache_size` that describe cache configuration + for each client's custom upstream configuration. + +### Fixed + +- `ipset` entries family validation ([#6420]). +- Pre-filling the *New static lease* window with data ([#6402]). +- Protection pause timer synchronization ([#5759]). + +[#1660]: https://github.com/AdguardTeam/AdGuardHome/issues/1660 +[#5759]: https://github.com/AdguardTeam/AdGuardHome/issues/5759 +[#6362]: https://github.com/AdguardTeam/AdGuardHome/issues/6362 +[#6369]: https://github.com/AdguardTeam/AdGuardHome/issues/6369 +[#6402]: https://github.com/AdguardTeam/AdGuardHome/issues/6402 +[#6420]: https://github.com/AdguardTeam/AdGuardHome/issues/6420 + +[go-1.20.12]: https://groups.google.com/g/golang-announce/c/iLGK3x6yuNo/m/z6MJ-eB0AQAJ +[ms-v0.107.42]: https://github.com/AdguardTeam/AdGuardHome/milestone/77?closed=1 + + + ## [v0.107.41] - 2023-11-13 See also the [v0.107.41 GitHub milestone][ms-v0.107.41]. @@ -2612,11 +2658,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2]. -[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.41...HEAD +[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.42...HEAD +[v0.107.42]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.41...v0.107.42 [v0.107.41]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...v0.107.41 [v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...v0.107.40 [v0.107.39]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.38...v0.107.39 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ffcda283 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ + # Contributing to AdGuard Home + +If you want to contribute to AdGuard Home by filing or commenting on an issue or +opening a pull request, please follow the instructions below. + + + +## General recommendations + +Please don't: + + * post comments like “+1” or “this”. Use the :+1: reaction on the issue + instead, as this allows us to actually see the level of support for issues. + + * file issues about localization errors or send localization updates as PRs. + We're using [CrowdIn] to manage our translations and we generally update + them before each Beta and Release build. You can learn more about + translating AdGuard products [in our Knowledge Base][kb-trans]. + + * file issues about a particular filtering-rule list misbehaving. These are + tracked through the [separate form for filtering issues][form]. + + * send updates to filtering-rule lists, such as the ones for the Blocked + Services feature or the list of approved filtering-rule lists. We update + them once before each Beta and Release build. + +Please do: + + * follow the template instructions and provide data for reproducing issues. + + * write the title of your issue or pull request in English. Any language is + fine in the body, but it is important to keep the title in English to make + it easier for people and bots to look up duplicated issues. + +[CrowdIn]: https://crowdin.com/project/adguard-applications/en#/adguard-home +[form]: https://link.adtidy.org/forward.html?action=report&app=home&from=github +[kb-trans]: https://kb.adguard.com/en/general/adguard-translations + + + +## Issues + + ### Search first + +Please make sure that the issue is not a duplicate or a question. If it's a +duplicate, please react to the original issue with a thumbs up. If it's a +question, please look through our [Wiki] and, if you haven't found the answer, +post it to the GitHub [Discussions] page. + +[Discussions]: https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a +[Wiki]: https://github.com/AdguardTeam/AdGuardHome/wiki + + + + ### Follow the issue template + +Developers need to be able to reproduce the faulty behavior in order to fix an +issue, so please make sure that you follow the instructions in the issue +template carefully. + + + +## Pull requests + + ### Discuss your changes first + +Please discuss your changes by opening an issue. The maintainers should +evaluate your proposal, and it's generally better if that's done before any code +is written. + + + + ### Review your changes for style + +We have a set of [code guidelines][hacking] that we expect the code to follow. +Please make sure you follow it. + +[hacking]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md + + + + ### Test your changes + +Make sure that it passes linters and tests by running the corresponding Make +targets. For backend changes, it's `make go-check`. For frontend, run +`make js-lint`. + +Additionally, a manual test is often required. While we're constantly working +on improving our test suites, they're still not as good as we'd like them to be. diff --git a/bamboo-specs/release.yaml b/bamboo-specs/release.yaml index 8743a21a..e7004570 100644 --- a/bamboo-specs/release.yaml +++ b/bamboo-specs/release.yaml @@ -7,7 +7,7 @@ # Make sure to sync any changes with the branch overrides below. 'variables': 'channel': 'edge' - 'dockerGo': 'adguard/golang-ubuntu:7.5' + 'dockerGo': 'adguard/golang-ubuntu:7.6' 'stages': - 'Build frontend': @@ -272,7 +272,7 @@ # need to build a few of these. 'variables': 'channel': 'beta' - 'dockerGo': 'adguard/golang-ubuntu:7.5' + 'dockerGo': 'adguard/golang-ubuntu:7.6' # release-vX.Y.Z branches are the branches from which the actual final # release is built. - '^release-v[0-9]+\.[0-9]+\.[0-9]+': @@ -287,4 +287,4 @@ # are the ones that actually get released. 'variables': 'channel': 'release' - 'dockerGo': 'adguard/golang-ubuntu:7.5' + 'dockerGo': 'adguard/golang-ubuntu:7.6' diff --git a/bamboo-specs/snapcraft.yaml b/bamboo-specs/snapcraft.yaml index d802dba0..77948f13 100644 --- a/bamboo-specs/snapcraft.yaml +++ b/bamboo-specs/snapcraft.yaml @@ -10,7 +10,7 @@ # Make sure to sync any changes with the branch overrides below. 'variables': 'channel': 'edge' - 'dockerGo': 'adguard/golang-ubuntu:7.5' + 'dockerGo': 'adguard/golang-ubuntu:7.6' 'snapcraftChannel': 'edge' 'stages': @@ -191,7 +191,7 @@ # need to build a few of these. 'variables': 'channel': 'beta' - 'dockerGo': 'adguard/golang-ubuntu:7.5' + 'dockerGo': 'adguard/golang-ubuntu:7.6' 'snapcraftChannel': 'beta' # release-vX.Y.Z branches are the branches from which the actual final # release is built. @@ -207,5 +207,5 @@ # are the ones that actually get released. 'variables': 'channel': 'release' - 'dockerGo': 'adguard/golang-ubuntu:7.5' + 'dockerGo': 'adguard/golang-ubuntu:7.6' 'snapcraftChannel': 'candidate' diff --git a/bamboo-specs/test.yaml b/bamboo-specs/test.yaml index 746b599d..86e08cc4 100644 --- a/bamboo-specs/test.yaml +++ b/bamboo-specs/test.yaml @@ -5,7 +5,7 @@ 'key': 'AHBRTSPECS' 'name': 'AdGuard Home - Build and run tests' 'variables': - 'dockerGo': 'adguard/golang-ubuntu:7.5' + 'dockerGo': 'adguard/golang-ubuntu:7.6' 'stages': - 'Tests': diff --git a/client/src/__locales/be.json b/client/src/__locales/be.json index b6ed5cdd..ad13bfac 100644 --- a/client/src/__locales/be.json +++ b/client/src/__locales/be.json @@ -1,6 +1,7 @@ { "client_settings": "Налады кліентаў", "example_upstream_reserved": "upstream <0>для канкрэтных даменаў;", + "example_multiple_upstreams_reserved": "некалькі DNS-сервераў <0>для канкрэтных даменаў;", "example_upstream_comment": "каментар.", "upstream_parallel": "Ужыць адначасныя запыты да ўсіх сервераў для паскарэння апрацоўкі запыту", "parallel_requests": "Паралельныя запыты", @@ -143,6 +144,8 @@ "enforced_save_search": "Ужыты бяспечны пошук", "number_of_dns_query_to_safe_search": "Колькасць запытаў DNS для пошукавых сістэм, для якіх быў ужыты Бяспечны пошук", "average_processing_time": "Сярэдні час апрацоўкі запыту", + "average_upstream_response_time": "Сярэдні час водгуку upstream-сервера", + "response_time": "Час водгуку", "average_processing_time_hint": "Сярэдні час для апрацоўкі запыту DNS у мілісекундах", "block_domain_use_filters_and_hosts": "Блакаваць дамены з выкарыстаннем фільтраў і файлаў хастоў", "filters_block_toggle_hint": "Вы можаце наладзіць правілы блакавання ў «Фільтрах».", @@ -307,6 +310,16 @@ "edns_use_custom_ip": "Выкарыстоўваць указаны IP для DNS", "edns_use_custom_ip_desc": "Дазволіць выкарыстоўваць уласны IP для DNS", "rate_limit_desc": "Абмежаванне на колькасць запытаў у секунду для кожнага кліента (0 — неабмежавана)", + "rate_limit_subnet_len_ipv4": "Даўжыня прэфікса падсеткі для адрасоў IPv4", + "rate_limit_subnet_len_ipv4_desc": "Даўжыня прэфікса падсеткі для адрасоў IPv4, якія выкарыстоўваюцца для абмежавання хуткасці. Значэнне па змаўчанні 24", + "rate_limit_subnet_len_ipv4_error": "Даўжыня прэфікса падсеткі IPv4 павінна быць ад 0 да 32", + "rate_limit_subnet_len_ipv6": "Даўжыня прэфікса падсеткі для адрасоў IPv6", + "rate_limit_subnet_len_ipv6_desc": "Даўжыня прэфікса падсеткі для адрасоў IPv6, якія выкарыстоўваюцца для абмежавання хуткасці. Значэнне па змаўчанні 56", + "rate_limit_subnet_len_ipv6_error": "Даўжыня прэфікса падсеткі IPv6 павінна быць ад 0 да 128", + "form_enter_rate_limit_subnet_len": "Увядзіце даўжыню прэфікса падсеткі для абмежавання хуткасці", + "rate_limit_whitelist": "Белы спіс з абмежаваннем хуткасці", + "rate_limit_whitelist_desc": "IP-адрасы выключаны з абмежавання хуткасці", + "rate_limit_whitelist_placeholder": "Увядзіце па адным адрасе на радок", "blocking_ipv4_desc": "IP-адрас, што вяртаецца пры блакаванню A-запыту", "blocking_ipv6_desc": "IP-адрас, што вяртаецца пры блакаванню AAAA-запыту", "blocking_mode_default": "Стандартны: Адказвае з нулёвым IP-адрасам (0.0.0.0 для A; :: для AAAA), калі заблакавана правілам у стылі Adblock; адказвае з IP-адрасам, паказаным у правіле, калі заблакавана правілам у стылі /etc/hosts-style", @@ -721,5 +734,8 @@ "wednesday_short": "Ср.", "thursday_short": "Чц.", "friday_short": "Пт.", - "saturday_short": "Сб." + "saturday_short": "Сб.", + "upstream_dns_cache_configuration": "Канфігурацыя кэша upstream DNS-сервераў", + "enable_upstream_dns_cache": "Ўключыць кэшаванне для карыстацкай канфігурацыі upstream-сервераў гэтага кліента", + "dns_cache_size": "Памер кэша DNS, у байтах" } diff --git a/client/src/__locales/cs.json b/client/src/__locales/cs.json index 6122dec8..5d02697f 100644 --- a/client/src/__locales/cs.json +++ b/client/src/__locales/cs.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Použít vlastní IP pro EDNS", "edns_use_custom_ip_desc": "Povolit použití vlastní IP pro EDNS", "rate_limit_desc": "Počet požadavků za sekundu, které smí jeden klient provádět (0: neomezeno)", + "rate_limit_subnet_len_ipv4": "Délka předpony podsítě pro adresy IPv4", + "rate_limit_subnet_len_ipv4_desc": "Délka předpony podsítě pro adresy IPv4 používané pro omezení rychlosti. Výchozí hodnota je 24", + "rate_limit_subnet_len_ipv4_error": "Délka předpony podsítě IPv4 by měla být mezi 0 a 32", + "rate_limit_subnet_len_ipv6": "Délka předpony podsítě pro adresy IPv6", + "rate_limit_subnet_len_ipv6_desc": "Délka předpony podsítě pro adresy IPv6 používané pro omezení rychlosti. Výchozí hodnota je 56", + "rate_limit_subnet_len_ipv6_error": "Délka předpony podsítě IPv6 by měla být mezi 0 a 128", + "form_enter_rate_limit_subnet_len": "Zadejte délku předpony podsítě pro omezení rychlosti", + "rate_limit_whitelist": "Seznam výjimek pro omezení rychlosti", + "rate_limit_whitelist_desc": "IP adresy vyloučené z omezení rychlosti", + "rate_limit_whitelist_placeholder": "Zadejte jednu IP adresu na řádek", "blocking_ipv4_desc": "IP adresa, která se má vrátit v případě blokovaného požadavku typu A", "blocking_ipv6_desc": "IP adresa, která se má vrátit v případě blokovaného požadavku typu AAAA", "blocking_mode_default": "Výchozí: Odezva s nulovou IP adresou (0.0.0.0 pro A; :: pro AAAA), pokud je blokováno pravidlem ve stylu Adblock; odezva pomocí IP adresy uvedené v pravidle, pokud je blokováno pravidlem /etc/hosts-style", @@ -724,5 +734,8 @@ "wednesday_short": "Středa", "thursday_short": "Čtvrtek", "friday_short": "Pátek", - "saturday_short": "Sobota" + "saturday_short": "Sobota", + "upstream_dns_cache_configuration": "Konfigurace mezipaměti odchozího DNS", + "enable_upstream_dns_cache": "Povolit ukládání do mezipaměti DNS pro vlastní konfiguraci odchozího připojení tohoto klienta", + "dns_cache_size": "Velikost mezipaměti DNS v bajtech" } diff --git a/client/src/__locales/da.json b/client/src/__locales/da.json index dcf6722c..005e60d7 100644 --- a/client/src/__locales/da.json +++ b/client/src/__locales/da.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Brug tilpasset IP til EDNS", "edns_use_custom_ip_desc": "Tillad brug af tilpasset IP til EDNS", "rate_limit_desc": "Antallet af forespørgsler pr. sekund tilladt pr. klient (værdien 0 = ubegrænset)", + "rate_limit_subnet_len_ipv4": "Længde på undernetpræfiks for IPv4-adresser", + "rate_limit_subnet_len_ipv4_desc": "Længde på undernetpræfiks for IPv4-adresser til hastighedsbegrænsning. Standard er 24", + "rate_limit_subnet_len_ipv4_error": "Længden på IPv4-undernetpræfiks skal være mellem 0 og 32", + "rate_limit_subnet_len_ipv6": "Længde på undernetpræfiks for IPv6-adresser", + "rate_limit_subnet_len_ipv6_desc": "Længde på undernetpræfiks for IPv6-adresser til hastighedsbegrænsning. Standard er 56", + "rate_limit_subnet_len_ipv6_error": "Længden på IPv6-undernetpræfiks skal være mellem 0 og 128", + "form_enter_rate_limit_subnet_len": "Angiv længden på undernetpræfiks til hastighedsbegrænsning", + "rate_limit_whitelist": "Hvidliste til hastighedsbegrænsning", + "rate_limit_whitelist_desc": "IP-adresser undtaget fra hastighedsbegrænsning", + "rate_limit_whitelist_placeholder": "Angiv én IP-adresse pr. linje", "blocking_ipv4_desc": "Returneret IP-adresse for en blokeret A-forespørgsel", "blocking_ipv6_desc": "Returneret IP-adresse for en blokeret AAAA-forespørgsel", "blocking_mode_default": "Standard: Svar med nul IP-adresse (0.0.0.0 for A; :: for AAAA), når blokeret af Adblock-lignende regel. Svar med IP-adressen angivet i reglen, når blokeret af /etc/hosts-lignende regel", @@ -724,5 +734,8 @@ "wednesday_short": "Ons", "thursday_short": "Tors", "friday_short": "Fre", - "saturday_short": "Lør" + "saturday_short": "Lør", + "upstream_dns_cache_configuration": "Upstream DNS-cacheopsætning", + "enable_upstream_dns_cache": "Aktivér DNS-cachelagring for denne klients tilpassede upstream-opsætning", + "dns_cache_size": "DNS-cachestørrelse i bytes" } diff --git a/client/src/__locales/de.json b/client/src/__locales/de.json index e9ed256c..bf104daf 100644 --- a/client/src/__locales/de.json +++ b/client/src/__locales/de.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Benutzerdefinierte IP für EDNS verwenden", "edns_use_custom_ip_desc": "Benutzerdefinierte IP für EDNS zulassen", "rate_limit_desc": "Die Anzahl der Anfragen pro Sekunde, die ein einzelner Client stellen darf. Das Setzen auf 0 bedeutet keine Begrenzung.", + "rate_limit_subnet_len_ipv4": "Länge des Subnetzpräfixes für IPv4-Adressen", + "rate_limit_subnet_len_ipv4_desc": "Subnetpräfixlänge für IPv4-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 24", + "rate_limit_subnet_len_ipv4_error": "Die Subnetzpräfixlänge für IPv4-Adressen sollte zwischen 0 und 32 liegen", + "rate_limit_subnet_len_ipv6": "Subnetzpräfixlänge für IPv6-Adressen", + "rate_limit_subnet_len_ipv6_desc": "Subnetpräfixlänge für IPv6-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 56", + "rate_limit_subnet_len_ipv6_error": "Die Subnetzpräfixlänge für IPv6-Adressen sollte zwischen 0 und 128 liegen", + "form_enter_rate_limit_subnet_len": "Geben Sie die Subnetzpräfixlänge für die Ratebegrenzung ein", + "rate_limit_whitelist": "Zulassungsliste für die Ratebegrenzung", + "rate_limit_whitelist_desc": "IP-Adressen, die von der Ratebegrenzung ausgeschlossen sind", + "rate_limit_whitelist_placeholder": "Geben Sie eine IP-Adresse pro Zeile ein", "blocking_ipv4_desc": "IP-Adresse, die für eine gesperrte A-Anfrage zurückgegeben werden soll", "blocking_ipv6_desc": "IP-Adresse, die für eine gesperrte AAAA-Anfrage zurückgegeben werden soll", "blocking_mode_default": "Standard: Mit Null IP Adress (0.0.0.0 for A; :: for AAAA) antworten, wenn sie durch eine Regel im Adblock-Stil gesperrt sind; mit der in der Regel angegebenen IP-Adresse antworten, wenn sie durch eine Regel im /etc/hosts-Stil gesperrt wurde", @@ -724,5 +734,8 @@ "wednesday_short": "Mi", "thursday_short": "Do", "friday_short": "Fr", - "saturday_short": "Sa" + "saturday_short": "Sa", + "upstream_dns_cache_configuration": "Konfiguration des Upstream-DNS-Cache", + "enable_upstream_dns_cache": "Caching für die benutzerdefinierte Upstream-Server-Konfiguration dieses Clients aktivieren", + "dns_cache_size": "Größe des DNS-Cache, in Bytes" } diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 5e0bf6d1..a5839c0f 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Use custom IP for EDNS", "edns_use_custom_ip_desc": "Allow to use custom IP for EDNS", "rate_limit_desc": "The number of requests per second allowed per client. Setting it to 0 means no limit.", + "rate_limit_subnet_len_ipv4": "Subnet prefix length for IPv4 addresses", + "rate_limit_subnet_len_ipv4_desc": "Subnet prefix length for IPv4 addresses used for rate limiting. The default is 24", + "rate_limit_subnet_len_ipv4_error": "The IPv4 subnet prefix length should be between 0 and 32", + "rate_limit_subnet_len_ipv6": "Subnet prefix length for IPv6 addresses", + "rate_limit_subnet_len_ipv6_desc": "Subnet prefix length for IPv6 addresses used for rate limiting. The default is 56", + "rate_limit_subnet_len_ipv6_error": "The IPv6 subnet prefix length should be between 0 and 128", + "form_enter_rate_limit_subnet_len": "Enter subnet prefix length for rate limiting", + "rate_limit_whitelist": "Rate limiting allowlist", + "rate_limit_whitelist_desc": "IP addresses excluded from rate limiting", + "rate_limit_whitelist_placeholder": "Enter one IP address per line", "blocking_ipv4_desc": "IP address to be returned for a blocked A request", "blocking_ipv6_desc": "IP address to be returned for a blocked AAAA request", "blocking_mode_default": "Default: Respond with zero IP address (0.0.0.0 for A; :: for AAAA) when blocked by Adblock-style rule; respond with the IP address specified in the rule when blocked by /etc/hosts-style rule", @@ -724,5 +734,8 @@ "wednesday_short": "Wed", "thursday_short": "Thu", "friday_short": "Fri", - "saturday_short": "Sat" + "saturday_short": "Sat", + "upstream_dns_cache_configuration": "Upstream DNS cache configuration", + "enable_upstream_dns_cache": "Enable DNS caching for this client's custom upstream configuration", + "dns_cache_size": "DNS cache size, in bytes" } diff --git a/client/src/__locales/es.json b/client/src/__locales/es.json index badc0c42..c2f56ad3 100644 --- a/client/src/__locales/es.json +++ b/client/src/__locales/es.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Usar IP personalizada para EDNS", "edns_use_custom_ip_desc": "Permitir el uso de IP personalizadas para EDNS", "rate_limit_desc": "Número de peticiones por segundo permitidas por cliente. Establecerlo en 0 significa que no hay límite.", + "rate_limit_subnet_len_ipv4": "Longitud del prefijo de subred para direcciones IPv4", + "rate_limit_subnet_len_ipv4_desc": "Longitud del prefijo de subred para direcciones IPv4 utilizadas para limitar la velocidad. El valor predeterminado es 24", + "rate_limit_subnet_len_ipv4_error": "La longitud del prefijo de subred IPv4 debe estar entre 0 y 32", + "rate_limit_subnet_len_ipv6": "Longitud del prefijo de subred para direcciones IPv6", + "rate_limit_subnet_len_ipv6_desc": "Longitud del prefijo de subred para direcciones IPv6 utilizadas para limitar la velocidad. El valor predeterminado es 56", + "rate_limit_subnet_len_ipv6_error": "La longitud del prefijo de subred IPv6 debe estar entre 0 y 128", + "form_enter_rate_limit_subnet_len": "Ingresa la longitud del prefijo de subred para limitar la velocidad", + "rate_limit_whitelist": "Lista de permitidos de limitación de velocidad", + "rate_limit_whitelist_desc": "Direcciones IP excluidas de la limitación de velocidad", + "rate_limit_whitelist_placeholder": "Ingresa una dirección IP por línea", "blocking_ipv4_desc": "Dirección IP devolverá una petición A bloqueada", "blocking_ipv6_desc": "Dirección IP devolverá una petición AAAA bloqueada", "blocking_mode_default": "Predeterminado: Responde con dirección IP cero (0.0.0.0 para A; :: para AAAA) cuando está bloqueado por la regla de estilo Adblock; responde con la dirección IP especificada en la regla cuando está bloqueado por una regla de estilo /etc/hosts", @@ -724,5 +734,8 @@ "wednesday_short": "Mié.", "thursday_short": "Jue.", "friday_short": "Vie.", - "saturday_short": "Sáb." + "saturday_short": "Sáb.", + "upstream_dns_cache_configuration": "Configuración de la caché DNS upstream", + "enable_upstream_dns_cache": "Habilitar el almacenamiento en caché de DNS para la configuración personalizada de este cliente", + "dns_cache_size": "Tamaño de la caché DNS, en bytes" } diff --git a/client/src/__locales/fi.json b/client/src/__locales/fi.json index e04256c7..3aa68ca5 100644 --- a/client/src/__locales/fi.json +++ b/client/src/__locales/fi.json @@ -1,6 +1,7 @@ { "client_settings": "Päätelaiteasetukset", "example_upstream_reserved": "ylävirta <0>tietyille verkkotunnuksille;", + "example_multiple_upstreams_reserved": "useita ylävirtoja <0>tietyille verkkotunnuksille;", "example_upstream_comment": "kommentti.", "upstream_parallel": "Käytä rinnakkaisia pyyntöjä ja nopeuta selvitystä käyttämällä kaikkia ylävirtapalvelimia samanaikaisesti.", "parallel_requests": "Rinnakkaiset pyynnöt", @@ -143,6 +144,7 @@ "enforced_save_search": "Turvallinen haku pakotettiin", "number_of_dns_query_to_safe_search": "DNS-pyyntöjen määrä, joille turvallinen haku pakotettiin käyttöön", "average_processing_time": "Keskimääräinen käsittelyaika", + "average_upstream_response_time": "Ylävirran keskimääräinen vasteaika", "response_time": "Vasteaika", "average_processing_time_hint": "Keskimääräinen DNS-pyynnön käsittelyyn kulutettu aika millisekunteina", "block_domain_use_filters_and_hosts": "Estä verkkotunnuksia suodattimilla ja hosts-tiedostoilla", @@ -308,6 +310,16 @@ "edns_use_custom_ip": "Käytä omaa IP-osoitetta EDNS:lle", "edns_use_custom_ip_desc": "Salli oman IP-osoitteen käyttö EDNS-mekanismille.", "rate_limit_desc": "Päätelaitteelle sallittu pyyntöjen enimmäismäärä sekunnissa. Arvo 0 tarkoittaa rajatonta.", + "rate_limit_subnet_len_ipv4": "IPv4-osoitteiden aliverkon etuliitteen pituus", + "rate_limit_subnet_len_ipv4_desc": "Aliverkon etuliitteen pituus IPv4-osoitteille, joita käytetään nopeuden rajoittamiseen. Oletusarvo on 24", + "rate_limit_subnet_len_ipv4_error": "IPv4-aliverkon etuliitteen pituuden tulee olla 0–32", + "rate_limit_subnet_len_ipv6": "IPv6-osoitteiden aliverkon etuliitteen pituus", + "rate_limit_subnet_len_ipv6_desc": "Aliverkon etuliitteen pituus IPv6-osoitteille, joita käytetään nopeuden rajoittamiseen. Oletusarvo on 56", + "rate_limit_subnet_len_ipv6_error": "IPv6-aliverkon etuliitteen pituuden tulee olla 0–128", + "form_enter_rate_limit_subnet_len": "Anna aliverkon etuliitteen pituus nopeuden rajoittamista varten", + "rate_limit_whitelist": "Nopeutta rajoittava sallittu luettelo", + "rate_limit_whitelist_desc": "IP-osoitteet, jotka eivät kuulu nopeusrajoituksen piiriin", + "rate_limit_whitelist_placeholder": "Syötä yksi IP-osoite per rivi", "blocking_ipv4_desc": "Estettyyn A-pyyntöön palautettava IP-osoite", "blocking_ipv6_desc": "Estettyyn AAAA-pyyntöön palautettava IP-osoite", "blocking_mode_default": "Oletus: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA) kun estetään mainoseston säännöllä; vastaa säännön määrittämällä IP-osoitteella kun estetään /etc/hosts-tyyppisellä säännöllä", @@ -722,5 +734,8 @@ "wednesday_short": "Ke", "thursday_short": "To", "friday_short": "Pe", - "saturday_short": "La" + "saturday_short": "La", + "upstream_dns_cache_configuration": "Ylävirran DNS-välimuistin määritykset", + "enable_upstream_dns_cache": "Käytä DNS-välimuistia tämän päätelaitteen mukautetuissa ylävirtamäärityksissä", + "dns_cache_size": "DNS-välimuistin koko tavuina" } diff --git a/client/src/__locales/fr.json b/client/src/__locales/fr.json index 2be62963..eb09b830 100644 --- a/client/src/__locales/fr.json +++ b/client/src/__locales/fr.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Utiliser une IP personnalisée pour EDNS", "edns_use_custom_ip_desc": "Autoriser l'utilisation d'une adresse IP personnalisée pour EDNS", "rate_limit_desc": "Le nombre de requêtes par seconde qu’un seul client est autorisé à faire. Le réglage 0 fait illimité.", + "rate_limit_subnet_len_ipv4": "Longueur du préfixe de sous-réseau pour les adresses IPv4", + "rate_limit_subnet_len_ipv4_desc": "Longueur du préfixe de sous-réseau pour les adresses IPv4 utilisé pour la limitation de vitesse. La valeur par défaut est 24", + "rate_limit_subnet_len_ipv4_error": "La longueur du préfixe du sous-réseau IPv4 doit être entre 0 et 32", + "rate_limit_subnet_len_ipv6": "Longueur du préfixe de sous-réseau pour les adresses IPv6", + "rate_limit_subnet_len_ipv6_desc": "Longueur du préfixe de sous-réseau pour les adresses IPv6 utilisé pour la limitation de débit. La valeur par défaut est 56", + "rate_limit_subnet_len_ipv6_error": "La longueur du préfixe du sous-réseau IPv6 doit être entre 0 et 128", + "form_enter_rate_limit_subnet_len": "Saisissez la longueur du préfixe de sous-réseau pour la limitation de débit", + "rate_limit_whitelist": "Liste d'autorisation de limitation de débit", + "rate_limit_whitelist_desc": "Adresses IP exclues de la limitation du débit", + "rate_limit_whitelist_placeholder": "Saisissez une adresse IP par ligne", "blocking_ipv4_desc": "Adresse IP à renvoyer pour une demande A bloquée", "blocking_ipv6_desc": "Adresse IP à renvoyer pour une demande AAAA bloquée", "blocking_mode_default": "Par défaut : Répondre avec adresse IP zéro (0.0.0.0 pour A ; :: pour AAAA) lorsque bloqué par la règle de style Adblock ; répondre avec l’adresse IP spécifiée dans la règle lorsque bloquée par la règle du style /etc/hosts", @@ -724,5 +734,8 @@ "wednesday_short": "Mer.", "thursday_short": "Jeu.", "friday_short": "Ven.", - "saturday_short": "Sam." + "saturday_short": "Sam.", + "upstream_dns_cache_configuration": "Configuration du cache DNS en amont", + "enable_upstream_dns_cache": "Activer la mise en cache pour la configuration personnalisée du serveur en amont de ce client", + "dns_cache_size": "Taille du cache DNS, en bytes" } diff --git a/client/src/__locales/hr.json b/client/src/__locales/hr.json index 1c9ec62e..7fe2c68a 100644 --- a/client/src/__locales/hr.json +++ b/client/src/__locales/hr.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Koristi prilagođeni IP za EDNS", "edns_use_custom_ip_desc": "Dopusti korištenje prilagođenog IP-a za EDNS", "rate_limit_desc": "Broj zahtjeva u sekundi koji su dopušteni po jednom klijentu. Postavljanje na 0 znači neograničeno.", + "rate_limit_subnet_len_ipv4": "Duljina prefiksa podmreže za IPv4 adrese", + "rate_limit_subnet_len_ipv4_desc": "Duljina prefiksa podmreže za IPv4 adrese koje se koriste za ograničavanje brzine. Zadana vrijednost je 24", + "rate_limit_subnet_len_ipv4_error": "Dužina IPv4 prefiksa podmreže trebala bi biti između 0 i 32", + "rate_limit_subnet_len_ipv6": "Duljina prefiksa podmreže za IPv6 adrese", + "rate_limit_subnet_len_ipv6_desc": "Duljina prefiksa podmreže za IPv6 adrese koje se koriste za ograničavanje brzine. Zadana vrijednost je 56", + "rate_limit_subnet_len_ipv6_error": "Dužina IPv6 prefiksa podmreže trebala bi biti između 0 i 128", + "form_enter_rate_limit_subnet_len": "Unesite duljinu prefiksa podmreže za ograničenje brzine", + "rate_limit_whitelist": "Popis dopuštenih za ograničavanje brzine", + "rate_limit_whitelist_desc": "IP adrese isključene iz ograničenja brzine", + "rate_limit_whitelist_placeholder": "Unesite jednu adresu poslužitelja po retku", "blocking_ipv4_desc": "Povratna IP adresa za blokirane A zahtjeve", "blocking_ipv6_desc": "Povratna IP adresa za blokirane AAAA zahtjeve", "blocking_mode_default": "Zadano: Odgovori s nultom IP adresom (0.0.0.0 za A; :: za AAAA) kada ga blokira Adblock slično pravilo; odgovorite s IP adresom definiranom u pravilu kada je blokirano od /etc/hosts sličnog pravila", @@ -724,5 +734,8 @@ "wednesday_short": "Sri", "thursday_short": "Čet", "friday_short": "Pet", - "saturday_short": "Sub" + "saturday_short": "Sub", + "upstream_dns_cache_configuration": "Konfiguracija predmemoriranja upstream DNS poslužitelja", + "enable_upstream_dns_cache": "Uključite keširanje za korisničku konfiguraciju upstream servera ovog klijenta", + "dns_cache_size": "Veličina DNS predmemorije, u bajtovima" } diff --git a/client/src/__locales/hu.json b/client/src/__locales/hu.json index 454704c8..40bf8512 100644 --- a/client/src/__locales/hu.json +++ b/client/src/__locales/hu.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Használjon egyéni IP-címet az EDNS-hez", "edns_use_custom_ip_desc": "Engedélyezze az egyéni IP-cím használatát az EDNS-hez", "rate_limit_desc": "Maximálisan hány kérést küldhet egy kliens másodpercenkén. Ha 0-ra állítja, akkor nincs korlátozás.", + "rate_limit_subnet_len_ipv4": "Az IPv4-címek alhálózati előtagjának hossza", + "rate_limit_subnet_len_ipv4_desc": "A sebességkorlátozáshoz használt IPv4-címek alhálózati előtagjának hossza. Az alapértelmezett érték 24", + "rate_limit_subnet_len_ipv4_error": "Az IPv4 alhálózati előtag hosszának 0 és 32 között kell lennie", + "rate_limit_subnet_len_ipv6": "Az IPv6-címek alhálózati előtagjának hossza", + "rate_limit_subnet_len_ipv6_desc": "A sebességkorlátozáshoz használt IPv6-címek alhálózati előtagjának hossza. Az alapértelmezett érték 56", + "rate_limit_subnet_len_ipv6_error": "Az IPv6 alhálózati előtag hosszának 0 és 128 között kell lennie", + "form_enter_rate_limit_subnet_len": "Adja meg az alhálózati előtag hosszát a sebességkorlátozáshoz", + "rate_limit_whitelist": "Sebességkorlátozó engedélyezési lista", + "rate_limit_whitelist_desc": "A sebességkorlátozásból kizárt IP-címek", + "rate_limit_whitelist_placeholder": "Adjon meg egy IP-címet soronként", "blocking_ipv4_desc": "A blokkolt A kéréshez visszaadandó IP-cím", "blocking_ipv6_desc": "A blokkolt AAAA kéréshez visszaadandó IP-cím", "blocking_mode_default": "Alapértelmezés: Válaszoljon nulla IP-címmel (vagyis 0.0.0.0 az A-hoz, :: pedig az AAAA-hoz), amikor a blokkolás egy adblock-stílusú szabállyal történik; illetve válaszoljon egy, a szabály által meghatározott IP címmel, amikor a blokkolás egy /etc/hosts stílusú szabállyal történik", @@ -724,5 +734,8 @@ "wednesday_short": "Szer", "thursday_short": "Csüt", "friday_short": "Pén", - "saturday_short": "Szom" + "saturday_short": "Szom", + "upstream_dns_cache_configuration": "Upstream DNS gyorsítótár konfigurációja", + "enable_upstream_dns_cache": "A DNS gyorsítótárazásának engedélyezése az ügyfél egyéni upstream konfigurációjához", + "dns_cache_size": "DNS gyorsítótár mérete, bájtokban" } diff --git a/client/src/__locales/id.json b/client/src/__locales/id.json index 2f7caecc..a8671452 100644 --- a/client/src/__locales/id.json +++ b/client/src/__locales/id.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Gunakan IP khusus untuk EDNS", "edns_use_custom_ip_desc": "Izinkan untuk menggunakan IP kustom untuk EDNS", "rate_limit_desc": "Jumlah permintaan per detik yang diperbolehkan untuk satu klien. Atur ke 0 untuk tidak terbatas.", + "rate_limit_subnet_len_ipv4": "Panjang awalan subnet untuk alamat IPv4", + "rate_limit_subnet_len_ipv4_desc": "Panjang awalan subnet untuk alamat IPv4 yang digunakan untuk pembatasan kecepatan. Standarnya adalah 24", + "rate_limit_subnet_len_ipv4_error": "Panjang awalan subnet IPv4 harus antara 0 dan 32", + "rate_limit_subnet_len_ipv6": "Panjang awalan subnet untuk alamat IPv6", + "rate_limit_subnet_len_ipv6_desc": "Panjang awalan subnet untuk alamat IPv6 yang digunakan untuk pembatasan kecepatan. Standarnya adalah 56", + "rate_limit_subnet_len_ipv6_error": "Panjang awalan subnet IPv6 harus antara 0 dan 128", + "form_enter_rate_limit_subnet_len": "Masukkan panjang awalan subnet untuk pembatasan kecepatan", + "rate_limit_whitelist": "Daftar pembatasan tarif yang diizinkan", + "rate_limit_whitelist_desc": "Alamat IP dikecualikan dari pembatasan tarif", + "rate_limit_whitelist_placeholder": "Masukkan satu alamat IP per baris", "blocking_ipv4_desc": "Alamat IP akan dikembalikan untuk permintaan A yang diblokir", "blocking_ipv6_desc": "Alamat IP akan dipulihkan untuk permintaan AAAA yang diblokir", "blocking_mode_default": "Default: Tanggapi dengan alamat IP nol (0.0.0.0 untuk A; :: untuk AAAA) saat diblokir oleh aturan gaya Adblock; tanggapi dengan alamat IP yang ditentukan dalam aturan ketika diblokir oleh aturan gaya host /etc/", @@ -724,5 +734,8 @@ "wednesday_short": "Rab", "thursday_short": "Kam", "friday_short": "Jum", - "saturday_short": "Sab" + "saturday_short": "Sab", + "upstream_dns_cache_configuration": "Konfigurasi cache DNS upstream", + "enable_upstream_dns_cache": "Aktifkan cache DNS untuk konfigurasi upstream kustom klien ini", + "dns_cache_size": "Ukuran cache DNS, dalam byte" } diff --git a/client/src/__locales/it.json b/client/src/__locales/it.json index 04a09411..fca0d970 100644 --- a/client/src/__locales/it.json +++ b/client/src/__locales/it.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Usa IP personalizzato per EDNS", "edns_use_custom_ip_desc": "Consentire l'uso di un IP personalizzato per EDNS", "rate_limit_desc": "Il numero di richieste al secondo consentite da un singolo client. Impostare questo valore a 0 rimuove le limitazioni.", + "rate_limit_subnet_len_ipv4": "Lunghezza prefisso di sottorete per indirizzi IPv4", + "rate_limit_subnet_len_ipv4_desc": "Lunghezza prefisso sottorete per indirizzi IPv4 usati per la limitazione della velocità. Valore predefinito 24", + "rate_limit_subnet_len_ipv4_error": "La lunghezza del prefisso di sottorete IPv4 deve essere compresa tra 0 e 32", + "rate_limit_subnet_len_ipv6": "Lunghezza prefisso di sottorete per indirizzi IPv6", + "rate_limit_subnet_len_ipv6_desc": "Lunghezza prefisso di sottorete per indirizzi IPv6 usati per la limitazione della velocità. Valore predefinito 56", + "rate_limit_subnet_len_ipv6_error": "La lunghezza del prefisso di sottorete IPv6 deve essere compresa tra 0 e 128", + "form_enter_rate_limit_subnet_len": "Inserisci lunghezza prefisso di sottorete per limitazione velocità", + "rate_limit_whitelist": "Lista consentita per limitazione velocità", + "rate_limit_whitelist_desc": "Indirizzi IP esclusi dalla limitazione della velocità", + "rate_limit_whitelist_placeholder": "Inserisci un indirizzo IP per riga", "blocking_ipv4_desc": "Indirizzo IP per una richiesta DNS IPv4 bloccata", "blocking_ipv6_desc": "Indirizzo IP restituito per una richiesta DNS IPv6 bloccata", "blocking_mode_default": "Risponde con un indirizzo IP pari a zero (0.0.0.0 per A; :: per AAAA) quando bloccato da una regola in stile Blocca-annunci; risponde con l'indirizzo IP specificato nella regola quando bloccato da una regola in stile /etc/hosts", @@ -724,5 +734,8 @@ "wednesday_short": "Mer", "thursday_short": "Gio", "friday_short": "Ven", - "saturday_short": "Sab" + "saturday_short": "Sab", + "upstream_dns_cache_configuration": "Configurazione cache DNS upstream", + "enable_upstream_dns_cache": "Abilita cache DNS per la configurazione upstream personalizzata del client", + "dns_cache_size": "Dimensioni cache DNS (in byte)" } diff --git a/client/src/__locales/ja.json b/client/src/__locales/ja.json index 1e2bb4a4..63cd3c3f 100644 --- a/client/src/__locales/ja.json +++ b/client/src/__locales/ja.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "EDNSにカスタムIPを使用する", "edns_use_custom_ip_desc": "EDNS に対してカスタム IP の使用を許可します。", "rate_limit_desc": "一つのクライアントに対して許可される1秒あたりのリクエスト数(「0」に設定すると、制限なしになります)", + "rate_limit_subnet_len_ipv4": "IPv4 アドレスのサブネットプレフィックス長", + "rate_limit_subnet_len_ipv4_desc": "rate limiting(レート制限)に使用される IPv4 アドレスのサブネットプレフィックス長です。デフォルト値は 24 です。", + "rate_limit_subnet_len_ipv4_error": "IPv4 サブネットプレフィックス長は0〜32の範囲内である必要があります。", + "rate_limit_subnet_len_ipv6": "IPv6 アドレスのサブネットプレフィックス長", + "rate_limit_subnet_len_ipv6_desc": "rate limiting(レート制限)に使用される IPv6 アドレスのサブネットプレフィックス長です。デフォルト値は 56 です。", + "rate_limit_subnet_len_ipv6_error": "IPv6 サブネットのプレフィックス長は0〜128の範囲内である必要があります。", + "form_enter_rate_limit_subnet_len": "rate limiting(レート制限)のためのサブネットプレフィックス長を入力してください", + "rate_limit_whitelist": "rate limiting(レート制限)の許可リスト", + "rate_limit_whitelist_desc": "rate limiting(レート制限)の対象から外すIPアドレスを指定できます。", + "rate_limit_whitelist_placeholder": "IPアドレスを1行に1つずづ入力してください。", "blocking_ipv4_desc": "ブロックされたAリクエストに対して応答されるIPアドレス", "blocking_ipv6_desc": "ブロックされたAAAAリクエストに対して応答されるIPアドレス", "blocking_mode_default": "デフォルト:Adblock系ルールによってブロックされると、ゼロIPアドレス(Aに対しては「0.0.0.0」、AAAAに対しては「::」)で応答します。/etc/hosts系ルールによってブロックされると、ルールにて指定されているIPアドレスで応答します。", @@ -724,5 +734,8 @@ "wednesday_short": "水", "thursday_short": "木", "friday_short": "金", - "saturday_short": "土" + "saturday_short": "土", + "upstream_dns_cache_configuration": "Upstream DNS cache configuration(アップストリームDNSキャッシュの構成)", + "enable_upstream_dns_cache": "このクライアントのカスタムアップストリーム構成に対してDNSキャッシュを有効にする", + "dns_cache_size": "DNSキャッシュサイズ(バイト単位)" } diff --git a/client/src/__locales/ko.json b/client/src/__locales/ko.json index b10190e8..e39ca472 100644 --- a/client/src/__locales/ko.json +++ b/client/src/__locales/ko.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "EDNS에 사용자 지정 IP 사용", "edns_use_custom_ip_desc": "EDNS에 사용자 지정 IP 사용하도록 허용합니다.", "rate_limit_desc": "단일 클라이언트에서 허용 가능한 초 당 요청 생성 숫자 (0: 무제한)", + "rate_limit_subnet_len_ipv4": "IPv4 주소의 서브넷 접두사 길이", + "rate_limit_subnet_len_ipv4_desc": "속도 제한에 사용되는 IPv4 주소의 서브넷 접두사 길이입니다. 기본값은 24입니다.", + "rate_limit_subnet_len_ipv4_error": "IPv4 서브넷 접두사 길이는 0에서 32 사이여야 합니다.", + "rate_limit_subnet_len_ipv6": "IPv6 주소의 서브넷 접두사 길이", + "rate_limit_subnet_len_ipv6_desc": "속도 제한에 사용되는 IPv6 주소의 서브넷 접두사 길이입니다. 기본값은 56입니다.", + "rate_limit_subnet_len_ipv6_error": "IPv6 서브넷 접두사 길이는 0에서 128 사이여야 합니다.", + "form_enter_rate_limit_subnet_len": "속도 제한을 위한 서브넷 접두사 길이를 입력하세요", + "rate_limit_whitelist": "속도 제한 허용 목록", + "rate_limit_whitelist_desc": "속도 제한에서 제외되는 IP 주소", + "rate_limit_whitelist_placeholder": "한 줄에 하나씩 IP 주소를 입력하세요.", "blocking_ipv4_desc": "차단된 A 요청에 대해서 반환할 IP 주소", "blocking_ipv6_desc": "차단된 AAAA 요청에 대해서 반환할 IP 주소", "blocking_mode_default": "기본: Adblock 스타일 규칙에 의해 차단되면 제로 IP 주소(A는 0.0.0.0; AAAA는 ::)로 응답합니다; /etc/hosts 스타일 규칙에 의해 차단되면 규칙에 정의된 IP 주소로 응답합니다", @@ -724,5 +734,8 @@ "wednesday_short": "수", "thursday_short": "목", "friday_short": "금", - "saturday_short": "토" + "saturday_short": "토", + "upstream_dns_cache_configuration": "업스트림 DNS 캐시 설정", + "enable_upstream_dns_cache": "이 클라이언트의 사용자 지정 업스트림 설정에서 DNS 캐싱 사용", + "dns_cache_size": "DNS 캐시 크기(바이트)" } diff --git a/client/src/__locales/nl.json b/client/src/__locales/nl.json index d35bd741..2fc0f1ee 100644 --- a/client/src/__locales/nl.json +++ b/client/src/__locales/nl.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Aangepast IP-adres gebruiken voor EDNS", "edns_use_custom_ip_desc": "Toestaan om aangepast IP-adres voor EDNS te gebruiken", "rate_limit_desc": "Het aantal verzoeken per seconde toegelaten per toestel. 0 betekent onbeperkt.", + "rate_limit_subnet_len_ipv4": "Lengte subnetvoorvoegsel voor IPv4-adressen", + "rate_limit_subnet_len_ipv4_desc": "Lengte subnetvoorvoegsel voor IPv4-adressen die worden gebruikt voor snelheidsbeperking. De standaardwaarde is 24", + "rate_limit_subnet_len_ipv4_error": "De lengte van het IPv4-subnetvoorvoegsel moet tussen 0 en 32 liggen", + "rate_limit_subnet_len_ipv6": "Lengte subnetvoorvoegsel voor IPv6-adressen", + "rate_limit_subnet_len_ipv6_desc": "Lengte subnetvoorvoegsel voor IPv6-adressen die worden gebruikt voor snelheidsbeperking. De standaardwaarde is 56", + "rate_limit_subnet_len_ipv6_error": "De lengte van het IPv6-subnetvoorvoegsel moet tussen 0 en 128 liggen", + "form_enter_rate_limit_subnet_len": "Voer de lengte van het subnetvoorvoegsel in voor snelheidsbeperking", + "rate_limit_whitelist": "Toelatingslijst voor snelheidsbeperking", + "rate_limit_whitelist_desc": "IP-adressen uitgesloten van snelheidsbeperking", + "rate_limit_whitelist_placeholder": "Voer één IP-adres per regel in", "blocking_ipv4_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek", "blocking_ipv6_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek", "blocking_mode_default": "Standaard: Reageer met een nul IP adres (0.0.0.0 for A; :: voor AAAA) wanneer geblokkeerd door een Adblock-type regel; reageer met het IP-adres dat is opgegeven in de regel wanneer geblokkeerd door een /etc/hosts type regel", @@ -724,5 +734,8 @@ "wednesday_short": "wo", "thursday_short": "do", "friday_short": "vr", - "saturday_short": "za" + "saturday_short": "za", + "upstream_dns_cache_configuration": "Upstream DNS-cacheconfiguratie", + "enable_upstream_dns_cache": "DNS-caching inschakelen voor de aangepaste upstream-configuratie van deze client", + "dns_cache_size": "DNS-cachegrootte, in bytes" } diff --git a/client/src/__locales/pl.json b/client/src/__locales/pl.json index 0668a8be..d657eb06 100644 --- a/client/src/__locales/pl.json +++ b/client/src/__locales/pl.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Użyj niestandardowego adresu IP dla EDNS", "edns_use_custom_ip_desc": "Zezwól na użycie niestandardowego adresu IP dla EDNS", "rate_limit_desc": "Liczba żądań na sekundę dozwolona na klienta. Ustawienie wartości 0 oznacza brak ograniczeń.", + "rate_limit_subnet_len_ipv4": "Długość maski podsieci dla adresów IPv4", + "rate_limit_subnet_len_ipv4_desc": "Długość maski podsieci dla adresów IPv4 używanych do ograniczania prędkości. Domyślnie jest to 24", + "rate_limit_subnet_len_ipv4_error": "Długość maski podsieci IPv4 powinna wynosić od 0 do 32", + "rate_limit_subnet_len_ipv6": "Długość prefiksu podsieci dla adresów IPv6", + "rate_limit_subnet_len_ipv6_desc": "Długość prefiksu podsieci dla adresów IPv6 używanych do ograniczania szybkości. Domyślnie jest to 56", + "rate_limit_subnet_len_ipv6_error": "Długość prefiksu podsieci IPv6 powinna wynosić od 0 do 128", + "form_enter_rate_limit_subnet_len": "Wprowadź długość prefiksu podsieci dla ograniczenia prędkości", + "rate_limit_whitelist": "Lista zezwoleń ograniczających prędkość", + "rate_limit_whitelist_desc": "Adresy IP wykluczone z ograniczania prędkości", + "rate_limit_whitelist_placeholder": "Wprowadź po jednym adresie IP w każdym wierszu", "blocking_ipv4_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania A", "blocking_ipv6_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania AAAA", "blocking_mode_default": "Domyślna: Odpowiedz z zerowym adresem IP (0.0.0.0 dla A; :: dla AAAA) po zablokowaniu przez regułę Adblock; odpowiedź adresem IP wpisanym w regule, jeśli jest blokowany przez regułę w stylu /etc/hosts", @@ -724,5 +734,8 @@ "wednesday_short": "Śro", "thursday_short": "Czw", "friday_short": "Pt", - "saturday_short": "Sob" + "saturday_short": "Sob", + "upstream_dns_cache_configuration": "Konfiguracja pamięci podręcznej upstream serwerów DNS", + "enable_upstream_dns_cache": "Włącz pamięć podręczną dla niestandardowej konfiguracji serwera upstream tego klienta", + "dns_cache_size": "Rozmiar pamięci podręcznej DNS, w bajtach" } diff --git a/client/src/__locales/pt-br.json b/client/src/__locales/pt-br.json index cee9aa64..83a6d9a0 100644 --- a/client/src/__locales/pt-br.json +++ b/client/src/__locales/pt-br.json @@ -303,13 +303,23 @@ "download_mobileconfig_dot": "BAixar .mobileconfig para DNS-sobre-TLS", "download_mobileconfig": "Baixar arquivo de configuração", "plain_dns": "DNS simples", - "form_enter_rate_limit": "Insira a taxa limite", - "rate_limit": "Taxa limite", + "form_enter_rate_limit": "Insira a velocidade limite", + "rate_limit": "Velocidade limite", "edns_enable": "Ativar a sub-rede do cliente EDNS", "edns_cs_desc": "Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registro de consulta.", "edns_use_custom_ip": "Usar IP personalizado para EDNS", "edns_use_custom_ip_desc": "Permitir o uso de IP personalizado para EDNS", "rate_limit_desc": "O número de solicitações por segundo permitidas por cliente. Definir como 0 significa que não há limite.", + "rate_limit_subnet_len_ipv4": "Comprimento do prefixo de sub-rede para endereços IPv4", + "rate_limit_subnet_len_ipv4_desc": "Comprimento do prefixo de sub-rede para endereços IPv4 usados para limitação de velocidade. O padrão é 24", + "rate_limit_subnet_len_ipv4_error": "O comprimento do prefixo da sub-rede IPv4 deve estar entre 0 e 32", + "rate_limit_subnet_len_ipv6": "Comprimento do prefixo de sub-rede para endereços IPv6", + "rate_limit_subnet_len_ipv6_desc": "Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56", + "rate_limit_subnet_len_ipv6_error": "O comprimento do prefixo da sub-rede IPv6 deve estar entre 0 e 128", + "form_enter_rate_limit_subnet_len": "Insira o comprimento do prefixo da sub-rede para limitação de taxa", + "rate_limit_whitelist": "Lista de permissões de limitação de velocidade", + "rate_limit_whitelist_desc": "Endereços IP excluídos da limitação de velocidade", + "rate_limit_whitelist_placeholder": "Insira um endereço IP por linha", "blocking_ipv4_desc": "Endereço de IP a ser retornado para uma solicitação bloqueada", "blocking_ipv6_desc": "Endereço de IP a ser retornado para uma solicitação AAAA bloqueada", "blocking_mode_default": "Padrão: Responder com zero endereço IP (0.0.0.0 para A; :: para AAAA) quando bloqueado pela regra de estilo Adblock; responde com o endereço IP especificado na regra quando bloqueado pela regra /etc/hosts-style", @@ -724,5 +734,8 @@ "wednesday_short": "Quar", "thursday_short": "Qui", "friday_short": "Sex", - "saturday_short": "Sab" + "saturday_short": "Sab", + "upstream_dns_cache_configuration": "Configuração do cache de DNS upstream", + "enable_upstream_dns_cache": "Ativar o armazenamento em cache do DNS para a configuração de upstream personalizada deste cliente", + "dns_cache_size": "Tamanho do cache do DNS, em bytes" } diff --git a/client/src/__locales/pt-pt.json b/client/src/__locales/pt-pt.json index 053d4f6f..8f6e6fcf 100644 --- a/client/src/__locales/pt-pt.json +++ b/client/src/__locales/pt-pt.json @@ -303,13 +303,23 @@ "download_mobileconfig_dot": "Transferir .mobileconfig para DNS-sobre-TLS", "download_mobileconfig": "Transferir ficheiro de configuração", "plain_dns": "DNS simples", - "form_enter_rate_limit": "Insira o limite de taxa", - "rate_limit": "Limite de taxa", + "form_enter_rate_limit": "Insira o limite de velocidade", + "rate_limit": "Limite de velocidade", "edns_enable": "Ativar a sub-rede do cliente EDNS", "edns_cs_desc": "Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registo de consulta.", "edns_use_custom_ip": "Usar IP personalizado para EDNS", "edns_use_custom_ip_desc": "Permitir a utilização de IP personalizado para EDNS", "rate_limit_desc": "O número de solicitações por segundo permitido por cliente. Configurando para 0 significa sem limite.", + "rate_limit_subnet_len_ipv4": "Comprimento do prefixo de sub-rede para endereços IPv4", + "rate_limit_subnet_len_ipv4_desc": "Comprimento do prefixo de sub-rede para endereços IPv4 usados para limitação de velocidade. O padrão é 24", + "rate_limit_subnet_len_ipv4_error": "O comprimento do prefixo da sub-rede IPv4 deve estar entre 0 e 32", + "rate_limit_subnet_len_ipv6": "Comprimento do prefixo de sub-rede para endereços IPv6", + "rate_limit_subnet_len_ipv6_desc": "Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56", + "rate_limit_subnet_len_ipv6_error": "O comprimento do prefixo da sub-rede IPv6 deve situar-se entre 0 e 128", + "form_enter_rate_limit_subnet_len": "Introduza o comprimento do prefixo da sub-rede para limitação da velocidade", + "rate_limit_whitelist": "Lista de permissões de limitação de velocidade", + "rate_limit_whitelist_desc": "Endereços IP excluídos da limitação de velocidade", + "rate_limit_whitelist_placeholder": "Insira um endereço IP por linha", "blocking_ipv4_desc": "Endereço IP a ser devolvido para uma solicitação A bloqueada", "blocking_ipv6_desc": "Endereço IP a ser devolvido para uma solicitação AAAA bloqueada", "blocking_mode_default": "Predefinido: Responder com zero endereço IP (0.0.0.0 para A; :: para AAAA) quando bloqueado pela regra de estilo Adblock; responde com o endereço IP especificado na regra quando bloqueado pela regra /etc/hosts-style", @@ -724,5 +734,8 @@ "wednesday_short": "Quarta", "thursday_short": "Quinta", "friday_short": "Sexta", - "saturday_short": "Sábado" + "saturday_short": "Sábado", + "upstream_dns_cache_configuration": "Configuração da cache do DNS upstream", + "enable_upstream_dns_cache": "Ativar o armazenamento em cache do DNS para a configuração de upstream personalizada deste cliente", + "dns_cache_size": "Tamanho da cache DNS, em bytes" } diff --git a/client/src/__locales/ro.json b/client/src/__locales/ro.json index 3d86bc6d..03331064 100644 --- a/client/src/__locales/ro.json +++ b/client/src/__locales/ro.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Utilizați IP personalizat pentru EDNS", "edns_use_custom_ip_desc": "Permiteți utilizarea IP-ului personalizat pentru EDNS", "rate_limit_desc": "Numărul de interogări pe secundă permise pe client. Setarea la 0 înseamnă că nu există limită.", + "rate_limit_subnet_len_ipv4": "Lungimea prefixului de subrețea pentru adrese IPv4", + "rate_limit_subnet_len_ipv4_desc": "Lungimea prefixului de subrețea pentru adresele IPv4 utilizate pentru limitarea ratei. Valoarea implicită este 24", + "rate_limit_subnet_len_ipv4_error": "Lungimea prefixului de subrețea IPv4 ar trebui să fie între 0 și 32", + "rate_limit_subnet_len_ipv6": "Lungimea prefixului de subrețea pentru adrese IPv6", + "rate_limit_subnet_len_ipv6_desc": "Lungimea prefixului de subrețea pentru adresele IPv6 utilizate pentru limitarea ratei. Valoarea implicită este 56", + "rate_limit_subnet_len_ipv6_error": "Lungimea prefixului de subrețea IPv6 ar trebui să fie între 0 și 128", + "form_enter_rate_limit_subnet_len": "Introduceți lungimea prefixului de subrețea pentru limitarea ratei", + "rate_limit_whitelist": "Lista permisă pentru limitarea ratei", + "rate_limit_whitelist_desc": "Adresele IP excluse de la limitarea ratei", + "rate_limit_whitelist_placeholder": "Introduceți o adresă IP per linie", "blocking_ipv4_desc": "Adresa IP de returnat pentru o cerere A de blocare", "blocking_ipv6_desc": "Adresa IP de returnat pentru o cerere AAAA de blocare", "blocking_mode_default": "Implicit: Răspunde cu adresa IP (0.0.0.0 for A; :: pentru AAAA) când sunt blocate de regulă tip Adblock; răspunde cu adresa IP specificată în regulă când sunt blocate de regula tip /etc/hosts", @@ -724,5 +734,8 @@ "wednesday_short": "mi", "thursday_short": "jo", "friday_short": "vi", - "saturday_short": "sa" + "saturday_short": "sa", + "upstream_dns_cache_configuration": "Configurarea cache-ului DNS în amonte", + "enable_upstream_dns_cache": "Activați memoria cache DNS pentru configurația personalizată în amonte a acestui client", + "dns_cache_size": "Dimensiunea cache-ului DNS, în octeți" } diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json index 9cc1c787..42ee4eb6 100644 --- a/client/src/__locales/ru.json +++ b/client/src/__locales/ru.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Использовать указанный IP для EDNS", "edns_use_custom_ip_desc": "Разрешить использовать собственный IP для EDNS", "rate_limit_desc": "Ограничение на количество запросов в секунду для каждого клиента (0 — неограниченно).", + "rate_limit_subnet_len_ipv4": "Длина префикса подсети для IPv4-адресов", + "rate_limit_subnet_len_ipv4_desc": "Длина префикса подсети для IPv4-адресов, используемых для ограничения скорости. По умолчанию 24", + "rate_limit_subnet_len_ipv4_error": "Длина префикса IPv4-подсетей должна составлять от 0 до 32", + "rate_limit_subnet_len_ipv6": "Длина префикса подсети для IPv6-адресов", + "rate_limit_subnet_len_ipv6_desc": "Длина префикса подсети для IPv6-адресов, используемых для ограничения скорости. По умолчанию 56", + "rate_limit_subnet_len_ipv6_error": "Длина префикса IPv6-подсетей должна составлять от 0 до 128", + "form_enter_rate_limit_subnet_len": "Введите длину префикса подсети для ограничения скорости", + "rate_limit_whitelist": "Белый список ограничения скорости", + "rate_limit_whitelist_desc": "IP-адреса, на которые не распространяется ограничение скорости", + "rate_limit_whitelist_placeholder": "Введите по одному адресу на строчку", "blocking_ipv4_desc": "IP-адрес, возвращаемый при блокировке A-запроса", "blocking_ipv6_desc": "IP-адрес, возвращаемый при блокировке AAAA-запроса", "blocking_mode_default": "Стандартный: Отвечает с нулевым IP-адресом, (0.0.0.0 для A; :: для AAAA) когда заблокировано правилом в стиле Adblock; отвечает с IP-адресом, указанным в правиле, когда заблокировано правилом в стиле файлов hosts", @@ -724,5 +734,8 @@ "wednesday_short": "Ср", "thursday_short": "Чт", "friday_short": "Пт", - "saturday_short": "Сб" + "saturday_short": "Сб", + "upstream_dns_cache_configuration": "Конфигурация кеша upstream DNS-серверов", + "enable_upstream_dns_cache": "Включить кеширование для пользовательской конфигурации upstream-серверов этого клиента", + "dns_cache_size": "Размер DNS-кеша в байтах" } diff --git a/client/src/__locales/sk.json b/client/src/__locales/sk.json index 53c00d69..d368fe3d 100644 --- a/client/src/__locales/sk.json +++ b/client/src/__locales/sk.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Použiť vlastnú IP adresu pre EDNS", "edns_use_custom_ip_desc": "Povoliť používanie vlastnej IP adresy pre EDNS", "rate_limit_desc": "Počet požiadaviek za sekundu, ktoré môže jeden klient vykonať. Nastavenie na hodnotu 0 znamená neobmedzene.", + "rate_limit_subnet_len_ipv4": "Dĺžka prefixu podsiete pre adresy IPv4", + "rate_limit_subnet_len_ipv4_desc": "Dĺžka prefixu podsiete pre adresy IPv4 používané na obmedzenie rýchlosti. Predvolená hodnota je 24", + "rate_limit_subnet_len_ipv4_error": "Dĺžka prefixu podsiete IPv4 musí byť od 0 do 32", + "rate_limit_subnet_len_ipv6": "Dĺžka prefixu podsiete pre adresy IPv6", + "rate_limit_subnet_len_ipv6_desc": "Dĺžka prefixu podsiete pre adresy IPv6 používané na obmedzenie rýchlosti. Predvolená hodnota je 56", + "rate_limit_subnet_len_ipv6_error": "Dĺžka prefixu podsiete IPv6 musí byť od 0 do 128", + "form_enter_rate_limit_subnet_len": "Zadajte dĺžku prefixu podsiete pre obmedzenie rýchlosti", + "rate_limit_whitelist": "Zoznam povolení obmedzujúcich rýchlosť", + "rate_limit_whitelist_desc": "IP adresy vylúčené z obmedzenia rýchlosti", + "rate_limit_whitelist_placeholder": "Na každý riadok zadajte IP adresu jedného servera", "blocking_ipv4_desc": "IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti A", "blocking_ipv6_desc": "IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti AAAA", "blocking_mode_default": "Predvolené: Odpovedať nulovou adresou IP (0,0.0.0 pre A; :: pre AAAA), keď je blokovaná pravidlom v štýle Adblock; odpovedať IP adresou uvedenou v pravidle, keď je blokovaná pravidlom v štýle /etc/hosts", @@ -724,5 +734,8 @@ "wednesday_short": "Str", "thursday_short": "Štr", "friday_short": "Pia", - "saturday_short": "Sob" + "saturday_short": "Sob", + "upstream_dns_cache_configuration": "Konfigurácia cache pamäte DNS pre upstream", + "enable_upstream_dns_cache": "Zapnúť ukladanie DNS do cache pamäte pre vlastnú konfiguráciu odosielania tohto klienta", + "dns_cache_size": "Veľkosť cache pamäte DNS v bajtoch" } diff --git a/client/src/__locales/sl.json b/client/src/__locales/sl.json index d26f994d..70e4eb93 100644 --- a/client/src/__locales/sl.json +++ b/client/src/__locales/sl.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Uporabi IP po meri za EDNS", "edns_use_custom_ip_desc": "Dovoli uporabo naslova IP po meri za EDNS", "rate_limit_desc": "Dovoljeno število zahtev na sekundo na odjemalca. Nastavitev na 0 pomeni brez omejitve.", + "rate_limit_subnet_len_ipv4": "Dolžina predpone podomrežja za naslove IPv4", + "rate_limit_subnet_len_ipv4_desc": "Dolžina predpone podomrežja za naslove IPv4, ki se uporabljajo za omejevanje hitrosti. Privzeto je 24", + "rate_limit_subnet_len_ipv4_error": "Dolžina predpone podomrežja IPv4 mora biti med 0 in 32", + "rate_limit_subnet_len_ipv6": "Dolžina predpone podomrežja za naslove IPv4", + "rate_limit_subnet_len_ipv6_desc": "Dolžina predpone podomrežja za naslove IPv6, ki se uporabljajo za omejevanje hitrosti. Privzeta vrednost je 56", + "rate_limit_subnet_len_ipv6_error": "Dolžina podomrežne predpone IPv6 mora biti med 0 in 128", + "form_enter_rate_limit_subnet_len": "Vnesite dolžino predpone podomrežja za omejitev hitrosti", + "rate_limit_whitelist": "Seznam dovoljenih za omejevanje hitrosti", + "rate_limit_whitelist_desc": "Naslovi IP so izključeni iz omejitve hitrosti", + "rate_limit_whitelist_placeholder": "Vnesite en naslov IP na vrstico", "blocking_ipv4_desc": "IP naslov, ki mora biti vrnjen za onemogočeno zahtevo A", "blocking_ipv6_desc": "IP naslov, ki mora biti vrnjen za onemogočeno zahtevo AAAA", "blocking_mode_default": "Privzeto: odgovori z ničelnim naslovom IP (0.0.0.0 za A; :: za AAAA), ko je onemogočen s pravilom v slogu Adblocka; odgovor z naslovom IP, določenim v pravilu, ko je onemogočen s pravilom /etc/hosts", @@ -724,5 +734,8 @@ "wednesday_short": "Sre", "thursday_short": "Čet", "friday_short": "Pet", - "saturday_short": "Sob" + "saturday_short": "Sob", + "upstream_dns_cache_configuration": "Nastavitve predpomnilnika gorvodnega DNS", + "enable_upstream_dns_cache": "Omogoči predpomnjenje nastavitev gorvodnega DNS po meri tega odjemalca", + "dns_cache_size": "Velikost predpomnilnika DNS, v bajtih" } diff --git a/client/src/__locales/sr-cs.json b/client/src/__locales/sr-cs.json index 2e0aadf6..583822e7 100644 --- a/client/src/__locales/sr-cs.json +++ b/client/src/__locales/sr-cs.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Koristi prilagođeni IP za EDNS", "edns_use_custom_ip_desc": "Dozvoli korišćenje prilagođenog IP-a za EDNS", "rate_limit_desc": "Broj zahteva u sekundi dozvoljen po klijentu. Postavljanje na 0 znači da nema ograničenja.", + "rate_limit_subnet_len_ipv4": "Dužina prefixa podmreže za IPv4 adrese", + "rate_limit_subnet_len_ipv4_desc": "Dužina prefixa podmreže za IPv4 adrese koje se koriste za ograničavanje brzine. Podrazumevano je 24", + "rate_limit_subnet_len_ipv4_error": "Dužina prefixa IPv4 podmreže treba da bude između 0 i 32", + "rate_limit_subnet_len_ipv6": "Dužina prefixa podmreže za IPv6 adrese", + "rate_limit_subnet_len_ipv6_desc": "Dužina prefixa podmreže za IPv6 adrese koje se koriste za ograničavanje brzine. Podrazumevano je 56", + "rate_limit_subnet_len_ipv6_error": "Dužina prefixa IPv6 podmreže treba da bude između 0 i 128", + "form_enter_rate_limit_subnet_len": "Unesite dužinu prefixa podmreže da biste ograničili brzinu", + "rate_limit_whitelist": "Lista dozvoljenih lista za ograničavanje brzine", + "rate_limit_whitelist_desc": "IP adrese koje nisu obuhvaćene ograničenjem brzine", + "rate_limit_whitelist_placeholder": "Unesite jednu IP adresu servera po redu", "blocking_ipv4_desc": "IP adresa koja će biti vraćena za blokirane zahteve", "blocking_ipv6_desc": "IP adresa koja će biti vraćena za blokirane AAAA zahteve", "blocking_mode_default": "Podrazumevano: Odgovara sa REFUSED kada je blokirano od Adblock-style pravila; odgovara sa IP adresom koja je određena u pravilu kada je blokiran od /etc/hosts-style pravila", @@ -724,5 +734,8 @@ "wednesday_short": "Sre", "thursday_short": "Čet", "friday_short": "Pet", - "saturday_short": "Sub" + "saturday_short": "Sub", + "upstream_dns_cache_configuration": "Konfiguracija keša upstream DNS servera", + "enable_upstream_dns_cache": "Uključite keširanje za korisničku konfiguraciju upstream servera ovog klijenta", + "dns_cache_size": "Veličina DNS keša, u bajtovima" } diff --git a/client/src/__locales/sv.json b/client/src/__locales/sv.json index 457859f6..a1a11a6f 100644 --- a/client/src/__locales/sv.json +++ b/client/src/__locales/sv.json @@ -310,6 +310,15 @@ "edns_use_custom_ip": "Använd anpassad IP för EDNS", "edns_use_custom_ip_desc": "Tillåt att använda anpassad IP för EDNS", "rate_limit_desc": "Antalet förfrågningar per sekund som tillåts per klient. Att sätta den till 0 innebär ingen gräns.", + "rate_limit_subnet_len_ipv4": "Prefixlängd för subnät för IPv4-adresser", + "rate_limit_subnet_len_ipv4_desc": "Subnätprefixlängd för IPv4-adresser som används för hastighetsbegränsning. Standard är 24", + "rate_limit_subnet_len_ipv4_error": "IPv4-subnätets prefixlängd ska vara mellan 0 och 32", + "rate_limit_subnet_len_ipv6": "Prefixlängd för subnät för IPv6-adresser", + "rate_limit_subnet_len_ipv6_desc": "Subnätprefixlängd för IPv6-adresser som används för hastighetsbegränsning. Standard är 56", + "rate_limit_subnet_len_ipv6_error": "IPv6-subnätets prefixlängd ska vara mellan 0 och 128", + "form_enter_rate_limit_subnet_len": "Ange subnätprefixlängd för hastighetsbegränsning", + "rate_limit_whitelist_desc": "IP-adresser uteslutna från hastighetsbegränsning", + "rate_limit_whitelist_placeholder": "Ange en IP-adress per rad", "blocking_ipv4_desc": "IP adress som ska returneras för en blockerad A förfrågan", "blocking_ipv6_desc": "IP adress som ska returneras för en blockerad AAAA förfrågan", "blocking_mode_default": "Standard: Svara med noll IP-adress (0.0.0.0 för A; :: för AAAA) när det blockeras av regel i Adblock-stil; svara med IP-adressen som anges i regeln när den blockeras av regel i /etc/hosts-stil", @@ -724,5 +733,8 @@ "wednesday_short": "Ons", "thursday_short": "Tor", "friday_short": "Fre", - "saturday_short": "Lör" + "saturday_short": "Lör", + "upstream_dns_cache_configuration": "Konfiguration av uppströms DNS-cache", + "enable_upstream_dns_cache": "Aktivera DNS-cachelagring för den här klientens anpassade uppströmskonfiguration", + "dns_cache_size": "DNS-cachestorlek, i byte" } diff --git a/client/src/__locales/tr.json b/client/src/__locales/tr.json index 82ad2c70..3e0008ce 100644 --- a/client/src/__locales/tr.json +++ b/client/src/__locales/tr.json @@ -98,7 +98,7 @@ "filters": "Filtreler", "filter": "Filtre", "query_log": "Sorgu Günlüğü", - "compact": "Yoğun", + "compact": "Sık", "nothing_found": "Hiçbir şey bulunamadı", "faq": "SSS", "version": "Sürüm", @@ -310,9 +310,19 @@ "edns_use_custom_ip": "EDNS için özel IP kullan", "edns_use_custom_ip_desc": "EDNS için özel IP kullanımına izin ver", "rate_limit_desc": "İstemci başına izin verilen saniyedeki istek sayısı. 0 olarak ayarlamak, sınır olmadığı anlamına gelir.", + "rate_limit_subnet_len_ipv4": "IPv4 adresleri için alt ağ önek uzunluğu", + "rate_limit_subnet_len_ipv4_desc": "Hız sınırlaması için kullanılan IPv4 adreslerinin alt ağ önek uzunluğu. Varsayılan 24'tür", + "rate_limit_subnet_len_ipv4_error": "IPv4 alt ağ önek uzunluğu 0 ile 32 arasında olmalıdır", + "rate_limit_subnet_len_ipv6": "IPv6 adresleri için alt ağ önek uzunluğu", + "rate_limit_subnet_len_ipv6_desc": "Hız sınırlaması için kullanılan IPv6 adreslerinin alt ağ önek uzunluğu. Varsayılan 56'tür", + "rate_limit_subnet_len_ipv6_error": "IPv6 alt ağ önek uzunluğu 0 ile 128 arasında olmalıdır", + "form_enter_rate_limit_subnet_len": "Hız sınırlaması için alt ağ önek uzunluğunu girin", + "rate_limit_whitelist": "Hız sınırlama izin listesi", + "rate_limit_whitelist_desc": "Hız sınırlamasından hariç tutulan IP adresleri", + "rate_limit_whitelist_placeholder": "Her satıra bir IP adresi girin", "blocking_ipv4_desc": "Engellenen bir A isteği için geri döndürülecek IP adresi", "blocking_ipv6_desc": "Engellenen bir AAAA isteği için geri döndürülecek IP adresi", - "blocking_mode_default": "Varsayılan: Reklam engelleme tarzı kural tarafından engellendiğinde sıfır IP adresiyle (A için 0.0.0.0; :: AAAA için) yanıt verin; /etc/hosts-tarzı kural tarafından engellendiğinde, kuralda belirtilen IP adresiyle yanıt verin", + "blocking_mode_default": "Varsayılan: Reklam engelleme stili kuralı tarafından engellendiğinde sıfır IP adresiyle (A için 0.0.0.0; :: AAAA için) yanıt verin; /etc/hosts-tarzı kural tarafından engellendiğinde, kuralda belirtilen IP adresiyle yanıt verin", "blocking_mode_refused": "REFUSED: REFUSED koduyla yanıt verin", "blocking_mode_nxdomain": "NXDOMAIN: NXDOMAIN koduyla yanıt verin", "blocking_mode_null_ip": "Boş IP: Sıfır IP adresiyle yanıt verin (A için 0.0.0.0; :: AAAA için)", @@ -724,5 +734,8 @@ "wednesday_short": "Çar", "thursday_short": "Per", "friday_short": "Cum", - "saturday_short": "Cmt" + "saturday_short": "Cmt", + "upstream_dns_cache_configuration": "Üst kaynak DNS önbellek yapılandırması", + "enable_upstream_dns_cache": "Bu istemcinin özel üst kaynak yapılandırması için DNS önbelleğe almayı etkinleştir", + "dns_cache_size": "DNS önbellek boyutu, bayt cinsinden" } diff --git a/client/src/__locales/uk.json b/client/src/__locales/uk.json index 1f38d597..98a63085 100644 --- a/client/src/__locales/uk.json +++ b/client/src/__locales/uk.json @@ -1,6 +1,7 @@ { "client_settings": "Налаштування клієнта", "example_upstream_reserved": "DNS-сервер <0>для певних доменів;", + "example_multiple_upstreams_reserved": "кілька DNS-серверів <0>для конкретних доменів;", "example_upstream_comment": "коментар.", "upstream_parallel": "Використовувати паралельні запити, щоб пришвидшити вирішення одночасною чергою всіх оригінальних серверів.", "parallel_requests": "Паралельні запити", @@ -143,6 +144,8 @@ "enforced_save_search": "Примусовий безпечний пошук", "number_of_dns_query_to_safe_search": "Кількість DNS-запитів до пошукових систем, для яких примусово застосований безпечний пошук", "average_processing_time": "Середній час обробки", + "average_upstream_response_time": "Середній час відгуку upstream-сервера", + "response_time": "Час відгуку", "average_processing_time_hint": "Середній час обробки DNS запиту в мілісекундах", "block_domain_use_filters_and_hosts": "Блокування доменів за допомогою фільтрів та hosts-файлів", "filters_block_toggle_hint": "Ви можете налаштувати правила блокування в розділі Фільтри.", @@ -307,6 +310,16 @@ "edns_use_custom_ip": "Використання користувацької IP-адреси для EDNS", "edns_use_custom_ip_desc": "Дозволити використовувати користувацьку IP-адресу для EDNS", "rate_limit_desc": "Кількість запитів в секунду, які може робити один клієнт. Встановлене значення «0» означатиме необмежену кількість.", + "rate_limit_subnet_len_ipv4": "Довжина префікса підмережі для адрес IPv4", + "rate_limit_subnet_len_ipv4_desc": "Довжина префікса підмережі для адрес IPv4, які використовуються для обмеження швидкості. Типовим значенням є 24", + "rate_limit_subnet_len_ipv4_error": "Довжина префікса підмережі IPv4 має бути від 0 до 32", + "rate_limit_subnet_len_ipv6": "Довжина префікса підмережі для адрес IPv6", + "rate_limit_subnet_len_ipv6_desc": "Довжина префікса підмережі для адрес IPv6, які використовуються для обмеження швидкості. Типовим значенням є 56", + "rate_limit_subnet_len_ipv6_error": "Довжина префікса підмережі IPv6 має бути від 0 до 128", + "form_enter_rate_limit_subnet_len": "Введіть довжину префікса підмережі для обмеження швидкості", + "rate_limit_whitelist": "Список дозволених обмежень швидкості", + "rate_limit_whitelist_desc": "IP-адреси, на які не поширюється обмеження швидкості", + "rate_limit_whitelist_placeholder": "Вводьте одну адресу на рядок", "blocking_ipv4_desc": "IP-адреса, яку потрібно видати для заблокованого A запиту", "blocking_ipv6_desc": "IP-адреса, яку потрібно видати для заблокованого АААА запиту", "blocking_mode_default": "Усталено: відповідь із нульовою IP-адресою (0.0.0.0 для A; :: для AAAA), якщо заблоковано правилом у Adblock-стилі; відповідь зазначеною у правилі IP-адресою, якщо заблокувано правилом у hosts-стилі", @@ -721,5 +734,8 @@ "wednesday_short": "СР", "thursday_short": "ЧТ", "friday_short": "ПТ", - "saturday_short": "СБ" + "saturday_short": "СБ", + "upstream_dns_cache_configuration": "Конфігурація кешу upstream DNS-серверів", + "enable_upstream_dns_cache": "Увімкнути кешування для користувацької конфігурації upstream-серверів цього клієнта", + "dns_cache_size": "Розмір кешу DNS, у байтах" } diff --git a/client/src/__locales/vi.json b/client/src/__locales/vi.json index 29c49769..fb68549f 100644 --- a/client/src/__locales/vi.json +++ b/client/src/__locales/vi.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "Sử dụng địa chỉ IP tùy chỉnh cho EDNS", "edns_use_custom_ip_desc": "Cho phép sử dụng địa chỉ IP tùy chỉnh cho EDNS", "rate_limit_desc": "Số lượng yêu cầu mỗi giây mà một khách hàng được phép thực hiện (0: không giới hạn)", + "rate_limit_subnet_len_ipv4": "Độ dài tiền tố mạng con cho địa chỉ IPv4", + "rate_limit_subnet_len_ipv4_desc": "Độ dài tiền tố mạng con cho các địa chỉ IPv4 được sử dụng để giới hạn tốc độ. Mặc định là 24", + "rate_limit_subnet_len_ipv4_error": "Độ dài tiền tố mạng con IPv4 phải nằm trong khoảng từ 0 đến 32", + "rate_limit_subnet_len_ipv6": "Độ dài tiền tố mạng con cho địa chỉ IPv6", + "rate_limit_subnet_len_ipv6_desc": "Độ dài tiền tố mạng con cho các địa chỉ IPv6 được sử dụng để giới hạn tốc độ. Mặc định là 56", + "rate_limit_subnet_len_ipv6_error": "Độ dài tiền tố mạng con IPv6 phải nằm trong khoảng từ 0 đến 128", + "form_enter_rate_limit_subnet_len": "Nhập độ dài tiền tố mạng con để giới hạn tốc độ", + "rate_limit_whitelist": "Danh sách cho phép giới hạn tỷ lệ", + "rate_limit_whitelist_desc": "Địa chỉ IP bị loại trừ khỏi giới hạn tốc độ", + "rate_limit_whitelist_placeholder": "Nhập một địa chỉ IP trên mỗi dòng", "blocking_ipv4_desc": "Địa chỉ IP được trả lại cho một yêu cầu A bị chặn", "blocking_ipv6_desc": "Địa chỉ IP được trả lại cho một yêu cầu AAA bị chặn", "blocking_mode_default": "Mặc định: Trả lời với NXDOMAIN khi bị chặn bởi quy tắc kiểu Adblock; phản hồi với địa chỉ IP được chỉ định trong quy tắc khi bị chặn bởi quy tắc / etc / hosts-style", @@ -724,5 +734,8 @@ "wednesday_short": "Thứ 4", "thursday_short": "Thứ 5", "friday_short": "Thứ 6", - "saturday_short": "Thứ 7" + "saturday_short": "Thứ 7", + "upstream_dns_cache_configuration": "Cấu hình bộ nhớ đệm upstream của các máy chủ DNS", + "enable_upstream_dns_cache": "Bật bộ nhớ cache cho cấu hình ngược dòng của máy chủ upstream của khách hàng này", + "dns_cache_size": "Kích thước bộ nhớ cache DNS, tính bằng byte" } diff --git a/client/src/__locales/zh-cn.json b/client/src/__locales/zh-cn.json index 26a2c3a0..8080c8db 100644 --- a/client/src/__locales/zh-cn.json +++ b/client/src/__locales/zh-cn.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "为 EDNS 使用自定义 IP", "edns_use_custom_ip_desc": "允许为 EDNS 使用自定义 IP", "rate_limit_desc": "每个客户端每秒钟查询次数的限制。设置为 0 意味着不限制。", + "rate_limit_subnet_len_ipv4": "IPv4 地址子网前缀长度", + "rate_limit_subnet_len_ipv4_desc": "用于速率限制的 IPv4 地址子网前缀长度。默认为 24", + "rate_limit_subnet_len_ipv4_error": "IPv4 子网前缀长度应介于 0 到 32 之间", + "rate_limit_subnet_len_ipv6": "IPv6 地址子网前缀长度", + "rate_limit_subnet_len_ipv6_desc": "用于速率限制的 IPv6 地址子网前缀长度。默认为 56", + "rate_limit_subnet_len_ipv6_error": "IPv6 子网前缀长度应介于 0 到 128 之间", + "form_enter_rate_limit_subnet_len": "输入用于速率限制的子网前缀长度", + "rate_limit_whitelist": "速率限制白名单", + "rate_limit_whitelist_desc": "排除在速率限制之外的 IP 地址", + "rate_limit_whitelist_placeholder": "每行输入一个 IP 地址", "blocking_ipv4_desc": "拦截 A 记录请求返回的 IP 地址", "blocking_ipv6_desc": "拦截 AAAA 记录请求返回的 IP 地址", "blocking_mode_default": "默认:被 Adblock 规则拦截时反应为零 IP 地址(A记录:0.0.0.0;AAAA记录:::);被 /etc/hosts 规则拦截时反应为规则中指定 IP 地址", @@ -724,5 +734,8 @@ "wednesday_short": "周三", "thursday_short": "周四", "friday_short": "周五", - "saturday_short": "周六" + "saturday_short": "周六", + "upstream_dns_cache_configuration": "上游 DNS 缓存配置", + "enable_upstream_dns_cache": "为该客户端的自定义上游配置启用 DNS 缓存", + "dns_cache_size": "DNS 缓存大小,单位:字节" } diff --git a/client/src/__locales/zh-tw.json b/client/src/__locales/zh-tw.json index 26b0ed6c..def8ed3d 100644 --- a/client/src/__locales/zh-tw.json +++ b/client/src/__locales/zh-tw.json @@ -310,6 +310,16 @@ "edns_use_custom_ip": "為 EDNS 使用自訂的 IP", "edns_use_custom_ip_desc": "允許為 EDNS 使用自訂的 IP", "rate_limit_desc": "每個用戶端被允許的每秒請求之數量。設定它為 0 表示無限制。", + "rate_limit_subnet_len_ipv4": "IPv4 位址的子網路前綴長度", + "rate_limit_subnet_len_ipv4_desc": "用於速率限制的 IPv4 位址的子網路前綴長度。預設值為 24", + "rate_limit_subnet_len_ipv4_error": "IPv4 子網路前綴長度應在 0 至 32 之間", + "rate_limit_subnet_len_ipv6": "IPv6 位址的子網路前綴長度", + "rate_limit_subnet_len_ipv6_desc": "用於速率限制的 IPv6 位址的子網路前綴長度。預設值為 56", + "rate_limit_subnet_len_ipv6_error": "IPv6 子網路前綴長度應在 0 至 128 之間", + "form_enter_rate_limit_subnet_len": "輸入用於速率限制的子網路前綴長度", + "rate_limit_whitelist": "速率限制允許清單", + "rate_limit_whitelist_desc": "從速率限制中排除的 IP 位址", + "rate_limit_whitelist_placeholder": "每行輸入一個 IP 位址", "blocking_ipv4_desc": "要被返回給已封鎖的 A 請求之 IP 位址", "blocking_ipv6_desc": "要被返回給已封鎖的 AAAA 請求之 IP 位址", "blocking_mode_default": "預設:當被 AdBlock 樣式的規則封鎖時,以零值 IP 位址(0.0.0.0 供 A;:: 供 AAAA)回覆;當被 /etc/hosts 樣式的規則封鎖時,以在該規則中之已明確指定的 IP 位址回覆", @@ -724,5 +734,8 @@ "wednesday_short": "週三", "thursday_short": "週四", "friday_short": "週五", - "saturday_short": "週六" + "saturday_short": "週六", + "upstream_dns_cache_configuration": "上游 DNS 快取設定", + "enable_upstream_dns_cache": "啟用本用戶端自訂上游配置的 DNS 快取", + "dns_cache_size": "DNS 快取大小,單位:位元" } diff --git a/client/src/actions/dnsConfig.js b/client/src/actions/dnsConfig.js index 47823f66..ce8560cf 100644 --- a/client/src/actions/dnsConfig.js +++ b/client/src/actions/dnsConfig.js @@ -62,6 +62,10 @@ export const setDnsConfig = (config) => async (dispatch) => { data.upstream_dns = splitByNewLine(config.upstream_dns); hasDnsSettings = true; } + if (Object.prototype.hasOwnProperty.call(data, 'ratelimit_whitelist')) { + data.ratelimit_whitelist = splitByNewLine(config.ratelimit_whitelist); + hasDnsSettings = true; + } await apiClient.setDnsConfig(data); diff --git a/client/src/actions/index.js b/client/src/actions/index.js index c577011f..e0e50841 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -338,6 +338,40 @@ export const getDnsStatus = () => async (dispatch) => { } }; +export const timerStatusRequest = createAction('TIMER_STATUS_REQUEST'); +export const timerStatusFailure = createAction('TIMER_STATUS_FAILURE'); +export const timerStatusSuccess = createAction('TIMER_STATUS_SUCCESS'); + +export const getTimerStatus = () => async (dispatch) => { + dispatch(timerStatusRequest()); + + const handleRequestError = () => { + dispatch(addErrorToast({ error: 'dns_status_error' })); + dispatch(dnsStatusFailure()); + window.location.reload(true); + }; + + const handleRequestSuccess = (response) => { + const dnsStatus = response.data; + if (dnsStatus.protection_disabled_duration === 0) { + dnsStatus.protection_disabled_duration = null; + } + const { running } = dnsStatus; + const runningStatus = dnsStatus && running; + if (runningStatus === true) { + dispatch(timerStatusSuccess(dnsStatus)); + } else { + dispatch(setDnsRunningStatus(running)); + } + }; + + try { + checkStatus(handleRequestSuccess, handleRequestError); + } catch (error) { + handleRequestError(); + } +}; + export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST'); export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE'); export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS'); diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js index 797bf1bc..9cf0083d 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -28,7 +28,7 @@ import { } from '../../helpers/constants'; import { getLogsUrlParams, setHtmlLangAttr, setUITheme } from '../../helpers/helpers'; import Header from '../Header'; -import { changeLanguage, getDnsStatus } from '../../actions'; +import { changeLanguage, getDnsStatus, getTimerStatus } from '../../actions'; import Dashboard from '../../containers/Dashboard'; import SetupGuide from '../../containers/SetupGuide'; @@ -126,6 +126,18 @@ const App = () => { useEffect(() => { dispatch(getDnsStatus()); + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + dispatch(getTimerStatus()); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; }, []); const setLanguage = () => { diff --git a/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js index 9f65986f..f4744a5a 100644 --- a/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js @@ -79,6 +79,10 @@ const ClientsTable = ({ } else { config.tags = []; } + + if (typeof values.upstreams_cache_size === 'string') { + config.upstreams_cache_size = 0; + } } if (modalType === MODAL_TYPE.EDIT_FILTERS) { diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 652957d0..ba4ec4b3 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -12,8 +12,13 @@ import i18n from '../../../i18n'; import Tabs from '../../ui/Tabs'; import Examples from '../Dns/Upstream/Examples'; import { ScheduleForm } from '../../Filters/Services/ScheduleForm'; -import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers'; import { + toggleAllServices, + trimLinesAndRemoveEmpty, + captitalizeWords, +} from '../../../helpers/helpers'; +import { + toNumber, renderInputField, renderGroupField, CheckboxField, @@ -21,7 +26,7 @@ import { renderTextareaField, } from '../../../helpers/form'; import { validateClientId, validateRequiredValue } from '../../../helpers/validators'; -import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants'; +import { CLIENT_ID_LINK, FORM_NAME, UINT32_RANGE } from '../../../helpers/constants'; import './Service.css'; const settingsCheckboxes = [ @@ -307,6 +312,35 @@ let Form = (props) => { normalizeOnBlur={trimLinesAndRemoveEmpty} /> +
+ {t('upstream_dns_cache_configuration')} +
+
+ +
+
+ + +
, }, }; diff --git a/client/src/components/Settings/Dhcp/Leases.js b/client/src/components/Settings/Dhcp/Leases.js index 96ca8852..9b245dbe 100644 --- a/client/src/components/Settings/Dhcp/Leases.js +++ b/client/src/components/Settings/Dhcp/Leases.js @@ -27,7 +27,7 @@ class Leases extends Component {
+
+
+ +
+ rate_limit_subnet_len_ipv4_desc +
+ +
+
+
+
+ +
+ rate_limit_subnet_len_ipv6_desc +
+ +
+
+
+
+ +
+ rate_limit_whitelist_desc +
+ +
+
{ const { blocking_mode, ratelimit, + ratelimit_subnet_len_ipv4, + ratelimit_subnet_len_ipv6, + ratelimit_whitelist, blocking_ipv4, blocking_ipv6, blocked_response_ttl, @@ -36,6 +39,9 @@ const Config = () => {
|\x00]+\\) export const R_CLIENT_ID = /^[a-z0-9-]{1,63}$/; +export const R_IPV4_SUBNET = /^([0-9]|[1-2][0-9]|3[0-2])?$/; + +export const R_IPV6_SUBNET = /^([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])?$/; + export const MIN_PASSWORD_LENGTH = 8; export const MAX_PASSWORD_LENGTH = 72; diff --git a/client/src/helpers/filters/filters.js b/client/src/helpers/filters/filters.js index e38c8c2e..a1bf0555 100644 --- a/client/src/helpers/filters/filters.js +++ b/client/src/helpers/filters/filters.js @@ -190,6 +190,12 @@ export default { "homepage": "https://github.com/hagezi/dns-blocklists#piracy", "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_46.txt" }, + "hagezi_encrypted_dns_vpn_tor_proxy_bypass": { + "name": "HaGeZi's Encrypted DNS/VPN/TOR/Proxy Bypass", + "categoryId": "security", + "homepage": "https://github.com/hagezi/dns-blocklists#bypass", + "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_52.txt" + }, "hagezi_gambling_blocklist": { "name": "HaGeZi's Gambling Blocklist", "categoryId": "other", diff --git a/client/src/helpers/trackers/trackers.json b/client/src/helpers/trackers/trackers.json index 8f32d0ee..b5016dfb 100644 --- a/client/src/helpers/trackers/trackers.json +++ b/client/src/helpers/trackers/trackers.json @@ -1,5 +1,5 @@ { - "timeUpdated": "2023-11-10T12:55:56.663Z", + "timeUpdated": "2023-12-01T15:24:07.522Z", "categories": { "0": "audio_video_player", "1": "comments", @@ -640,7 +640,8 @@ "name": "AdChina", "categoryId": 4, "url": "http://www.adchina.com/", - "companyId": "alibaba" + "companyId": null, + "source": "AdGuard" }, "adcito": { "name": "Adcito", @@ -1321,6 +1322,13 @@ "companyId": "adobe", "source": "AdGuard" }, + "adobe_experience_league": { + "name": "Adobe Experience League", + "categoryId": 6, + "url": "https://experienceleague.adobe.com/", + "companyId": "adobe", + "source": "AdGuard" + }, "adobe_login": { "name": "Adobe Login", "categoryId": 2, @@ -2281,27 +2289,29 @@ "name": "Alibaba", "categoryId": 8, "url": "http://www.alibaba.com/", - "companyId": "alibaba" + "companyId": "softbank", + "source": "AdGuard" }, "alibaba_cloud": { "name": "Alibaba Cloud", "categoryId": 10, "url": "https://www.alibabacloud.com/", - "companyId": "alibaba", + "companyId": "softbank", "source": "AdGuard" }, "alibaba_ucbrowser": { "name": "UC Browser", "categoryId": 8, "url": "https://ucweb.com/", - "companyId": "alibaba", + "companyId": "softbank", "source": "AdGuard" }, "alipay.com": { "name": "Alipay", "categoryId": 2, - "url": "https://www.alipay.com/", - "companyId": "alibaba" + "url": "https://global.alipay.com/", + "companyId": "softbank", + "source": "AdGuard" }, "alivechat": { "name": "AliveChat", @@ -3767,10 +3777,11 @@ "companyId": "branica" }, "braze": { - "name": "Braze", + "name": "Braze, Inc.", "categoryId": 6, "url": "https://www.braze.com/", - "companyId": "braze_inc" + "companyId": "braze", + "source": "AdGuard" }, "brealtime": { "name": "EMX Digital", @@ -12534,7 +12545,7 @@ "name": "Network Time Protocol", "categoryId": 5, "url": "https://ntp.org/", - "companyId": "ntppool", + "companyId": "network_time_foundation", "source": "AdGuard" }, "nttcom_online_marketing_solutions": { @@ -17062,7 +17073,8 @@ "name": "Taobao", "categoryId": 4, "url": "https://world.taobao.com/", - "companyId": "alibaba" + "companyId": "softbank", + "source": "AdGuard" }, "tapad": { "name": "Tapad", @@ -20431,6 +20443,7 @@ "nedstat.com": "adobe_experience_cloud", "omtrdc.net": "adobe_experience_cloud", "sitestat.com": "adobe_experience_cloud", + "adobedc.net": "adobe_experience_league", "adobelogin.com": "adobe_login", "adobetag.com": "adobe_tagmanager", "typekit.com": "adobe_typekit", @@ -21046,6 +21059,7 @@ "brandwire.tv": "brandwire.tv", "branica.com": "branica", "appboycdn.com": "braze", + "braze.com": "braze", "brealtime.com": "brealtime", "bridgetrack.com": "bridgetrack", "brightcove.com": "brightcove", @@ -23140,8 +23154,11 @@ "s-microsoft.com": "microsoft", "trouter.io": "microsoft", "windows.net": "microsoft", + "aka.ms": "microsoft", + "microsoftazuread-sso.com": "microsoft", "bingapis.com": "microsoft", "msauth.net": "microsoft", + "msauthimages.net": "microsoft", "msftauth.net": "microsoft", "msftstatic.com": "microsoft", "msidentity.com": "microsoft", @@ -23232,10 +23249,13 @@ "mrpdata.net": "mrpdata", "mrskincash.com": "mrskincash", "a-msedge.net": "msedge", + "b-msedge.net": "msedge", "e-msedge.net": "msedge", + "k-msedge.net": "msedge", "l-msedge.net": "msedge", "s-msedge.net": "msedge", "t-msedge.net": "msedge", + "wac-msedge.net": "msedge", "msn.com": "msn", "s-msn.com": "msn", "musculahq.appspot.com": "muscula", diff --git a/client/src/helpers/validators.js b/client/src/helpers/validators.js index c5aa4618..e9d100a8 100644 --- a/client/src/helpers/validators.js +++ b/client/src/helpers/validators.js @@ -15,6 +15,8 @@ import { R_DOMAIN, MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, + R_IPV4_SUBNET, + R_IPV6_SUBNET, } from './constants'; import { ip4ToInt, isValidAbsolutePath } from './form'; import { isIpInCidr, parseSubnetMask } from './helpers'; @@ -365,3 +367,25 @@ export const validateIpGateway = (value, allValues) => { } return undefined; }; + +/** + * @param value {string} + * @returns {Function} + */ +export const validateIPv4Subnet = (value) => { + if (!R_IPV4_SUBNET.test(value)) { + return i18next.t('rate_limit_subnet_len_ipv4_error'); + } + return undefined; +}; + +/** + * @param value {string} + * @returns {Function} + */ +export const validateIPv6Subnet = (value) => { + if (!R_IPV6_SUBNET.test(value)) { + return i18next.t('rate_limit_subnet_len_ipv6_error'); + } + return undefined; +}; diff --git a/client/src/reducers/dashboard.js b/client/src/reducers/dashboard.js index f23717f9..f47a1732 100644 --- a/client/src/reducers/dashboard.js +++ b/client/src/reducers/dashboard.js @@ -44,6 +44,19 @@ const dashboard = handleActions( return newState; }, + [actions.timerStatusSuccess]: (state, { payload }) => { + const { + protection_enabled: protectionEnabled, + protection_disabled_duration: protectionDisabledDuration, + } = payload; + const newState = { + ...state, + protectionEnabled, + protectionDisabledDuration, + }; + + return newState; + }, [actions.getVersionRequest]: (state) => ({ ...state, diff --git a/client/src/reducers/dhcp.js b/client/src/reducers/dhcp.js index 47768b50..877451a5 100644 --- a/client/src/reducers/dhcp.js +++ b/client/src/reducers/dhcp.js @@ -128,8 +128,7 @@ const dhcp = handleActions( const newState = { ...state, isModalOpen: !state.isModalOpen, - modalType: payload?.type || '', - leaseModalConfig: payload?.config, + leaseModalConfig: payload, }; return newState; }, diff --git a/client/src/reducers/dnsConfig.js b/client/src/reducers/dnsConfig.js index 49224b6b..d877b6c4 100644 --- a/client/src/reducers/dnsConfig.js +++ b/client/src/reducers/dnsConfig.js @@ -18,6 +18,7 @@ const dnsConfig = handleActions( fallback_dns, bootstrap_dns, local_ptr_upstreams, + ratelimit_whitelist, ...values } = payload; @@ -30,6 +31,7 @@ const dnsConfig = handleActions( fallback_dns: (fallback_dns && fallback_dns.join('\n')) || '', bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\n')) || '', local_ptr_upstreams: (local_ptr_upstreams && local_ptr_upstreams.join('\n')) || '', + ratelimit_whitelist: (ratelimit_whitelist && ratelimit_whitelist.join('\n')) || '', processingGetConfig: false, }; }, diff --git a/go.mod b/go.mod index cbc0ea9f..ca03cd70 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,20 @@ module github.com/AdguardTeam/AdGuardHome go 1.20 require ( - github.com/AdguardTeam/dnsproxy v0.57.3 - github.com/AdguardTeam/golibs v0.17.2 + github.com/AdguardTeam/dnsproxy v0.60.0 + github.com/AdguardTeam/golibs v0.18.0 github.com/AdguardTeam/urlfilter v0.17.3 github.com/NYTimes/gziphandler v1.1.1 github.com/ameshkov/dnscrypt/v2 v2.2.7 github.com/bluele/gcache v0.0.2 github.com/digineo/go-ipset/v2 v2.2.1 github.com/dimfeld/httptreemux/v5 v5.5.0 - github.com/fsnotify/fsnotify v1.6.0 + github.com/fsnotify/fsnotify v1.7.0 github.com/go-ping/ping v1.1.0 github.com/google/go-cmp v0.6.0 github.com/google/gopacket v1.1.19 github.com/google/renameio/v2 v2.0.0 - github.com/google/uuid v1.3.1 + github.com/google/uuid v1.4.0 github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 github.com/kardianos/service v1.2.2 @@ -27,14 +27,14 @@ require ( // own code for that. Perhaps, use gopacket. github.com/mdlayher/raw v0.1.0 github.com/miekg/dns v1.1.56 - github.com/quic-go/quic-go v0.39.2 + github.com/quic-go/quic-go v0.40.0 github.com/stretchr/testify v1.8.4 github.com/ti-mo/netfilter v0.5.1 - go.etcd.io/bbolt v1.3.7 - golang.org/x/crypto v0.14.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.17.0 - golang.org/x/sys v0.13.0 + go.etcd.io/bbolt v1.3.8 + golang.org/x/crypto v0.15.0 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/net v0.18.0 + golang.org/x/sys v0.14.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 howett.net/plist v1.0.0 @@ -47,19 +47,20 @@ require ( github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect - github.com/mdlayher/socket v0.5.0 // indirect - github.com/onsi/ginkgo/v2 v2.13.0 // indirect + github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect + // TODO(a.garipov): Upgrade to v0.5.0 once we switch to Go 1.21+. + github.com/mdlayher/socket v0.4.1 // indirect + github.com/onsi/ginkgo/v2 v2.13.1 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.3.4 // indirect + github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect go.uber.org/mock v0.3.0 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.14.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index 22ce0750..20697702 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/AdguardTeam/dnsproxy v0.57.3 h1:0v7D+LQrOL2k2fvkG3Ft3Cn3ayUsvAdlOlJR+gLxSGA= -github.com/AdguardTeam/dnsproxy v0.57.3/go.mod h1:ZvkbM71HwpilgkCnTubDiR4Ba6x5Qvnhy2iasMWaTDM= -github.com/AdguardTeam/golibs v0.17.2 h1:vg6wHMjUKscnyPGRvxS5kAt7Uw4YxcJiITZliZ476W8= -github.com/AdguardTeam/golibs v0.17.2/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U= +github.com/AdguardTeam/dnsproxy v0.60.0 h1:0gLYoFyWRhQ1MP6g6AqqZXL5/h2QM4FE1aybUZ5HGuE= +github.com/AdguardTeam/dnsproxy v0.60.0/go.mod h1:B7FvvTFQZBfey1cJXQo732EyCLX6xj4JqrciCawATzg= +github.com/AdguardTeam/golibs v0.18.0 h1:ckS2YK7t2Ub6UkXl0fnreVaM15Zb07Hh1gmFqttjpWg= +github.com/AdguardTeam/golibs v0.18.0/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U= github.com/AdguardTeam/urlfilter v0.17.3 h1:fg/ObbnO0Cv6aw0tW6N/ETDMhhNvmcUUOZ7HlmKC3rw= github.com/AdguardTeam/urlfilter v0.17.3/go.mod h1:Jru7jFfeH2CoDf150uDs+rRYcZBzHHBz05r9REyDKyE= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= @@ -25,9 +25,9 @@ github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1M github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU= github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ= github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= @@ -41,13 +41,13 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= -github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM= github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= @@ -71,14 +71,14 @@ github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU github.com/mdlayher/raw v0.1.0 h1:K4PFMVy+AFsp0Zdlrts7yNhxc/uXoPVHi9RzRvtZF2Y= github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5s9Sg= github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E= -github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= -github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= +github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -92,10 +92,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= -github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.39.2 h1:hmwAf8zAHlvan0Y5PXxeeBFZEW17IW99sXLry8I2kjk= -github.com/quic-go/quic-go v0.39.2/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= +github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= +github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw= +github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -112,32 +112,32 @@ github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYm github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -148,19 +148,18 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= diff --git a/internal/aghnet/hostscontainer.go b/internal/aghnet/hostscontainer.go index 0d9a4bcc..e27b115f 100644 --- a/internal/aghnet/hostscontainer.go +++ b/internal/aghnet/hostscontainer.go @@ -1,14 +1,17 @@ package aghnet import ( + "context" "fmt" "io" "io/fs" "net/netip" "path" + "strings" "sync/atomic" "github.com/AdguardTeam/AdGuardHome/internal/aghos" + "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/hostsfile" "github.com/AdguardTeam/golibs/log" @@ -141,13 +144,9 @@ func NewHostsContainer( func (hc *HostsContainer) Close() (err error) { log.Debug("%s: closing", hostsContainerPrefix) - err = hc.watcher.Close() - if err != nil { - err = fmt.Errorf("closing fs watcher: %w", err) - - // Go on and close the container either way. - } + err = errors.Annotate(hc.watcher.Close(), "closing fs watcher: %w") + // Go on and close the container either way. close(hc.done) return err @@ -319,3 +318,39 @@ func (hc *HostsContainer) refresh() (err error) { return nil } + +// type check +var _ upstream.Resolver = (*HostsContainer)(nil) + +// LookupNetIP implements the [upstream.Resolver] interface for *HostsContainer. +func (hc *HostsContainer) LookupNetIP( + ctx context.Context, + network string, + hostname string, +) (addrs []netip.Addr, err error) { + // TODO(e.burkov): Think of extracting this logic to a golibs function if + // needed anywhere else. + var isDesiredProto func(ip netip.Addr) (ok bool) + switch network { + case "ip4": + isDesiredProto = (netip.Addr).Is4 + case "ip6": + isDesiredProto = (netip.Addr).Is6 + case "ip": + isDesiredProto = func(ip netip.Addr) (ok bool) { return true } + default: + return nil, fmt.Errorf("unsupported network: %q", network) + } + + idx := hc.current.Load() + recs := idx.names[strings.ToLower(hostname)] + + addrs = make([]netip.Addr, 0, len(recs)) + for _, rec := range recs { + if isDesiredProto(rec.Addr) { + addrs = append(addrs, rec.Addr) + } + } + + return slices.Clip(addrs), nil +} diff --git a/internal/aghnet/net.go b/internal/aghnet/net.go index a0489dd8..ac9fb7bd 100644 --- a/internal/aghnet/net.go +++ b/internal/aghnet/net.go @@ -10,9 +10,11 @@ import ( "net" "net/netip" "net/url" + "strings" "syscall" "github.com/AdguardTeam/AdGuardHome/internal/aghos" + "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" ) @@ -307,6 +309,50 @@ func ParseAddrPort(s string, defaultPort uint16) (ipp netip.AddrPort, err error) return ipp, nil } +// ParseSubnet parses s either as a CIDR prefix itself, or as an IP address, +// returning the corresponding single-IP CIDR prefix. +// +// TODO(e.burkov): Taken from dnsproxy, move to golibs. +func ParseSubnet(s string) (p netip.Prefix, err error) { + if strings.Contains(s, "/") { + p, err = netip.ParsePrefix(s) + if err != nil { + return netip.Prefix{}, err + } + } else { + var ip netip.Addr + ip, err = netip.ParseAddr(s) + if err != nil { + return netip.Prefix{}, err + } + + p = netip.PrefixFrom(ip, ip.BitLen()) + } + + return p, nil +} + +// ParseBootstraps returns the slice of upstream resolvers parsed from addrs. +// It additionally returns the closers for each resolver, that should be closed +// after use. +func ParseBootstraps( + addrs []string, + opts *upstream.Options, +) (boots []*upstream.UpstreamResolver, err error) { + boots = make([]*upstream.UpstreamResolver, 0, len(boots)) + for i, b := range addrs { + var r *upstream.UpstreamResolver + r, err = upstream.NewUpstreamResolver(b, opts) + if err != nil { + return nil, fmt.Errorf("bootstrap at index %d: %w", i, err) + } + + boots = append(boots, r) + } + + return boots, nil +} + // BroadcastFromPref calculates the broadcast IP address for p. func BroadcastFromPref(p netip.Prefix) (bc netip.Addr) { bc = p.Addr().Unmap() diff --git a/internal/aghtest/interface.go b/internal/aghtest/interface.go index 10789d8e..f21f6e57 100644 --- a/internal/aghtest/interface.go +++ b/internal/aghtest/interface.go @@ -11,6 +11,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/next/agh" "github.com/AdguardTeam/AdGuardHome/internal/rdns" "github.com/AdguardTeam/AdGuardHome/internal/whois" + "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/miekg/dns" ) @@ -116,6 +117,26 @@ func (p *AddressUpdater) UpdateAddress(ip netip.Addr, host string, info *whois.I p.OnUpdateAddress(ip, host, info) } +// Package dnsforward + +// ClientsContainer is a fake [dnsforward.ClientsContainer] implementation for +// tests. +type ClientsContainer struct { + OnUpstreamConfigByID func( + id string, + boot upstream.Resolver, + ) (conf *proxy.CustomUpstreamConfig, err error) +} + +// UpstreamConfigByID implements the [dnsforward.ClientsContainer] interface +// for *ClientsContainer. +func (c *ClientsContainer) UpstreamConfigByID( + id string, + boot upstream.Resolver, +) (conf *proxy.CustomUpstreamConfig, err error) { + return c.OnUpstreamConfigByID(id, boot) +} + // Package filtering // Resolver is a fake [filtering.Resolver] implementation for tests. diff --git a/internal/aghtest/interface_test.go b/internal/aghtest/interface_test.go index a17c5e67..c1a376ba 100644 --- a/internal/aghtest/interface_test.go +++ b/internal/aghtest/interface_test.go @@ -2,6 +2,7 @@ package aghtest_test import ( "github.com/AdguardTeam/AdGuardHome/internal/aghtest" + "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/filtering" ) @@ -9,3 +10,6 @@ import ( // type check var _ filtering.Resolver = (*aghtest.Resolver)(nil) + +// type check +var _ dnsforward.ClientsContainer = (*aghtest.ClientsContainer)(nil) diff --git a/internal/dhcpd/config.go b/internal/dhcpd/config.go index 92179a59..d11d9342 100644 --- a/internal/dhcpd/config.go +++ b/internal/dhcpd/config.go @@ -8,6 +8,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" ) @@ -49,16 +50,16 @@ type ServerConfig struct { // DHCPServer - DHCP server interface type DHCPServer interface { // ResetLeases resets leases. - ResetLeases(leases []*Lease) (err error) + ResetLeases(leases []*dhcpsvc.Lease) (err error) // GetLeases returns deep clones of the current leases. - GetLeases(flags GetLeasesFlags) (leases []*Lease) + GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) // AddStaticLease - add a static lease - AddStaticLease(l *Lease) (err error) + AddStaticLease(l *dhcpsvc.Lease) (err error) // RemoveStaticLease - remove a static lease - RemoveStaticLease(l *Lease) (err error) + RemoveStaticLease(l *dhcpsvc.Lease) (err error) // UpdateStaticLease updates IP, hostname of the lease. - UpdateStaticLease(l *Lease) (err error) + UpdateStaticLease(l *dhcpsvc.Lease) (err error) // FindMACbyIP returns a MAC address by the IP address of its lease, if // there is one. @@ -81,7 +82,7 @@ type DHCPServer interface { Start() (err error) // Stop - stop server Stop() (err error) - getLeasesRef() []*Lease + getLeasesRef() []*dhcpsvc.Lease } // V4ServerConf - server configuration diff --git a/internal/dhcpd/db.go b/internal/dhcpd/db.go index 8d8c90fa..9862d0e5 100644 --- a/internal/dhcpd/db.go +++ b/internal/dhcpd/db.go @@ -5,9 +5,13 @@ package dhcpd import ( "encoding/json" "fmt" + "net" + "net/netip" "os" "strings" + "time" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/google/renameio/v2/maybe" @@ -28,7 +32,60 @@ type dataLeases struct { Version int `json:"version"` // Leases is the list containing stored DHCP leases. - Leases []*Lease `json:"leases"` + Leases []*dbLease `json:"leases"` +} + +// dbLease is the structure of stored lease. +type dbLease struct { + Expiry string `json:"expires"` + IP netip.Addr `json:"ip"` + Hostname string `json:"hostname"` + HWAddr string `json:"mac"` + IsStatic bool `json:"static"` +} + +// fromLease converts *dhcpsvc.Lease to *dbLease. +func fromLease(l *dhcpsvc.Lease) (dl *dbLease) { + var expiryStr string + if !l.IsStatic { + // The front-end is waiting for RFC 3999 format of the time value. It + // also shouldn't got an Expiry field for static leases. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2692. + expiryStr = l.Expiry.Format(time.RFC3339) + } + + return &dbLease{ + Expiry: expiryStr, + Hostname: l.Hostname, + HWAddr: l.HWAddr.String(), + IP: l.IP, + IsStatic: l.IsStatic, + } +} + +// toLease converts *dbLease to *dhcpsvc.Lease. +func (dl *dbLease) toLease() (l *dhcpsvc.Lease, err error) { + mac, err := net.ParseMAC(dl.HWAddr) + if err != nil { + return nil, fmt.Errorf("parsing hardware address: %w", err) + } + + expiry := time.Time{} + if !dl.IsStatic { + expiry, err = time.Parse(time.RFC3339, dl.Expiry) + if err != nil { + return nil, fmt.Errorf("parsing expiry time: %w", err) + } + } + + return &dhcpsvc.Lease{ + Expiry: expiry, + IP: dl.IP, + Hostname: dl.Hostname, + HWAddr: mac, + IsStatic: dl.IsStatic, + }, nil } // dbLoad loads stored leases. @@ -49,15 +106,22 @@ func (s *server) dbLoad() (err error) { } leases := dl.Leases - - leases4 := []*Lease{} - leases6 := []*Lease{} + leases4 := []*dhcpsvc.Lease{} + leases6 := []*dhcpsvc.Lease{} for _, l := range leases { - if l.IP.Is4() { - leases4 = append(leases4, l) + var lease *dhcpsvc.Lease + lease, err = l.toLease() + if err != nil { + log.Info("dhcp: invalid lease: %s", err) + + continue + } + + if lease.IP.Is4() { + leases4 = append(leases4, lease) } else { - leases6 = append(leases6, l) + leases6 = append(leases6, lease) } } @@ -73,8 +137,12 @@ func (s *server) dbLoad() (err error) { } } - log.Info("dhcp: loaded leases v4:%d v6:%d total-read:%d from DB", - len(leases4), len(leases6), len(leases)) + log.Info( + "dhcp: loaded leases v4:%d v6:%d total-read:%d from DB", + len(leases4), + len(leases6), + len(leases), + ) return nil } @@ -83,24 +151,26 @@ func (s *server) dbLoad() (err error) { func (s *server) dbStore() (err error) { // Use an empty slice here as opposed to nil so that it doesn't write // "null" into the database file if leases are empty. - leases := []*Lease{} + leases := []*dbLease{} - leases4 := s.srv4.getLeasesRef() - leases = append(leases, leases4...) + for _, l := range s.srv4.getLeasesRef() { + leases = append(leases, fromLease(l)) + } if s.srv6 != nil { - leases6 := s.srv6.getLeasesRef() - leases = append(leases, leases6...) + for _, l := range s.srv6.getLeasesRef() { + leases = append(leases, fromLease(l)) + } } return writeDB(s.conf.dbFilePath, leases) } // writeDB writes leases to file at path. -func writeDB(path string, leases []*Lease) (err error) { +func writeDB(path string, leases []*dbLease) (err error) { defer func() { err = errors.Annotate(err, "writing db: %w") }() - slices.SortFunc(leases, func(a, b *Lease) (res int) { + slices.SortFunc(leases, func(a, b *dbLease) (res int) { return strings.Compare(a.Hostname, b.Hostname) }) diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index e4ee14a9..edc3d3a4 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -2,7 +2,6 @@ package dhcpd import ( - "encoding/json" "fmt" "net" "net/netip" @@ -12,7 +11,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/timeutil" - "golang.org/x/exp/slices" ) const ( @@ -29,105 +27,6 @@ const ( defaultBackoff time.Duration = 500 * time.Millisecond ) -// Lease contains the necessary information about a DHCP lease. It's used as is -// in the database, so don't change it until it's absolutely necessary, see -// [dataVersion]. -// -// TODO(e.burkov): Unexport it and use [dhcpsvc.Lease]. -type Lease struct { - // Expiry is the expiration time of the lease. - Expiry time.Time `json:"expires"` - - // Hostname of the client. - Hostname string `json:"hostname"` - - // HWAddr is the physical hardware address (MAC address). - HWAddr net.HardwareAddr `json:"mac"` - - // IP is the IP address leased to the client. - IP netip.Addr `json:"ip"` - - // IsStatic defines if the lease is static. - IsStatic bool `json:"static"` -} - -// Clone returns a deep copy of l. -func (l *Lease) Clone() (clone *Lease) { - if l == nil { - return nil - } - - return &Lease{ - Expiry: l.Expiry, - Hostname: l.Hostname, - HWAddr: slices.Clone(l.HWAddr), - IP: l.IP, - IsStatic: l.IsStatic, - } -} - -// IsBlocklisted returns true if the lease is blocklisted. -// -// TODO(a.garipov): Just make it a boolean field. -func (l *Lease) IsBlocklisted() (ok bool) { - if len(l.HWAddr) == 0 { - return false - } - - for _, b := range l.HWAddr { - if b != 0 { - return false - } - } - - return true -} - -// MarshalJSON implements the json.Marshaler interface for Lease. -func (l Lease) MarshalJSON() ([]byte, error) { - var expiryStr string - if !l.IsStatic { - // The front-end is waiting for RFC 3999 format of the time - // value. It also shouldn't got an Expiry field for static - // leases. - // - // See https://github.com/AdguardTeam/AdGuardHome/issues/2692. - expiryStr = l.Expiry.Format(time.RFC3339) - } - - type lease Lease - return json.Marshal(&struct { - HWAddr string `json:"mac"` - Expiry string `json:"expires,omitempty"` - lease - }{ - HWAddr: l.HWAddr.String(), - Expiry: expiryStr, - lease: lease(l), - }) -} - -// UnmarshalJSON implements the json.Unmarshaler interface for *Lease. -func (l *Lease) UnmarshalJSON(data []byte) (err error) { - type lease Lease - aux := struct { - *lease - HWAddr string `json:"mac"` - }{ - lease: (*lease)(l), - } - if err = json.Unmarshal(data, &aux); err != nil { - return err - } - - l.HWAddr, err = net.ParseMAC(aux.HWAddr) - if err != nil { - return fmt.Errorf("couldn't parse MAC address: %w", err) - } - - return nil -} - // OnLeaseChangedT is a callback for lease changes. type OnLeaseChangedT func(flags int) @@ -370,19 +269,7 @@ func (s *server) Stop() (err error) { // Leases returns the list of active DHCP leases. func (s *server) Leases() (leases []*dhcpsvc.Lease) { - ls := append(s.srv4.GetLeases(LeasesAll), s.srv6.GetLeases(LeasesAll)...) - leases = make([]*dhcpsvc.Lease, len(ls)) - for i, l := range ls { - leases[i] = &dhcpsvc.Lease{ - Expiry: l.Expiry, - Hostname: l.Hostname, - HWAddr: l.HWAddr, - IP: l.IP, - IsStatic: l.IsStatic, - } - } - - return leases + return append(s.srv4.GetLeases(LeasesAll), s.srv6.GetLeases(LeasesAll)...) } // MACByIP returns a MAC address by the IP address of its lease, if there is @@ -414,6 +301,6 @@ func (s *server) IPByHost(host string) (ip netip.Addr) { } // AddStaticLease - add static v4 lease -func (s *server) AddStaticLease(l *Lease) error { +func (s *server) AddStaticLease(l *dhcpsvc.Lease) error { return s.srv4.AddStaticLease(l) } diff --git a/internal/dhcpd/dhcpd_unix_test.go b/internal/dhcpd/dhcpd_unix_test.go index 7eced536..3aea125a 100644 --- a/internal/dhcpd/dhcpd_unix_test.go +++ b/internal/dhcpd/dhcpd_unix_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -44,7 +45,7 @@ func TestDB(t *testing.T) { s.srv6, err = v6Create(V6ServerConf{}) require.NoError(t, err) - leases := []*Lease{{ + leases := []*dhcpsvc.Lease{{ Expiry: time.Now().Add(time.Hour), Hostname: "static-1.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, diff --git a/internal/dhcpd/http_unix.go b/internal/dhcpd/http_unix.go index 622bf1e7..199d1113 100644 --- a/internal/dhcpd/http_unix.go +++ b/internal/dhcpd/http_unix.go @@ -93,13 +93,13 @@ func leasesToStatic(leases []*dhcpsvc.Lease) (static []*leaseStatic) { } // toLease converts leaseStatic to Lease or returns error. -func (l *leaseStatic) toLease() (lease *Lease, err error) { +func (l *leaseStatic) toLease() (lease *dhcpsvc.Lease, err error) { addr, err := net.ParseMAC(l.HWAddr) if err != nil { return nil, fmt.Errorf("couldn't parse MAC address: %w", err) } - return &Lease{ + return &dhcpsvc.Lease{ HWAddr: addr, IP: l.IP, Hostname: l.Hostname, @@ -593,7 +593,7 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) { // parseLease parses a lease from r. If there is no error returns DHCPServer // and *Lease. r must be non-nil. -func (s *server) parseLease(r io.Reader) (srv DHCPServer, lease *Lease, err error) { +func (s *server) parseLease(r io.Reader) (srv DHCPServer, lease *dhcpsvc.Lease, err error) { l := &leaseStatic{} err = json.NewDecoder(r).Decode(l) if err != nil { diff --git a/internal/dhcpd/migrate.go b/internal/dhcpd/migrate.go index 4fa9339f..436ba2dc 100644 --- a/internal/dhcpd/migrate.go +++ b/internal/dhcpd/migrate.go @@ -2,6 +2,7 @@ package dhcpd import ( "encoding/json" + "fmt" "net" "net/netip" "os" @@ -25,9 +26,9 @@ const ( dbFilename = "leases.db" ) -// leaseJSON is the structure of stored lease. +// leaseJSON is the structure of stored lease in a legacy database. // -// Deprecated: Use [Lease]. +// Deprecated: Use [dbLease]. type leaseJSON struct { HWAddr []byte `json:"mac"` IP []byte `json:"ip"` @@ -35,13 +36,28 @@ type leaseJSON struct { Expiry int64 `json:"exp"` } -func normalizeIP(ip net.IP) net.IP { - ip4 := ip.To4() - if ip4 != nil { - return ip4 +// readOldDB reads the old database from the given path. +func readOldDB(path string) (leases []*leaseJSON, err error) { + // #nosec G304 -- Trust this path, since it's taken from the old file name + // relative to the working directory and should generally be considered + // safe. + file, err := os.Open(path) + if errors.Is(err, os.ErrNotExist) { + // Nothing to migrate. + return nil, nil + } else if err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } + defer func() { err = errors.WithDeferred(err, file.Close()) }() + + leases = []*leaseJSON{} + err = json.NewDecoder(file).Decode(&leases) + if err != nil { + return nil, fmt.Errorf("decoding old db: %w", err) } - return ip + return leases, nil } // migrateDB migrates stored leases if necessary. @@ -51,59 +67,50 @@ func migrateDB(conf *ServerConfig) (err error) { oldLeasesPath := filepath.Join(conf.WorkDir, dbFilename) dataDirPath := filepath.Join(conf.DataDir, dataFilename) - // #nosec G304 -- Trust this path, since it's taken from the old file name - // relative to the working directory and should generally be considered - // safe. - file, err := os.Open(oldLeasesPath) - if errors.Is(err, os.ErrNotExist) { + oldLeases, err := readOldDB(oldLeasesPath) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } else if oldLeases == nil { // Nothing to migrate. return nil - } else if err != nil { - // Don't wrap the error since it's informative enough as is. - return err } - ljs := []leaseJSON{} - err = json.NewDecoder(file).Decode(&ljs) - if err != nil { - // Don't wrap the error since it's informative enough as is. - return err - } - - err = file.Close() - if err != nil { - // Don't wrap the error since it's informative enough as is. - return err - } - - leases := []*Lease{} - - for _, lj := range ljs { - lj.IP = normalizeIP(lj.IP) - - ip, ok := netip.AddrFromSlice(lj.IP) + leases := make([]*dbLease, 0, len(oldLeases)) + for _, l := range oldLeases { + l.IP = normalizeIP(l.IP) + ip, ok := netip.AddrFromSlice(l.IP) if !ok { - log.Info("dhcp: invalid IP: %s", lj.IP) + log.Info("dhcp: invalid IP: %s", l.IP) continue } - lease := &Lease{ - Expiry: time.Unix(lj.Expiry, 0), - Hostname: lj.Hostname, - HWAddr: lj.HWAddr, + leases = append(leases, &dbLease{ + Expiry: time.Unix(l.Expiry, 0).Format(time.RFC3339), + Hostname: l.Hostname, + HWAddr: net.HardwareAddr(l.HWAddr).String(), IP: ip, - IsStatic: lj.Expiry == leaseExpireStatic, - } - - leases = append(leases, lease) + IsStatic: l.Expiry == leaseExpireStatic, + }) } err = writeDB(dataDirPath, leases) if err != nil { - // Don't wrap the error since it's informative enough as is. + // Don't wrap the error since an annotation deferred already. return err } return os.Remove(oldLeasesPath) } + +// normalizeIP converts the given IP address to IPv4 if it's IPv4-mapped IPv6, +// or leaves it as is otherwise. +func normalizeIP(ip net.IP) (normalized net.IP) { + normalized = ip.To4() + if normalized != nil { + return normalized + } + + return ip +} diff --git a/internal/dhcpd/migrate_internal_test.go b/internal/dhcpd/migrate_internal_test.go index 2c0e6ecd..4ed99361 100644 --- a/internal/dhcpd/migrate_internal_test.go +++ b/internal/dhcpd/migrate_internal_test.go @@ -2,7 +2,6 @@ package dhcpd import ( "encoding/json" - "net" "net/netip" "os" "path/filepath" @@ -27,16 +26,16 @@ func TestMigrateDB(t *testing.T) { err := os.WriteFile(oldLeasesPath, []byte(testData), 0o644) require.NoError(t, err) - wantLeases := []*Lease{{ - Expiry: time.Time{}, + wantLeases := []*dbLease{{ + Expiry: time.Unix(1, 0).Format(time.RFC3339), Hostname: "test1", - HWAddr: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + HWAddr: "11:22:33:44:55:66", IP: netip.MustParseAddr("1.2.3.4"), IsStatic: true, }, { - Expiry: time.Unix(1231231231, 0), + Expiry: time.Unix(1231231231, 0).Format(time.RFC3339), Hostname: "test2", - HWAddr: net.HardwareAddr{0x66, 0x55, 0x44, 0x33, 0x22, 0x11}, + HWAddr: "66:55:44:33:22:11", IP: netip.MustParseAddr("4.3.2.1"), IsStatic: false, }} @@ -62,12 +61,12 @@ func TestMigrateDB(t *testing.T) { leases := dl.Leases - for i, wl := range wantLeases { - assert.Equal(t, wl.Hostname, leases[i].Hostname) - assert.Equal(t, wl.HWAddr, leases[i].HWAddr) - assert.Equal(t, wl.IP, leases[i].IP) - assert.Equal(t, wl.IsStatic, leases[i].IsStatic) + for i, wantLease := range wantLeases { + assert.Equal(t, wantLease.Hostname, leases[i].Hostname) + assert.Equal(t, wantLease.HWAddr, leases[i].HWAddr) + assert.Equal(t, wantLease.IP, leases[i].IP) + assert.Equal(t, wantLease.IsStatic, leases[i].IsStatic) - require.True(t, wl.Expiry.Equal(leases[i].Expiry)) + require.Equal(t, wantLease.Expiry, leases[i].Expiry) } } diff --git a/internal/dhcpd/v46_windows.go b/internal/dhcpd/v46_windows.go index 2dbe302e..241429c6 100644 --- a/internal/dhcpd/v46_windows.go +++ b/internal/dhcpd/v46_windows.go @@ -7,6 +7,8 @@ package dhcpd import ( "net" "net/netip" + + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" ) type winServer struct{} @@ -14,19 +16,19 @@ type winServer struct{} // type check var _ DHCPServer = winServer{} -func (winServer) ResetLeases(_ []*Lease) (err error) { return nil } -func (winServer) GetLeases(_ GetLeasesFlags) (leases []*Lease) { return nil } -func (winServer) getLeasesRef() []*Lease { return nil } -func (winServer) AddStaticLease(_ *Lease) (err error) { return nil } -func (winServer) RemoveStaticLease(_ *Lease) (err error) { return nil } -func (winServer) UpdateStaticLease(_ *Lease) (err error) { return nil } -func (winServer) FindMACbyIP(_ netip.Addr) (mac net.HardwareAddr) { return nil } -func (winServer) WriteDiskConfig4(_ *V4ServerConf) {} -func (winServer) WriteDiskConfig6(_ *V6ServerConf) {} -func (winServer) Start() (err error) { return nil } -func (winServer) Stop() (err error) { return nil } -func (winServer) HostByIP(_ netip.Addr) (host string) { return "" } -func (winServer) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} } +func (winServer) ResetLeases(_ []*dhcpsvc.Lease) (err error) { return nil } +func (winServer) GetLeases(_ GetLeasesFlags) (leases []*dhcpsvc.Lease) { return nil } +func (winServer) getLeasesRef() []*dhcpsvc.Lease { return nil } +func (winServer) AddStaticLease(_ *dhcpsvc.Lease) (err error) { return nil } +func (winServer) RemoveStaticLease(_ *dhcpsvc.Lease) (err error) { return nil } +func (winServer) UpdateStaticLease(_ *dhcpsvc.Lease) (err error) { return nil } +func (winServer) FindMACbyIP(_ netip.Addr) (mac net.HardwareAddr) { return nil } +func (winServer) WriteDiskConfig4(_ *V4ServerConf) {} +func (winServer) WriteDiskConfig6(_ *V6ServerConf) {} +func (winServer) Start() (err error) { return nil } +func (winServer) Stop() (err error) { return nil } +func (winServer) HostByIP(_ netip.Addr) (host string) { return "" } +func (winServer) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} } func v4Create(_ *V4ServerConf) (s DHCPServer, err error) { return winServer{}, nil } func v6Create(_ V6ServerConf) (s DHCPServer, err error) { return winServer{}, nil } diff --git a/internal/dhcpd/v4_unix.go b/internal/dhcpd/v4_unix.go index 2b78e1cf..3685c2e8 100644 --- a/internal/dhcpd/v4_unix.go +++ b/internal/dhcpd/v4_unix.go @@ -12,6 +12,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" @@ -38,7 +39,7 @@ type v4Server struct { // have intersections with [implicitOpts]. explicitOpts dhcpv4.Options - // leasesLock protects leases, leaseHosts, and leasedOffsets. + // leasesLock protects leases, hostsIndex, ipIndex, and leasedOffsets. leasesLock sync.Mutex // leasedOffsets contains offsets from conf.ipRange.start that have been @@ -46,13 +47,13 @@ type v4Server struct { leasedOffsets *bitSet // leases contains all dynamic and static leases. - leases []*Lease + leases []*dhcpsvc.Lease // hostsIndex is the set of all hostnames of all known DHCP clients. - hostsIndex map[string]*Lease + hostsIndex map[string]*dhcpsvc.Lease // ipIndex is an index of leases by their IP addresses. - ipIndex map[netip.Addr]*Lease + ipIndex map[netip.Addr]*dhcpsvc.Lease } func (s *v4Server) enabled() (ok bool) { @@ -141,7 +142,7 @@ func (s *v4Server) IPByHost(host string) (ip netip.Addr) { } // ResetLeases resets leases. -func (s *v4Server) ResetLeases(leases []*Lease) (err error) { +func (s *v4Server) ResetLeases(leases []*dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() if s.conf == nil { @@ -152,8 +153,8 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) { defer s.leasesLock.Unlock() s.leasedOffsets = newBitSet() - s.hostsIndex = make(map[string]*Lease, len(leases)) - s.ipIndex = make(map[netip.Addr]*Lease, len(leases)) + s.hostsIndex = make(map[string]*dhcpsvc.Lease, len(leases)) + s.ipIndex = make(map[netip.Addr]*dhcpsvc.Lease, len(leases)) s.leases = nil for _, l := range leases { @@ -173,14 +174,14 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) { } // getLeasesRef returns the actual leases slice. For internal use only. -func (s *v4Server) getLeasesRef() []*Lease { +func (s *v4Server) getLeasesRef() []*dhcpsvc.Lease { return s.leases } // isBlocklisted returns true if this lease holds a blocklisted IP. // // TODO(a.garipov): Make a method of *Lease? -func (s *v4Server) isBlocklisted(l *Lease) (ok bool) { +func (s *v4Server) isBlocklisted(l *dhcpsvc.Lease) (ok bool) { if len(l.HWAddr) == 0 { return false } @@ -196,11 +197,11 @@ func (s *v4Server) isBlocklisted(l *Lease) (ok bool) { // GetLeases returns the list of current DHCP leases. It is safe for concurrent // use. -func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) { +func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) { // The function shouldn't return nil, because zero-length slice behaves // differently in cases like marshalling. Our front-end also requires // a non-nil value in the response. - leases = []*Lease{} + leases = []*dhcpsvc.Lease{} getDynamic := flags&LeasesDynamic != 0 getStatic := flags&LeasesStatic != 0 @@ -248,7 +249,7 @@ func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) { const defaultHwAddrLen = 6 // Add the specified IP to the black list for a time period -func (s *v4Server) blocklistLease(l *Lease) { +func (s *v4Server) blocklistLease(l *dhcpsvc.Lease) { l.HWAddr = make(net.HardwareAddr, defaultHwAddrLen) l.Hostname = "" l.Expiry = time.Now().Add(s.conf.leaseTime) @@ -284,7 +285,7 @@ func (s *v4Server) rmLeaseByIndex(i int) { // Return error if a static lease is found // // TODO(s.chzhen): Refactor the code. -func (s *v4Server) rmDynamicLease(lease *Lease) (err error) { +func (s *v4Server) rmDynamicLease(lease *dhcpsvc.Lease) (err error) { for i, l := range s.leases { isStatic := l.IsStatic @@ -320,7 +321,7 @@ const ( ) // addLease adds a dynamic or static lease. -func (s *v4Server) addLease(l *Lease) (err error) { +func (s *v4Server) addLease(l *dhcpsvc.Lease) (err error) { r := s.conf.ipRange leaseIP := net.IP(l.IP.AsSlice()) offset, inOffset := r.offset(leaseIP) @@ -352,7 +353,7 @@ func (s *v4Server) addLease(l *Lease) (err error) { } // rmLease removes a lease with the same properties. -func (s *v4Server) rmLease(lease *Lease) (err error) { +func (s *v4Server) rmLease(lease *dhcpsvc.Lease) (err error) { if len(s.leases) == 0 { return nil } @@ -378,7 +379,7 @@ const ErrUnconfigured errors.Error = "server is unconfigured" // AddStaticLease implements the DHCPServer interface for *v4Server. It is // safe for concurrent use. -func (s *v4Server) AddStaticLease(l *Lease) (err error) { +func (s *v4Server) AddStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: adding static lease: %w") }() if s.conf == nil { @@ -435,7 +436,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) { } // UpdateStaticLease updates IP, hostname of the static lease. -func (s *v4Server) UpdateStaticLease(l *Lease) (err error) { +func (s *v4Server) UpdateStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { if err != nil { err = errors.Annotate(err, "dhcpv4: updating static lease: %w") @@ -474,7 +475,7 @@ func (s *v4Server) UpdateStaticLease(l *Lease) (err error) { } // validateStaticLease returns an error if the static lease is invalid. -func (s *v4Server) validateStaticLease(l *Lease) (err error) { +func (s *v4Server) validateStaticLease(l *dhcpsvc.Lease) (err error) { hostname, err := normalizeHostname(l.Hostname) if err != nil { // Don't wrap the error, because it's informative enough as is. @@ -511,7 +512,7 @@ func (s *v4Server) validateStaticLease(l *Lease) (err error) { // updateStaticLease safe removes dynamic lease with the same properties and // then adds a static lease l. -func (s *v4Server) updateStaticLease(l *Lease) (err error) { +func (s *v4Server) updateStaticLease(l *dhcpsvc.Lease) (err error) { s.leasesLock.Lock() defer s.leasesLock.Unlock() @@ -529,7 +530,7 @@ func (s *v4Server) updateStaticLease(l *Lease) (err error) { } // RemoveStaticLease removes a static lease. It is safe for concurrent use. -func (s *v4Server) RemoveStaticLease(l *Lease) (err error) { +func (s *v4Server) RemoveStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() if s.conf == nil { @@ -606,7 +607,7 @@ func (s *v4Server) addrAvailable(target net.IP) (avail bool) { } // findLease finds a lease by its MAC-address. -func (s *v4Server) findLease(mac net.HardwareAddr) (l *Lease) { +func (s *v4Server) findLease(mac net.HardwareAddr) (l *dhcpsvc.Lease) { for _, l = range s.leases { if bytes.Equal(mac, l.HWAddr) { return l @@ -646,8 +647,8 @@ func (s *v4Server) findExpiredLease() int { // reserveLease reserves a lease for a client by its MAC-address. It returns // nil if it couldn't allocate a new lease. -func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) { - l = &Lease{HWAddr: slices.Clone(mac)} +func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *dhcpsvc.Lease, err error) { + l = &dhcpsvc.Lease{HWAddr: slices.Clone(mac)} nextIP := s.nextIP() if nextIP == nil { @@ -679,7 +680,7 @@ func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) { // commitLease refreshes l's values. It takes the desired hostname into account // when setting it into the lease, but generates a unique one if the provided // can't be used. -func (s *v4Server) commitLease(l *Lease, hostname string) { +func (s *v4Server) commitLease(l *dhcpsvc.Lease, hostname string) { prev := l.Hostname hostname = s.validHostnameForClient(hostname, l.IP) @@ -709,7 +710,7 @@ func (s *v4Server) commitLease(l *Lease, hostname string) { // allocateLease allocates a new lease for the MAC address. If there are no IP // addresses left, both l and err are nil. -func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) { +func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *dhcpsvc.Lease, err error) { for { l, err = s.reserveLease(mac) if err != nil { @@ -728,7 +729,7 @@ func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) { } // handleDiscover is the handler for the DHCP Discover request. -func (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err error) { +func (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *dhcpsvc.Lease, err error) { mac := req.ClientHWAddr defer s.conf.notify(LeaseChangedDBStore) @@ -787,7 +788,7 @@ func OptionFQDN(fqdn string) (opt dhcpv4.Option) { // checkLease checks if the pair of mac and ip is already leased. The mismatch // is true when the existing lease has the same hardware address but differs in // its IP address. -func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mismatch bool) { +func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (l *dhcpsvc.Lease, mismatch bool) { s.leasesLock.Lock() defer s.leasesLock.Unlock() @@ -798,7 +799,7 @@ func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mi return nil, false } - for _, l := range s.leases { + for _, l = range s.leases { if !bytes.Equal(l.HWAddr, mac) { continue } @@ -823,7 +824,7 @@ func (s *v4Server) handleSelecting( req *dhcpv4.DHCPv4, reqIP net.IP, sid net.IP, -) (l *Lease, needsReply bool) { +) (l *dhcpsvc.Lease, needsReply bool) { // Client inserts the address of the selected server in server identifier, // ciaddr MUST be zero. mac := req.ClientHWAddr @@ -857,7 +858,10 @@ func (s *v4Server) handleSelecting( } // handleInitReboot handles the DHCPREQUEST generated during INIT-REBOOT state. -func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, needsReply bool) { +func (s *v4Server) handleInitReboot( + req *dhcpv4.DHCPv4, + reqIP net.IP, +) (l *dhcpsvc.Lease, needsReply bool) { mac := req.ClientHWAddr ip4 := reqIP.To4() @@ -899,7 +903,7 @@ func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, // handleRenew handles the DHCPREQUEST generated during RENEWING or REBINDING // state. -func (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *Lease, needsReply bool) { +func (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *dhcpsvc.Lease, needsReply bool) { mac := req.ClientHWAddr // ciaddr MUST be filled in with client's IP address. @@ -926,7 +930,7 @@ func (s *v4Server) handleRenew(req *dhcpv4.DHCPv4) (l *Lease, needsReply bool) { // handleByRequestType handles the DHCPREQUEST according to the state during // which it's generated by client. -func (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) { +func (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *dhcpsvc.Lease, needsReply bool) { reqIP, sid := req.RequestedIPAddress(), req.ServerIdentifier() if sid != nil && !sid.IsUnspecified() { @@ -950,7 +954,7 @@ func (s *v4Server) handleByRequestType(req *dhcpv4.DHCPv4) (lease *Lease, needsR // handleRequest is the handler for a DHCPREQUEST message. // // See https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2. -func (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, needsReply bool) { +func (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *dhcpsvc.Lease, needsReply bool) { lease, needsReply = s.handleByRequestType(req) if lease == nil { return nil, needsReply @@ -1043,7 +1047,7 @@ func (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) { } // findLeaseForIP returns a lease for provided ip and mac. -func (s *v4Server) findLeaseForIP(ip net.IP, mac net.HardwareAddr) (l *Lease) { +func (s *v4Server) findLeaseForIP(ip net.IP, mac net.HardwareAddr) (l *dhcpsvc.Lease) { netIP, ok := netip.AddrFromSlice(ip) if !ok { log.Info("dhcpv4: invalid IP: %s", ip) @@ -1106,7 +1110,11 @@ func (s *v4Server) handleRelease(req, resp *dhcpv4.DHCPv4) (err error) { } // messageHandler describes a DHCPv4 message handler function. -type messageHandler func(s *v4Server, req, resp *dhcpv4.DHCPv4) (rCode int, l *Lease, err error) +type messageHandler func( + s *v4Server, + req *dhcpv4.DHCPv4, + resp *dhcpv4.DHCPv4, +) (rCode int, l *dhcpsvc.Lease, err error) // messageHandlers is a map of handlers for various messages with message types // keys. @@ -1115,7 +1123,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { l, err = s.handleDiscover(req, resp) if err != nil { return 0, nil, fmt.Errorf("handling discover: %s", err) @@ -1131,7 +1139,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { var toReply bool l, toReply = s.handleRequest(req, resp) if l == nil { @@ -1149,7 +1157,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { err = s.handleDecline(req, resp) if err != nil { return 0, nil, fmt.Errorf("handling decline: %s", err) @@ -1161,7 +1169,7 @@ var messageHandlers = map[dhcpv4.MessageType]messageHandler{ s *v4Server, req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4, - ) (rCode int, l *Lease, err error) { + ) (rCode int, l *dhcpsvc.Lease, err error) { err = s.handleRelease(req, resp) if err != nil { return 0, nil, fmt.Errorf("handling release: %s", err) @@ -1402,8 +1410,8 @@ func (s *v4Server) Stop() (err error) { // Create DHCPv4 server func v4Create(conf *V4ServerConf) (srv *v4Server, err error) { s := &v4Server{ - hostsIndex: map[string]*Lease{}, - ipIndex: map[netip.Addr]*Lease{}, + hostsIndex: map[string]*dhcpsvc.Lease{}, + ipIndex: map[netip.Addr]*dhcpsvc.Lease{}, } err = conf.Validate() diff --git a/internal/dhcpd/v4_unix_test.go b/internal/dhcpd/v4_unix_test.go index 3ad43b74..4e0df75b 100644 --- a/internal/dhcpd/v4_unix_test.go +++ b/internal/dhcpd/v4_unix_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/testutil" @@ -67,7 +68,7 @@ func TestV4Server_leasing(t *testing.T) { s := defaultSrv(t) t.Run("add_static", func(t *testing.T) { - err := s.AddStaticLease(&Lease{ + err := s.AddStaticLease(&dhcpsvc.Lease{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -76,7 +77,7 @@ func TestV4Server_leasing(t *testing.T) { require.NoError(t, err) t.Run("same_name", func(t *testing.T) { - err = s.AddStaticLease(&Lease{ + err = s.AddStaticLease(&dhcpsvc.Lease{ Hostname: staticName, HWAddr: anotherMAC, IP: anotherIP, @@ -90,7 +91,7 @@ func TestV4Server_leasing(t *testing.T) { "dynamic leases for " + anotherIP.String() + " (" + staticMAC.String() + "): static lease already exists" - err = s.AddStaticLease(&Lease{ + err = s.AddStaticLease(&dhcpsvc.Lease{ Hostname: anotherName, HWAddr: staticMAC, IP: anotherIP, @@ -104,7 +105,7 @@ func TestV4Server_leasing(t *testing.T) { "dynamic leases for " + staticIP.String() + " (" + anotherMAC.String() + "): static lease already exists" - err = s.AddStaticLease(&Lease{ + err = s.AddStaticLease(&dhcpsvc.Lease{ Hostname: anotherName, HWAddr: anotherMAC, IP: staticIP, @@ -208,11 +209,11 @@ func TestV4Server_AddRemove_static(t *testing.T) { require.Empty(t, ls) testCases := []struct { - lease *Lease + lease *dhcpsvc.Lease name string wantErrMsg string }{{ - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "success.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -220,7 +221,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { name: "success", wantErrMsg: "", }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "probably-router.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: DefaultGatewayIP, @@ -229,7 +230,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { wantErrMsg: "dhcpv4: adding static lease: " + `can't assign the gateway IP "192.168.10.1" to the lease`, }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "ip6.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("ffff::1"), @@ -238,7 +239,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { wantErrMsg: `dhcpv4: adding static lease: ` + `invalid IP "ffff::1": only IPv4 is supported`, }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "bad-mac.local", HWAddr: net.HardwareAddr{0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -247,7 +248,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { wantErrMsg: `dhcpv4: adding static lease: bad mac address "aa:aa": ` + `bad mac address length 2, allowed: [6 8 20]`, }, { - lease: &Lease{ + lease: &dhcpsvc.Lease{ Hostname: "bad-lbl-.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -266,7 +267,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { return } - err = s.RemoveStaticLease(&Lease{ + err = s.RemoveStaticLease(&dhcpsvc.Lease{ IP: tc.lease.IP, HWAddr: tc.lease.HWAddr, }) @@ -289,7 +290,7 @@ func TestV4_AddReplace(t *testing.T) { s, ok := sIface.(*v4Server) require.True(t, ok) - dynLeases := []Lease{{ + dynLeases := []dhcpsvc.Lease{{ Hostname: "dynamic-1.local", HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -304,7 +305,7 @@ func TestV4_AddReplace(t *testing.T) { require.NoError(t, err) } - stLeases := []*Lease{{ + stLeases := []*dhcpsvc.Lease{{ Hostname: "static-1.local", HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -513,7 +514,7 @@ func TestV4StaticLease_Get(t *testing.T) { s.conf.dnsIPAddrs = []netip.Addr{dnsAddr} s.implicitOpts.Update(dhcpv4.OptDNS(dnsAddr.AsSlice())) - l := &Lease{ + l := &dhcpsvc.Lease{ Hostname: "static-1.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: netip.MustParseAddr("192.168.10.150"), @@ -779,7 +780,7 @@ func TestV4Server_FindMACbyIP(t *testing.T) { anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB} s := &v4Server{ - leases: []*Lease{{ + leases: []*dhcpsvc.Lease{{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -791,11 +792,11 @@ func TestV4Server_FindMACbyIP(t *testing.T) { IP: anotherIP, }}, } - s.ipIndex = map[netip.Addr]*Lease{ + s.ipIndex = map[netip.Addr]*dhcpsvc.Lease{ staticIP: s.leases[0], anotherIP: s.leases[1], } - s.hostsIndex = map[string]*Lease{ + s.hostsIndex = map[string]*dhcpsvc.Lease{ staticName: s.leases[0], anotherName: s.leases[1], } @@ -845,7 +846,7 @@ func TestV4Server_handleDecline(t *testing.T) { s4, ok := s.(*v4Server) require.True(t, ok) - s4.leases = []*Lease{{ + s4.leases = []*dhcpsvc.Lease{{ Hostname: dynamicName, HWAddr: dynamicMAC, IP: dynamicIP, @@ -887,7 +888,7 @@ func TestV4Server_handleRelease(t *testing.T) { s4, ok := s.(*v4Server) require.True(t, ok) - s4.leases = []*Lease{{ + s4.leases = []*dhcpsvc.Lease{{ Hostname: dynamicName, HWAddr: dynamicMAC, IP: dynamicIP, diff --git a/internal/dhcpd/v6_unix.go b/internal/dhcpd/v6_unix.go index 4101830e..aeaed266 100644 --- a/internal/dhcpd/v6_unix.go +++ b/internal/dhcpd/v6_unix.go @@ -11,6 +11,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" @@ -31,7 +32,7 @@ type v6Server struct { sid dhcpv6.DUID srv *server6.Server - leases []*Lease + leases []*dhcpsvc.Lease leasesLock sync.Mutex ipAddrs [256]byte } @@ -87,7 +88,7 @@ func (s *v6Server) IPByHost(host string) (ip netip.Addr) { } // ResetLeases resets leases. -func (s *v6Server) ResetLeases(leases []*Lease) (err error) { +func (s *v6Server) ResetLeases(leases []*dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() s.leasesLock.Lock() @@ -111,12 +112,14 @@ func (s *v6Server) ResetLeases(leases []*Lease) (err error) { // GetLeases returns the list of current DHCP leases. It is safe for concurrent // use. -func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) { +func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*dhcpsvc.Lease) { // The function shouldn't return nil value because zero-length slice // behaves differently in cases like marshalling. Our front-end also // requires non-nil value in the response. - leases = []*Lease{} + leases = []*dhcpsvc.Lease{} s.leasesLock.Lock() + defer s.leasesLock.Unlock() + for _, l := range s.leases { if l.IsStatic { if (flags & LeasesStatic) != 0 { @@ -128,12 +131,12 @@ func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) { } } } - s.leasesLock.Unlock() + return leases } // getLeasesRef returns the actual leases slice. For internal use only. -func (s *v6Server) getLeasesRef() []*Lease { +func (s *v6Server) getLeasesRef() []*dhcpsvc.Lease { return s.leases } @@ -174,7 +177,7 @@ func (s *v6Server) leaseRemoveSwapByIndex(i int) { // Remove a dynamic lease with the same properties // Return error if a static lease is found -func (s *v6Server) rmDynamicLease(lease *Lease) (err error) { +func (s *v6Server) rmDynamicLease(lease *dhcpsvc.Lease) (err error) { for i := 0; i < len(s.leases); i++ { l := s.leases[i] @@ -204,7 +207,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) { } // AddStaticLease adds a static lease. It is safe for concurrent use. -func (s *v6Server) AddStaticLease(l *Lease) (err error) { +func (s *v6Server) AddStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() if !l.IP.Is6() { @@ -236,7 +239,7 @@ func (s *v6Server) AddStaticLease(l *Lease) (err error) { } // UpdateStaticLease updates IP, hostname of the static lease. -func (s *v6Server) UpdateStaticLease(l *Lease) (err error) { +func (s *v6Server) UpdateStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { if err != nil { err = errors.Annotate(err, "dhcpv6: updating static lease: %w") @@ -267,7 +270,7 @@ func (s *v6Server) UpdateStaticLease(l *Lease) (err error) { } // RemoveStaticLease removes a static lease. It is safe for concurrent use. -func (s *v6Server) RemoveStaticLease(l *Lease) (err error) { +func (s *v6Server) RemoveStaticLease(l *dhcpsvc.Lease) (err error) { defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() if !l.IP.Is6() { @@ -292,7 +295,7 @@ func (s *v6Server) RemoveStaticLease(l *Lease) (err error) { } // Add a lease -func (s *v6Server) addLease(l *Lease) { +func (s *v6Server) addLease(l *dhcpsvc.Lease) { s.leases = append(s.leases, l) ip := l.IP.As16() s.ipAddrs[ip[15]] = 1 @@ -300,7 +303,7 @@ func (s *v6Server) addLease(l *Lease) { } // Remove a lease with the same properties -func (s *v6Server) rmLease(lease *Lease) (err error) { +func (s *v6Server) rmLease(lease *dhcpsvc.Lease) (err error) { for i, l := range s.leases { if l.IP == lease.IP { if !bytes.Equal(l.HWAddr, lease.HWAddr) || @@ -318,7 +321,7 @@ func (s *v6Server) rmLease(lease *Lease) (err error) { } // Find lease by MAC. -func (s *v6Server) findLease(mac net.HardwareAddr) (lease *Lease) { +func (s *v6Server) findLease(mac net.HardwareAddr) (lease *dhcpsvc.Lease) { for i := range s.leases { if bytes.Equal(mac, s.leases[i].HWAddr) { return s.leases[i] @@ -356,8 +359,8 @@ func (s *v6Server) findFreeIP() net.IP { } // Reserve lease for MAC -func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease { - l := Lease{ +func (s *v6Server) reserveLease(mac net.HardwareAddr) *dhcpsvc.Lease { + l := dhcpsvc.Lease{ HWAddr: make([]byte, len(mac)), } @@ -390,7 +393,7 @@ func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease { return &l } -func (s *v6Server) commitDynamicLease(l *Lease) { +func (s *v6Server) commitDynamicLease(l *dhcpsvc.Lease) { l.Expiry = time.Now().Add(s.conf.leaseTime) s.leasesLock.Lock() @@ -438,7 +441,7 @@ func (s *v6Server) checkSID(msg *dhcpv6.Message) error { } // . IAAddress must be equal to the lease's IP -func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error { +func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *dhcpsvc.Lease) error { switch msg.Type() { case dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeConfirm, @@ -464,7 +467,7 @@ func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error { } // Store lease in DB (if necessary) and return lease life time -func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *Lease) time.Duration { +func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *dhcpsvc.Lease) time.Duration { lifetime := s.conf.leaseTime switch msg.Type() { @@ -506,7 +509,7 @@ func (s *v6Server) process(msg *dhcpv6.Message, req, resp dhcpv6.DHCPv6) bool { return false } - var lease *Lease + var lease *dhcpsvc.Lease func() { s.leasesLock.Lock() defer s.leasesLock.Unlock() diff --git a/internal/dhcpd/v6_unix_test.go b/internal/dhcpd/v6_unix_test.go index 3ed5221a..b642eed7 100644 --- a/internal/dhcpd/v6_unix_test.go +++ b/internal/dhcpd/v6_unix_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/assert" @@ -28,7 +29,7 @@ func TestV6_AddRemove_static(t *testing.T) { require.Empty(t, s.GetLeases(LeasesStatic)) // Add static lease. - l := &Lease{ + l := &dhcpsvc.Lease{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } @@ -47,7 +48,7 @@ func TestV6_AddRemove_static(t *testing.T) { assert.True(t, ls[0].IsStatic) // Try to remove non-existent static lease. - err = s.RemoveStaticLease(&Lease{ + err = s.RemoveStaticLease(&dhcpsvc.Lease{ IP: netip.MustParseAddr("2001::2"), HWAddr: l.HWAddr, }) @@ -72,7 +73,7 @@ func TestV6_AddReplace(t *testing.T) { require.True(t, ok) // Add dynamic leases. - dynLeases := []*Lease{{ + dynLeases := []*dhcpsvc.Lease{{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, }, { @@ -84,7 +85,7 @@ func TestV6_AddReplace(t *testing.T) { s.addLease(l) } - stLeases := []*Lease{{ + stLeases := []*dhcpsvc.Lease{{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, }, { @@ -126,7 +127,7 @@ func TestV6GetLease(t *testing.T) { LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } - l := &Lease{ + l := &dhcpsvc.Lease{ IP: netip.MustParseAddr("2001::1"), HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } @@ -324,7 +325,7 @@ func TestV6_FindMACbyIP(t *testing.T) { anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB} s := &v6Server{ - leases: []*Lease{{ + leases: []*dhcpsvc.Lease{{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, @@ -337,7 +338,7 @@ func TestV6_FindMACbyIP(t *testing.T) { }}, } - s.leases = []*Lease{{ + s.leases = []*dhcpsvc.Lease{{ Hostname: staticName, HWAddr: staticMAC, IP: staticIP, diff --git a/internal/dhcpsvc/config.go b/internal/dhcpsvc/config.go index b01dc263..52d28a33 100644 --- a/internal/dhcpsvc/config.go +++ b/internal/dhcpsvc/config.go @@ -1,10 +1,12 @@ package dhcpsvc import ( - "net/netip" + "fmt" "time" - "github.com/google/gopacket/layers" + "github.com/AdguardTeam/golibs/netutil" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) // Config is the configuration for the DHCP service. @@ -33,54 +35,58 @@ type InterfaceConfig struct { IPv6 *IPv6Config } -// IPv4Config is the interface-specific configuration for DHCPv4. -type IPv4Config struct { - // GatewayIP is the IPv4 address of the network's gateway. It is used as - // the default gateway for DHCP clients and also used in calculating the - // network-specific broadcast address. - GatewayIP netip.Addr +// Validate returns an error in conf if any. +func (conf *Config) Validate() (err error) { + switch { + case conf == nil: + return errNilConfig + case !conf.Enabled: + return nil + case conf.ICMPTimeout < 0: + return newMustErr("icmp timeout", "be non-negative", conf.ICMPTimeout) + } - // SubnetMask is the IPv4 subnet mask of the network. It should be a valid - // IPv4 subnet mask (i.e. all 1s followed by all 0s). - SubnetMask netip.Addr + err = netutil.ValidateDomainName(conf.LocalDomainName) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } - // RangeStart is the first address in the range to assign to DHCP clients. - RangeStart netip.Addr + if len(conf.Interfaces) == 0 { + return errNoInterfaces + } - // RangeEnd is the last address in the range to assign to DHCP clients. - RangeEnd netip.Addr + ifaces := maps.Keys(conf.Interfaces) + slices.Sort(ifaces) - // Options is the list of DHCP options to send to DHCP clients. - Options layers.DHCPOptions + for _, iface := range ifaces { + if err = conf.Interfaces[iface].validate(); err != nil { + return fmt.Errorf("interface %q: %w", iface, err) + } + } - // LeaseDuration is the TTL of a DHCP lease. - LeaseDuration time.Duration - - // Enabled is the state of the DHCPv4 service, whether it is enabled or not - // on the specific interface. - Enabled bool + return nil } -// IPv6Config is the interface-specific configuration for DHCPv6. -type IPv6Config struct { - // RangeStart is the first address in the range to assign to DHCP clients. - RangeStart netip.Addr - - // Options is the list of DHCP options to send to DHCP clients. - Options layers.DHCPOptions - - // LeaseDuration is the TTL of a DHCP lease. - LeaseDuration time.Duration - - // RASlaacOnly defines whether the DHCP clients should only use SLAAC for - // address assignment. - RASLAACOnly bool - - // RAAllowSlaac defines whether the DHCP clients may use SLAAC for address - // assignment. - RAAllowSLAAC bool - - // Enabled is the state of the DHCPv6 service, whether it is enabled or not - // on the specific interface. - Enabled bool +// newMustErr returns an error that indicates that valName must be as must +// describes. +func newMustErr(valName, must string, val fmt.Stringer) (err error) { + return fmt.Errorf("%s %s must %s", valName, val, must) +} + +// validate returns an error in ic, if any. +func (ic *InterfaceConfig) validate() (err error) { + if ic == nil { + return errNilConfig + } + + if err = ic.IPv4.validate(); err != nil { + return fmt.Errorf("ipv4: %w", err) + } + + if err = ic.IPv6.validate(); err != nil { + return fmt.Errorf("ipv6: %w", err) + } + + return nil } diff --git a/internal/dhcpsvc/config_test.go b/internal/dhcpsvc/config_test.go new file mode 100644 index 00000000..6663d378 --- /dev/null +++ b/internal/dhcpsvc/config_test.go @@ -0,0 +1,88 @@ +package dhcpsvc_test + +import ( + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" + "github.com/AdguardTeam/golibs/testutil" +) + +func TestConfig_Validate(t *testing.T) { + testCases := []struct { + name string + conf *dhcpsvc.Config + wantErrMsg string + }{{ + name: "nil_config", + conf: nil, + wantErrMsg: "config is nil", + }, { + name: "disabled", + conf: &dhcpsvc.Config{}, + wantErrMsg: "", + }, { + name: "empty", + conf: &dhcpsvc.Config{ + Enabled: true, + }, + wantErrMsg: `bad domain name "": domain name is empty`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: nil, + }, + name: "no_interfaces", + wantErrMsg: "no interfaces specified", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: nil, + }, + name: "no_interfaces", + wantErrMsg: "no interfaces specified", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": nil, + }, + }, + name: "nil_interface", + wantErrMsg: `interface "eth0": config is nil`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: nil, + IPv6: &dhcpsvc.IPv6Config{Enabled: false}, + }, + }, + }, + name: "nil_ipv4", + wantErrMsg: `interface "eth0": ipv4: config is nil`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: &dhcpsvc.IPv4Config{Enabled: false}, + IPv6: nil, + }, + }, + }, + name: "nil_ipv6", + wantErrMsg: `interface "eth0": ipv6: config is nil`, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testutil.AssertErrorMsg(t, tc.wantErrMsg, tc.conf.Validate()) + }) + } +} diff --git a/internal/dhcpsvc/dhcpsvc.go b/internal/dhcpsvc/dhcpsvc.go index 4b3f5c21..efa7404f 100644 --- a/internal/dhcpsvc/dhcpsvc.go +++ b/internal/dhcpsvc/dhcpsvc.go @@ -10,13 +10,13 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "golang.org/x/exp/slices" ) // Lease is a DHCP lease. // -// TODO(e.burkov): Consider it to [agh], since it also may be needed in -// [websvc]. Also think of implementing iterating methods with appropriate -// signatures. +// TODO(e.burkov): Consider moving it to [agh], since it also may be needed in +// [websvc]. type Lease struct { // IP is the IP address leased to the client. IP netip.Addr @@ -34,6 +34,21 @@ type Lease struct { IsStatic bool } +// Clone returns a deep copy of l. +func (l *Lease) Clone() (clone *Lease) { + if l == nil { + return nil + } + + return &Lease{ + Expiry: l.Expiry, + Hostname: l.Hostname, + HWAddr: slices.Clone(l.HWAddr), + IP: l.IP, + IsStatic: l.IsStatic, + } +} + type Interface interface { agh.ServiceWithConfig[*Config] @@ -56,16 +71,20 @@ type Interface interface { // hostname, either set or generated. IPByHost(host string) (ip netip.Addr) - // Leases returns all the DHCP leases. - Leases() (leases []*Lease) + // Leases returns all the active DHCP leases. + // + // TODO(e.burkov): Consider implementing iterating methods with appropriate + // signatures instead of cloning the whole list. + Leases() (ls []*Lease) // AddLease adds a new DHCP lease. It returns an error if the lease is // invalid or already exists. AddLease(l *Lease) (err error) - // EditLease changes an existing DHCP lease. It returns an error if there - // is no lease equal to old or if new is invalid or already exists. - EditLease(old, new *Lease) (err error) + // UpdateStaticLease changes an existing DHCP lease. It returns an error if + // there is no lease with such hardware addressor if new values are invalid + // or already exist. + UpdateStaticLease(l *Lease) (err error) // RemoveLease removes an existing DHCP lease. It returns an error if there // is no lease equal to l. @@ -79,7 +98,7 @@ type Interface interface { type Empty struct{} // type check -var _ Interface = Empty{} +var _ agh.ServiceWithConfig[*Config] = Empty{} // Start implements the [Service] interface for Empty. func (Empty) Start() (err error) { return nil } @@ -87,11 +106,12 @@ func (Empty) Start() (err error) { return nil } // Shutdown implements the [Service] interface for Empty. func (Empty) Shutdown(_ context.Context) (err error) { return nil } -var _ agh.ServiceWithConfig[*Config] = Empty{} - // Config implements the [ServiceWithConfig] interface for Empty. func (Empty) Config() (conf *Config) { return nil } +// type check +var _ Interface = Empty{} + // Enabled implements the [Interface] interface for Empty. func (Empty) Enabled() (ok bool) { return false } @@ -104,17 +124,14 @@ func (Empty) MACByIP(_ netip.Addr) (mac net.HardwareAddr) { return nil } // IPByHost implements the [Interface] interface for Empty. func (Empty) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} } -// type check -var _ Interface = Empty{} - // Leases implements the [Interface] interface for Empty. func (Empty) Leases() (leases []*Lease) { return nil } // AddLease implements the [Interface] interface for Empty. func (Empty) AddLease(_ *Lease) (err error) { return nil } -// EditLease implements the [Interface] interface for Empty. -func (Empty) EditLease(_, _ *Lease) (err error) { return nil } +// UpdateStaticLease implements the [Interface] interface for Empty. +func (Empty) UpdateStaticLease(_ *Lease) (err error) { return nil } // RemoveLease implements the [Interface] interface for Empty. func (Empty) RemoveLease(_ *Lease) (err error) { return nil } diff --git a/internal/dhcpsvc/errors.go b/internal/dhcpsvc/errors.go new file mode 100644 index 00000000..a7cc8931 --- /dev/null +++ b/internal/dhcpsvc/errors.go @@ -0,0 +1,11 @@ +package dhcpsvc + +import "github.com/AdguardTeam/golibs/errors" + +const ( + // errNilConfig is returned when a nil config met. + errNilConfig errors.Error = "config is nil" + + // errNoInterfaces is returned when no interfaces found in configuration. + errNoInterfaces errors.Error = "no interfaces specified" +) diff --git a/internal/dhcpsvc/iprange.go b/internal/dhcpsvc/iprange.go new file mode 100644 index 00000000..0f922bba --- /dev/null +++ b/internal/dhcpsvc/iprange.go @@ -0,0 +1,98 @@ +package dhcpsvc + +import ( + "encoding/binary" + "fmt" + "math" + "math/big" + "net/netip" + + "github.com/AdguardTeam/golibs/errors" +) + +// ipRange is an inclusive range of IP addresses. A zero range doesn't contain +// any IP addresses. +// +// It is safe for concurrent use. +type ipRange struct { + start netip.Addr + end netip.Addr +} + +// maxRangeLen is the maximum IP range length. The bitsets used in servers only +// accept uints, which can have the size of 32 bit. +// +// TODO(a.garipov, e.burkov): Reconsider the value for IPv6. +const maxRangeLen = math.MaxUint32 + +// newIPRange creates a new IP address range. start must be less than end. The +// resulting range must not be greater than maxRangeLen. +func newIPRange(start, end netip.Addr) (r ipRange, err error) { + defer func() { err = errors.Annotate(err, "invalid ip range: %w") }() + + switch false { + case start.Is4() == end.Is4(): + return ipRange{}, fmt.Errorf("%s and %s must be within the same address family", start, end) + case start.Less(end): + return ipRange{}, fmt.Errorf("start %s is greater than or equal to end %s", start, end) + default: + diff := (&big.Int{}).Sub( + (&big.Int{}).SetBytes(end.AsSlice()), + (&big.Int{}).SetBytes(start.AsSlice()), + ) + + if !diff.IsUint64() || diff.Uint64() > maxRangeLen { + return ipRange{}, fmt.Errorf("range length must be within %d", uint32(maxRangeLen)) + } + } + + return ipRange{ + start: start, + end: end, + }, nil +} + +// contains returns true if r contains ip. +func (r ipRange) contains(ip netip.Addr) (ok bool) { + // Assume that the end was checked to be within the same address family as + // the start during construction. + return r.start.Is4() == ip.Is4() && !ip.Less(r.start) && !r.end.Less(ip) +} + +// ipPredicate is a function that is called on every IP address in +// [ipRange.find]. +type ipPredicate func(ip netip.Addr) (ok bool) + +// find finds the first IP address in r for which p returns true. It returns an +// empty [netip.Addr] if there are no addresses that satisfy p. +// +// TODO(e.burkov): Use. +func (r ipRange) find(p ipPredicate) (ip netip.Addr) { + for ip = r.start; !r.end.Less(ip); ip = ip.Next() { + if p(ip) { + return ip + } + } + + return netip.Addr{} +} + +// offset returns the offset of ip from the beginning of r. It returns 0 and +// false if ip is not in r. +func (r ipRange) offset(ip netip.Addr) (offset uint64, ok bool) { + if !r.contains(ip) { + return 0, false + } + + startData, ipData := r.start.As16(), ip.As16() + be := binary.BigEndian + + // Assume that the range length was checked against maxRangeLen during + // construction. + return be.Uint64(ipData[8:]) - be.Uint64(startData[8:]), true +} + +// String implements the fmt.Stringer interface for *ipRange. +func (r ipRange) String() (s string) { + return fmt.Sprintf("%s-%s", r.start, r.end) +} diff --git a/internal/dhcpsvc/iprange_internal_test.go b/internal/dhcpsvc/iprange_internal_test.go new file mode 100644 index 00000000..1cb12688 --- /dev/null +++ b/internal/dhcpsvc/iprange_internal_test.go @@ -0,0 +1,204 @@ +package dhcpsvc + +import ( + "net/netip" + "strconv" + "testing" + + "github.com/AdguardTeam/golibs/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewIPRange(t *testing.T) { + start4 := netip.MustParseAddr("0.0.0.1") + end4 := netip.MustParseAddr("0.0.0.3") + start6 := netip.MustParseAddr("1::1") + end6 := netip.MustParseAddr("1::3") + end6Large := netip.MustParseAddr("2::3") + + testCases := []struct { + start netip.Addr + end netip.Addr + name string + wantErrMsg string + }{{ + start: start4, + end: end4, + name: "success_ipv4", + wantErrMsg: "", + }, { + start: start6, + end: end6, + name: "success_ipv6", + wantErrMsg: "", + }, { + start: end4, + end: start4, + name: "start_gt_end", + wantErrMsg: "invalid ip range: start 0.0.0.3 is greater than or equal to end 0.0.0.1", + }, { + start: start4, + end: start4, + name: "start_eq_end", + wantErrMsg: "invalid ip range: start 0.0.0.1 is greater than or equal to end 0.0.0.1", + }, { + start: start6, + end: end6Large, + name: "too_large", + wantErrMsg: "invalid ip range: range length must be within " + + strconv.FormatUint(maxRangeLen, 10), + }, { + start: start4, + end: end6, + name: "different_family", + wantErrMsg: "invalid ip range: 0.0.0.1 and 1::3 must be within the same address family", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := newIPRange(tc.start, tc.end) + testutil.AssertErrorMsg(t, tc.wantErrMsg, err) + }) + } +} + +func TestIPRange_Contains(t *testing.T) { + start, end := netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.3") + r, err := newIPRange(start, end) + require.NoError(t, err) + + testCases := []struct { + in netip.Addr + want assert.BoolAssertionFunc + name string + }{{ + in: start, + want: assert.True, + name: "start", + }, { + in: end, + want: assert.True, + name: "end", + }, { + in: start.Next(), + want: assert.True, + name: "within", + }, { + in: netip.MustParseAddr("0.0.0.0"), + want: assert.False, + name: "before", + }, { + in: netip.MustParseAddr("0.0.0.4"), + want: assert.False, + name: "after", + }, { + in: netip.MustParseAddr("::"), + want: assert.False, + name: "another_family", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.want(t, r.contains(tc.in)) + }) + } +} + +func TestIPRange_Find(t *testing.T) { + start, end := netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.5") + r, err := newIPRange(start, end) + require.NoError(t, err) + + num, ok := r.offset(end) + require.True(t, ok) + + testCases := []struct { + predicate ipPredicate + want netip.Addr + name string + }{{ + predicate: func(ip netip.Addr) (ok bool) { + ipData := ip.AsSlice() + + return ipData[len(ipData)-1]%2 == 0 + }, + want: netip.MustParseAddr("0.0.0.2"), + name: "even", + }, { + predicate: func(ip netip.Addr) (ok bool) { + ipData := ip.AsSlice() + + return ipData[len(ipData)-1]%10 == 0 + }, + want: netip.Addr{}, + name: "none", + }, { + predicate: func(ip netip.Addr) (ok bool) { + return true + }, + want: start, + name: "first", + }, { + predicate: func(ip netip.Addr) (ok bool) { + off, _ := r.offset(ip) + + return off == num + }, + want: end, + name: "last", + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := r.find(tc.predicate) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestIPRange_Offset(t *testing.T) { + start, end := netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.5") + r, err := newIPRange(start, end) + require.NoError(t, err) + + testCases := []struct { + in netip.Addr + name string + wantOffset uint64 + wantOK bool + }{{ + in: netip.MustParseAddr("0.0.0.2"), + name: "in", + wantOffset: 1, + wantOK: true, + }, { + in: start, + name: "in_start", + wantOffset: 0, + wantOK: true, + }, { + in: end, + name: "in_end", + wantOffset: 4, + wantOK: true, + }, { + in: netip.MustParseAddr("0.0.0.6"), + name: "out_after", + wantOffset: 0, + wantOK: false, + }, { + in: netip.MustParseAddr("0.0.0.0"), + name: "out_before", + wantOffset: 0, + wantOK: false, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + offset, ok := r.offset(tc.in) + assert.Equal(t, tc.wantOffset, offset) + assert.Equal(t, tc.wantOK, ok) + }) + } +} diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go new file mode 100644 index 00000000..2d24ef27 --- /dev/null +++ b/internal/dhcpsvc/server.go @@ -0,0 +1,100 @@ +package dhcpsvc + +import ( + "fmt" + "sync/atomic" + "time" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// DHCPServer is a DHCP server for both IPv4 and IPv6 address families. +type DHCPServer struct { + // enabled indicates whether the DHCP server is enabled and can provide + // information about its clients. + enabled *atomic.Bool + + // localTLD is the top-level domain name to use for resolving DHCP + // clients' hostnames. + localTLD string + + // interfaces4 is the set of IPv4 interfaces sorted by interface name. + interfaces4 []*iface4 + + // interfaces6 is the set of IPv6 interfaces sorted by interface name. + interfaces6 []*iface6 + + // leases is the set of active DHCP leases. + leases []*Lease + + // icmpTimeout is the timeout for checking another DHCP server's presence. + icmpTimeout time.Duration +} + +// New creates a new DHCP server with the given configuration. It returns an +// error if the given configuration can't be used. +// +// TODO(e.burkov): Use. +func New(conf *Config) (srv *DHCPServer, err error) { + if !conf.Enabled { + // TODO(e.burkov): Perhaps return [Empty]? + return nil, nil + } + + ifaces4 := make([]*iface4, len(conf.Interfaces)) + ifaces6 := make([]*iface6, len(conf.Interfaces)) + + ifaceNames := maps.Keys(conf.Interfaces) + slices.Sort(ifaceNames) + + var i4 *iface4 + var i6 *iface6 + + for _, ifaceName := range ifaceNames { + iface := conf.Interfaces[ifaceName] + + i4, err = newIface4(ifaceName, iface.IPv4) + if err != nil { + return nil, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err) + } else if i4 != nil { + ifaces4 = append(ifaces4, i4) + } + + i6 = newIface6(ifaceName, iface.IPv6) + if i6 != nil { + ifaces6 = append(ifaces6, i6) + } + } + + enabled := &atomic.Bool{} + enabled.Store(conf.Enabled) + + return &DHCPServer{ + enabled: enabled, + interfaces4: ifaces4, + interfaces6: ifaces6, + localTLD: conf.LocalDomainName, + icmpTimeout: conf.ICMPTimeout, + }, nil +} + +// type check +// +// TODO(e.burkov): Uncomment when the [Interface] interface is implemented. +// var _ Interface = (*DHCPServer)(nil) + +// Enabled implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) Enabled() (ok bool) { + return srv.enabled.Load() +} + +// Leases implements the [Interface] interface for *DHCPServer. +func (srv *DHCPServer) Leases() (leases []*Lease) { + leases = make([]*Lease, 0, len(srv.leases)) + for _, lease := range srv.leases { + leases = append(leases, lease.Clone()) + } + + return leases +} diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go new file mode 100644 index 00000000..6475dfa4 --- /dev/null +++ b/internal/dhcpsvc/server_test.go @@ -0,0 +1,115 @@ +package dhcpsvc_test + +import ( + "net/netip" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" + "github.com/AdguardTeam/golibs/testutil" +) + +// testLocalTLD is a common local TLD for tests. +const testLocalTLD = "local" + +func TestNew(t *testing.T) { + validIPv4Conf := &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("192.168.0.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("192.168.0.2"), + RangeEnd: netip.MustParseAddr("192.168.0.254"), + LeaseDuration: 1 * time.Hour, + } + gwInRangeConf := &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("192.168.0.100"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("192.168.0.1"), + RangeEnd: netip.MustParseAddr("192.168.0.254"), + LeaseDuration: 1 * time.Hour, + } + badStartConf := &dhcpsvc.IPv4Config{ + Enabled: true, + GatewayIP: netip.MustParseAddr("192.168.0.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("127.0.0.1"), + RangeEnd: netip.MustParseAddr("192.168.0.254"), + LeaseDuration: 1 * time.Hour, + } + + validIPv6Conf := &dhcpsvc.IPv6Config{ + Enabled: true, + RangeStart: netip.MustParseAddr("2001:db8::1"), + LeaseDuration: 1 * time.Hour, + RAAllowSLAAC: true, + RASLAACOnly: true, + } + + testCases := []struct { + conf *dhcpsvc.Config + name string + wantErrMsg string + }{{ + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: validIPv4Conf, + IPv6: validIPv6Conf, + }, + }, + }, + name: "valid", + wantErrMsg: "", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: &dhcpsvc.IPv4Config{Enabled: false}, + IPv6: &dhcpsvc.IPv6Config{Enabled: false}, + }, + }, + }, + name: "disabled_interfaces", + wantErrMsg: "", + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: gwInRangeConf, + IPv6: validIPv6Conf, + }, + }, + }, + name: "gateway_within_range", + wantErrMsg: `interface "eth0": ipv4: ` + + `gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`, + }, { + conf: &dhcpsvc.Config{ + Enabled: true, + LocalDomainName: testLocalTLD, + Interfaces: map[string]*dhcpsvc.InterfaceConfig{ + "eth0": { + IPv4: badStartConf, + IPv6: validIPv6Conf, + }, + }, + }, + name: "bad_start", + wantErrMsg: `interface "eth0": ipv4: ` + + `range start 127.0.0.1 is not within 192.168.0.1/24`, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := dhcpsvc.New(tc.conf) + testutil.AssertErrorMsg(t, tc.wantErrMsg, err) + }) + } +} diff --git a/internal/dhcpsvc/v4.go b/internal/dhcpsvc/v4.go new file mode 100644 index 00000000..44c4f840 --- /dev/null +++ b/internal/dhcpsvc/v4.go @@ -0,0 +1,113 @@ +package dhcpsvc + +import ( + "fmt" + "net" + "net/netip" + "time" + + "github.com/google/gopacket/layers" +) + +// IPv4Config is the interface-specific configuration for DHCPv4. +type IPv4Config struct { + // GatewayIP is the IPv4 address of the network's gateway. It is used as + // the default gateway for DHCP clients and also used in calculating the + // network-specific broadcast address. + GatewayIP netip.Addr + + // SubnetMask is the IPv4 subnet mask of the network. It should be a valid + // IPv4 CIDR (i.e. all 1s followed by all 0s). + SubnetMask netip.Addr + + // RangeStart is the first address in the range to assign to DHCP clients. + RangeStart netip.Addr + + // RangeEnd is the last address in the range to assign to DHCP clients. + RangeEnd netip.Addr + + // Options is the list of DHCP options to send to DHCP clients. + Options layers.DHCPOptions + + // LeaseDuration is the TTL of a DHCP lease. + LeaseDuration time.Duration + + // Enabled is the state of the DHCPv4 service, whether it is enabled or not + // on the specific interface. + Enabled bool +} + +// validate returns an error in conf if any. +func (conf *IPv4Config) validate() (err error) { + switch { + case conf == nil: + return errNilConfig + case !conf.Enabled: + return nil + case !conf.GatewayIP.Is4(): + return newMustErr("gateway ip", "be a valid ipv4", conf.GatewayIP) + case !conf.SubnetMask.Is4(): + return newMustErr("subnet mask", "be a valid ipv4 cidr mask", conf.SubnetMask) + case !conf.RangeStart.Is4(): + return newMustErr("range start", "be a valid ipv4", conf.RangeStart) + case !conf.RangeEnd.Is4(): + return newMustErr("range end", "be a valid ipv4", conf.RangeEnd) + case conf.LeaseDuration <= 0: + return newMustErr("lease duration", "be less than %d", conf.LeaseDuration) + default: + return nil + } +} + +// iface4 is a DHCP interface for IPv4 address family. +type iface4 struct { + // gateway is the IP address of the network gateway. + gateway netip.Addr + + // subnet is the network subnet. + subnet netip.Prefix + + // addrSpace is the IPv4 address space allocated for leasing. + addrSpace ipRange + + // name is the name of the interface. + name string + + // TODO(e.burkov): Add options. + + // leaseTTL is the time-to-live of dynamic leases on this interface. + leaseTTL time.Duration +} + +// newIface4 creates a new DHCP interface for IPv4 address family with the given +// configuration. It returns an error if the given configuration can't be used. +func newIface4(name string, conf *IPv4Config) (i *iface4, err error) { + if !conf.Enabled { + return nil, nil + } + + maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size() + subnet := netip.PrefixFrom(conf.GatewayIP, maskLen) + + switch { + case !subnet.Contains(conf.RangeStart): + return nil, fmt.Errorf("range start %s is not within %s", conf.RangeStart, subnet) + case !subnet.Contains(conf.RangeEnd): + return nil, fmt.Errorf("range end %s is not within %s", conf.RangeEnd, subnet) + } + + addrSpace, err := newIPRange(conf.RangeStart, conf.RangeEnd) + if err != nil { + return nil, err + } else if addrSpace.contains(conf.GatewayIP) { + return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace) + } + + return &iface4{ + name: name, + gateway: conf.GatewayIP, + subnet: subnet, + addrSpace: addrSpace, + leaseTTL: conf.LeaseDuration, + }, nil +} diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go new file mode 100644 index 00000000..8bdc1637 --- /dev/null +++ b/internal/dhcpsvc/v6.go @@ -0,0 +1,88 @@ +package dhcpsvc + +import ( + "fmt" + "net/netip" + "time" + + "github.com/google/gopacket/layers" +) + +// IPv6Config is the interface-specific configuration for DHCPv6. +type IPv6Config struct { + // RangeStart is the first address in the range to assign to DHCP clients. + RangeStart netip.Addr + + // Options is the list of DHCP options to send to DHCP clients. + Options layers.DHCPOptions + + // LeaseDuration is the TTL of a DHCP lease. + LeaseDuration time.Duration + + // RASlaacOnly defines whether the DHCP clients should only use SLAAC for + // address assignment. + RASLAACOnly bool + + // RAAllowSlaac defines whether the DHCP clients may use SLAAC for address + // assignment. + RAAllowSLAAC bool + + // Enabled is the state of the DHCPv6 service, whether it is enabled or not + // on the specific interface. + Enabled bool +} + +// validate returns an error in conf if any. +func (conf *IPv6Config) validate() (err error) { + switch { + case conf == nil: + return errNilConfig + case !conf.Enabled: + return nil + case !conf.RangeStart.Is6(): + return fmt.Errorf("range start %s should be a valid ipv6", conf.RangeStart) + case conf.LeaseDuration <= 0: + return fmt.Errorf("lease duration %s must be positive", conf.LeaseDuration) + default: + return nil + } +} + +// iface6 is a DHCP interface for IPv6 address family. +// +// TODO(e.burkov): Add options. +type iface6 struct { + // rangeStart is the first IP address in the range. + rangeStart netip.Addr + + // name is the name of the interface. + name string + + // leaseTTL is the time-to-live of dynamic leases on this interface. + leaseTTL time.Duration + + // raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO + // flags. + raSLAACOnly bool + + // raAllowSLAAC defines if DHCP should send ICMPv6.RA packets with MO flags. + raAllowSLAAC bool +} + +// newIface6 creates a new DHCP interface for IPv6 address family with the given +// configuration. +// +// TODO(e.burkov): Validate properly. +func newIface6(name string, conf *IPv6Config) (i *iface6) { + if !conf.Enabled { + return nil + } + + return &iface6{ + name: name, + rangeStart: conf.RangeStart, + leaseTTL: conf.LeaseDuration, + raSLAACOnly: conf.RASLAACOnly, + raAllowSLAAC: conf.RAAllowSLAAC, + } +} diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 32a0d52a..ec20096b 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -27,6 +27,19 @@ import ( "golang.org/x/exp/slices" ) +// ClientsContainer provides information about preconfigured DNS clients. +type ClientsContainer interface { + // UpstreamConfigByID returns the custom upstream configuration for the + // client having id, using boot to initialize the one if necessary. It + // returns nil if there is no custom upstream configuration for the client. + // The id is expected to be either a string representation of an IP address + // or the ClientID. + UpstreamConfigByID( + id string, + boot upstream.Resolver, + ) (conf *proxy.CustomUpstreamConfig, err error) +} + // Config represents the DNS filtering configuration of AdGuard Home. The zero // Config is empty and ready for use. type Config struct { @@ -35,10 +48,9 @@ type Config struct { // FilterHandler is an optional additional filtering callback. FilterHandler func(cliAddr netip.Addr, clientID string, settings *filtering.Settings) `yaml:"-"` - // GetCustomUpstreamByClient is a callback that returns upstreams - // configuration based on the client IP address or ClientID. It returns - // nil if there are no custom upstreams for the client. - GetCustomUpstreamByClient func(id string) (conf *proxy.UpstreamConfig, err error) `yaml:"-"` + // ClientsContainer stores the information about special handling of some + // DNS clients. + ClientsContainer ClientsContainer `yaml:"-"` // Anti-DNS amplification @@ -55,7 +67,7 @@ type Config struct { RatelimitSubnetLenIPv6 int `yaml:"ratelimit_subnet_len_ipv6"` // RatelimitWhitelist is the list of whitelisted client IP addresses. - RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` + RatelimitWhitelist []netip.Addr `yaml:"ratelimit_whitelist"` // RefuseAny, if true, refuse ANY requests. RefuseAny bool `yaml:"refuse_any"` @@ -277,32 +289,33 @@ type ServerConfig struct { // UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS // upstreams. UseHTTP3Upstreams bool + + // ServePlainDNS defines if plain DNS is allowed for incoming requests. + ServePlainDNS bool } -// createProxyConfig creates and validates configuration for the main proxy. -func (s *Server) createProxyConfig() (conf proxy.Config, err error) { +// newProxyConfig creates and validates configuration for the main proxy. +func (s *Server) newProxyConfig() (conf *proxy.Config, err error) { srvConf := s.conf - conf = proxy.Config{ - UDPListenAddr: srvConf.UDPListenAddrs, - TCPListenAddr: srvConf.TCPListenAddrs, - HTTP3: srvConf.ServeHTTP3, - Ratelimit: int(srvConf.Ratelimit), - RatelimitSubnetMaskIPv4: net.CIDRMask(srvConf.RatelimitSubnetLenIPv4, netutil.IPv4BitLen), - RatelimitSubnetMaskIPv6: net.CIDRMask(srvConf.RatelimitSubnetLenIPv6, netutil.IPv6BitLen), - RatelimitWhitelist: srvConf.RatelimitWhitelist, - RefuseAny: srvConf.RefuseAny, - TrustedProxies: srvConf.TrustedProxies, - CacheMinTTL: srvConf.CacheMinTTL, - CacheMaxTTL: srvConf.CacheMaxTTL, - CacheOptimistic: srvConf.CacheOptimistic, - UpstreamConfig: srvConf.UpstreamConfig, - BeforeRequestHandler: s.beforeRequestHandler, - RequestHandler: s.handleDNSRequest, - HTTPSServerName: aghhttp.UserAgent(), - EnableEDNSClientSubnet: srvConf.EDNSClientSubnet.Enabled, - MaxGoroutines: int(srvConf.MaxGoroutines), - UseDNS64: srvConf.UseDNS64, - DNS64Prefs: srvConf.DNS64Prefixes, + conf = &proxy.Config{ + HTTP3: srvConf.ServeHTTP3, + Ratelimit: int(srvConf.Ratelimit), + RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4, + RatelimitSubnetLenIPv6: srvConf.RatelimitSubnetLenIPv6, + RatelimitWhitelist: srvConf.RatelimitWhitelist, + RefuseAny: srvConf.RefuseAny, + TrustedProxies: srvConf.TrustedProxies, + CacheMinTTL: srvConf.CacheMinTTL, + CacheMaxTTL: srvConf.CacheMaxTTL, + CacheOptimistic: srvConf.CacheOptimistic, + UpstreamConfig: srvConf.UpstreamConfig, + BeforeRequestHandler: s.beforeRequestHandler, + RequestHandler: s.handleDNSRequest, + HTTPSServerName: aghhttp.UserAgent(), + EnableEDNSClientSubnet: srvConf.EDNSClientSubnet.Enabled, + MaxGoroutines: int(srvConf.MaxGoroutines), + UseDNS64: srvConf.UseDNS64, + DNS64Prefs: srvConf.DNS64Prefixes, } if srvConf.EDNSClientSubnet.UseCustom { @@ -316,27 +329,25 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) { } setProxyUpstreamMode( - &conf, + conf, srvConf.AllServers, srvConf.FastestAddr, srvConf.FastestTimeout.Duration, ) - for i, s := range srvConf.BogusNXDomain { - var subnet *net.IPNet - subnet, err = netutil.ParseSubnet(s) - if err != nil { - log.Error("subnet at index %d: %s", i, err) - - continue - } - - conf.BogusNXDomain = append(conf.BogusNXDomain, subnet) + conf.BogusNXDomain, err = parseBogusNXDOMAIN(srvConf.BogusNXDomain) + if err != nil { + return nil, fmt.Errorf("bogus_nxdomain: %w", err) } - err = s.prepareTLS(&conf) + err = s.prepareTLS(conf) if err != nil { - return proxy.Config{}, fmt.Errorf("validating tls: %w", err) + return nil, fmt.Errorf("validating tls: %w", err) + } + + err = s.preparePlain(conf) + if err != nil { + return nil, fmt.Errorf("validating plain: %w", err) } if c := srvConf.DNSCryptConfig; c.Enabled { @@ -347,12 +358,27 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) { } if conf.UpstreamConfig == nil || len(conf.UpstreamConfig.Upstreams) == 0 { - return proxy.Config{}, errors.Error("no default upstream servers configured") + return nil, errors.Error("no default upstream servers configured") } return conf, nil } +// parseBogusNXDOMAIN parses the bogus NXDOMAIN strings into valid subnets. +func parseBogusNXDOMAIN(confBogusNXDOMAIN []string) (subnets []netip.Prefix, err error) { + for i, s := range confBogusNXDOMAIN { + var subnet netip.Prefix + subnet, err = aghnet.ParseSubnet(s) + if err != nil { + return nil, fmt.Errorf("subnet at index %d: %w", i, err) + } + + subnets = append(subnets, subnet) + } + + return subnets, nil +} + const defaultBlockedResponseTTL = 3600 // initDefaultSettings initializes default settings if nothing @@ -423,10 +449,7 @@ func collectListenAddr( // collectDNSAddrs returns configured set of listening addresses. It also // returns a set of ports of each unspecified listening address. -func (conf *ServerConfig) collectDNSAddrs() ( - addrs map[netip.AddrPort]unit, - unspecPorts map[uint16]unit, -) { +func (conf *ServerConfig) collectDNSAddrs() (addrs mapAddrPortSet, unspecPorts map[uint16]unit) { // TODO(e.burkov): Perhaps, we shouldn't allocate as much memory, since the // TCP and UDP listening addresses are currently the same. addrs = make(map[netip.AddrPort]unit, len(conf.TCPListenAddrs)+len(conf.UDPListenAddrs)) @@ -446,20 +469,64 @@ func (conf *ServerConfig) collectDNSAddrs() ( // defaultPlainDNSPort is the default port for plain DNS. const defaultPlainDNSPort uint16 = 53 -// addrPortMatcher is a function that matches an IP address with port. -type addrPortMatcher func(addr netip.AddrPort) (ok bool) +// addrPortSet is a set of [netip.AddrPort] values. +type addrPortSet interface { + // Has returns true if addrPort is in the set. + Has(addrPort netip.AddrPort) (ok bool) +} + +// type check +var _ addrPortSet = emptyAddrPortSet{} + +// emptyAddrPortSet is the [addrPortSet] containing no values. +type emptyAddrPortSet struct{} + +// Has implements the [addrPortSet] interface for [emptyAddrPortSet]. +func (emptyAddrPortSet) Has(_ netip.AddrPort) (ok bool) { return false } + +// mapAddrPortSet is the [addrPortSet] containing values of [netip.AddrPort] as +// keys of a map. +type mapAddrPortSet map[netip.AddrPort]unit + +// type check +var _ addrPortSet = mapAddrPortSet{} + +// Has implements the [addrPortSet] interface for [mapAddrPortSet]. +func (m mapAddrPortSet) Has(addrPort netip.AddrPort) (ok bool) { + _, ok = m[addrPort] + + return ok +} + +// combinedAddrPortSet is the [addrPortSet] defined by some IP addresses along +// with ports, any combination of which is considered being in the set. +type combinedAddrPortSet struct { + // TODO(e.burkov): Use sorted slices in combination with binary search. + ports map[uint16]unit + addrs []netip.Addr +} + +// type check +var _ addrPortSet = (*combinedAddrPortSet)(nil) + +// Has implements the [addrPortSet] interface for [*combinedAddrPortSet]. +func (m *combinedAddrPortSet) Has(addrPort netip.AddrPort) (ok bool) { + _, ok = m.ports[addrPort.Port()] + + return ok && slices.Contains(m.addrs, addrPort.Addr()) +} // filterOut filters out all the upstreams that match um. It returns all the // closing errors joined. -func (m addrPortMatcher) filterOut(upsConf *proxy.UpstreamConfig) (err error) { +func filterOutAddrs(upsConf *proxy.UpstreamConfig, set addrPortSet) (err error) { var errs []error delFunc := func(u upstream.Upstream) (ok bool) { // TODO(e.burkov): We should probably consider the protocol of u to // only filter out the listening addresses of the same protocol. addr, parseErr := aghnet.ParseAddrPort(u.Address(), defaultPlainDNSPort) - if parseErr != nil || !m(addr) { + if parseErr != nil || !set.Has(addr) { // Don't filter out the upstream if it either cannot be parsed, or - // does not match um. + // does not match m. return false } @@ -479,26 +546,20 @@ func (m addrPortMatcher) filterOut(upsConf *proxy.UpstreamConfig) (err error) { return errors.Join(errs...) } -// ourAddrsMatcher returns a matcher that matches all the configured listening +// ourAddrsSet returns an addrPortSet that contains all the configured listening // addresses. -func (conf *ServerConfig) ourAddrsMatcher() (m addrPortMatcher, err error) { +func (conf *ServerConfig) ourAddrsSet() (m addrPortSet, err error) { addrs, unspecPorts := conf.collectDNSAddrs() - if len(addrs) == 0 { + switch { + case len(addrs) == 0: log.Debug("dnsforward: no listen addresses") - // Match no addresses. - return func(_ netip.AddrPort) (ok bool) { return false }, nil - } - - if len(unspecPorts) == 0 { + return emptyAddrPortSet{}, nil + case len(unspecPorts) == 0: log.Debug("dnsforward: filtering out addresses %s", addrs) - m = func(a netip.AddrPort) (ok bool) { - _, ok = addrs[a] - - return ok - } - } else { + return addrs, nil + default: var ifaceAddrs []netip.Addr ifaceAddrs, err = aghnet.CollectAllIfacesAddrs() if err != nil { @@ -508,16 +569,11 @@ func (conf *ServerConfig) ourAddrsMatcher() (m addrPortMatcher, err error) { log.Debug("dnsforward: filtering out addresses %s on ports %d", ifaceAddrs, unspecPorts) - m = func(a netip.AddrPort) (ok bool) { - if _, ok = unspecPorts[a.Port()]; ok { - return slices.Contains(ifaceAddrs, a.Addr()) - } - - return false - } + return &combinedAddrPortSet{ + ports: unspecPorts, + addrs: ifaceAddrs, + }, nil } - - return m, nil } // prepareTLS - prepares TLS configuration for the DNS proxy @@ -574,7 +630,7 @@ func (s *Server) prepareTLS(proxyConfig *proxy.Config) (err error) { // isWildcard returns true if host is a wildcard hostname. func isWildcard(host string) (ok bool) { - return len(host) >= 2 && host[0] == '*' && host[1] == '.' + return strings.HasPrefix(host, "*.") } // matchesDomainWildcard returns true if host matches the domain wildcard @@ -614,6 +670,31 @@ func (s *Server) onGetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, er return &s.conf.cert, nil } +// preparePlain prepares the plain-DNS configuration for the DNS proxy. +// preparePlain assumes that prepareTLS has already been called. +func (s *Server) preparePlain(proxyConf *proxy.Config) (err error) { + if s.conf.ServePlainDNS { + proxyConf.UDPListenAddr = s.conf.UDPListenAddrs + proxyConf.TCPListenAddr = s.conf.TCPListenAddrs + + return nil + } + + lenEncrypted := len(proxyConf.DNSCryptTCPListenAddr) + + len(proxyConf.DNSCryptUDPListenAddr) + + len(proxyConf.HTTPSListenAddr) + + len(proxyConf.QUICListenAddr) + + len(proxyConf.TLSListenAddr) + if lenEncrypted == 0 { + // TODO(a.garipov): Support full disabling of all DNS. + return errors.Error("disabling plain dns requires at least one encrypted protocol") + } + + log.Info("dnsforward: warning: plain dns is disabled") + + return nil +} + // UpdatedProtectionStatus updates protection state, if the protection was // disabled temporarily. Returns the updated state of protection. func (s *Server) UpdatedProtectionStatus() (enabled bool, disabledUntil *time.Time) { diff --git a/internal/dnsforward/configvalidator.go b/internal/dnsforward/configvalidator.go new file mode 100644 index 00000000..b55f53cb --- /dev/null +++ b/internal/dnsforward/configvalidator.go @@ -0,0 +1,349 @@ +package dnsforward + +import ( + "fmt" + "strings" + "sync" + + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/log" + "github.com/miekg/dns" + "golang.org/x/exp/slices" +) + +// upstreamConfigValidator parses the [*proxy.UpstreamConfig] and checks the +// actual DNS availability of each upstream. +type upstreamConfigValidator struct { + // general is the general upstream configuration. + general []*upstreamResult + + // fallback is the fallback upstream configuration. + fallback []*upstreamResult + + // private is the private upstream configuration. + private []*upstreamResult +} + +// upstreamResult is a result of validation of an [upstream.Upstream] within an +// [proxy.UpstreamConfig]. +type upstreamResult struct { + // server is the parsed upstream. It is nil when there was an error during + // parsing. + server upstream.Upstream + + // err is the error either from parsing or from checking the upstream. + err error + + // original is the piece of configuration that have either been turned to an + // upstream or caused an error. + original string + + // isSpecific is true if the upstream is domain-specific. + isSpecific bool +} + +// compare compares two [upstreamResult]s. It returns 0 if they are equal, -1 +// if ur should be sorted before other, and 1 otherwise. +// +// TODO(e.burkov): Perhaps it makes sense to sort the results with errors near +// the end. +func (ur *upstreamResult) compare(other *upstreamResult) (res int) { + return strings.Compare(ur.original, other.original) +} + +// newUpstreamConfigValidator parses the upstream configuration and returns a +// validator for it. cv already contains the parsed upstreams along with errors +// related. +func newUpstreamConfigValidator( + general []string, + fallback []string, + private []string, + opts *upstream.Options, +) (cv *upstreamConfigValidator) { + cv = &upstreamConfigValidator{} + + for _, line := range general { + cv.general = cv.insertLineResults(cv.general, line, opts) + } + for _, line := range fallback { + cv.fallback = cv.insertLineResults(cv.fallback, line, opts) + } + for _, line := range private { + cv.private = cv.insertLineResults(cv.private, line, opts) + } + + return cv +} + +// insertLineResults parses line and inserts the result into s. It can insert +// multiple results as well as none. +func (cv *upstreamConfigValidator) insertLineResults( + s []*upstreamResult, + line string, + opts *upstream.Options, +) (result []*upstreamResult) { + upstreams, isSpecific, err := splitUpstreamLine(line) + if err != nil { + return cv.insert(s, &upstreamResult{ + err: err, + original: line, + }) + } + + for _, upstreamAddr := range upstreams { + var res *upstreamResult + if upstreamAddr != "#" { + res = cv.parseUpstream(upstreamAddr, opts) + } else if !isSpecific { + res = &upstreamResult{ + err: errNotDomainSpecific, + original: upstreamAddr, + } + } else { + continue + } + + res.isSpecific = isSpecific + s = cv.insert(s, res) + } + + return s +} + +// insert inserts r into slice in a sorted order, except duplicates. slice must +// not be nil. +func (cv *upstreamConfigValidator) insert( + s []*upstreamResult, + r *upstreamResult, +) (result []*upstreamResult) { + i, has := slices.BinarySearchFunc(s, r, (*upstreamResult).compare) + if has { + log.Debug("dnsforward: duplicate configuration %q", r.original) + + return s + } + + return slices.Insert(s, i, r) +} + +// parseUpstream parses addr and returns the result of parsing. It returns nil +// if the specified server points at the default upstream server which is +// validated separately. +func (cv *upstreamConfigValidator) parseUpstream( + addr string, + opts *upstream.Options, +) (r *upstreamResult) { + // Check if the upstream has a valid protocol prefix. + // + // TODO(e.burkov): Validate the domain name. + if proto, _, ok := strings.Cut(addr, "://"); ok { + if !slices.Contains(protocols, proto) { + return &upstreamResult{ + err: fmt.Errorf("bad protocol %q", proto), + original: addr, + } + } + } + + ups, err := upstream.AddressToUpstream(addr, opts) + + return &upstreamResult{ + server: ups, + err: err, + original: addr, + } +} + +// check tries to exchange with each successfully parsed upstream and enriches +// the results with the healthcheck errors. It should not be called after the +// [upsConfValidator.close] method, since it makes no sense to check the closed +// upstreams. +func (cv *upstreamConfigValidator) check() { + const ( + // testTLD is the special-use fully-qualified domain name for testing + // the DNS server reachability. + // + // See https://datatracker.ietf.org/doc/html/rfc6761#section-6.2. + testTLD = "test." + + // inAddrARPATLD is the special-use fully-qualified domain name for PTR + // IP address resolution. + // + // See https://datatracker.ietf.org/doc/html/rfc1035#section-3.5. + inAddrARPATLD = "in-addr.arpa." + ) + + commonChecker := &healthchecker{ + hostname: testTLD, + qtype: dns.TypeA, + ansEmpty: true, + } + + arpaChecker := &healthchecker{ + hostname: inAddrARPATLD, + qtype: dns.TypePTR, + ansEmpty: false, + } + + wg := &sync.WaitGroup{} + wg.Add(len(cv.general) + len(cv.fallback) + len(cv.private)) + + for _, res := range cv.general { + go cv.checkSrv(res, wg, commonChecker) + } + for _, res := range cv.fallback { + go cv.checkSrv(res, wg, commonChecker) + } + for _, res := range cv.private { + go cv.checkSrv(res, wg, arpaChecker) + } + + wg.Wait() +} + +// checkSrv runs hc on the server from res, if any, and stores any occurred +// error in res. wg is always marked done in the end. It used to be called in +// a separate goroutine. +func (cv *upstreamConfigValidator) checkSrv( + res *upstreamResult, + wg *sync.WaitGroup, + hc *healthchecker, +) { + defer wg.Done() + + if res.server == nil { + return + } + + res.err = hc.check(res.server) + if res.err != nil && res.isSpecific { + res.err = domainSpecificTestError{Err: res.err} + } +} + +// close closes all the upstreams that were successfully parsed. It enriches +// the results with deferred closing errors. +func (cv *upstreamConfigValidator) close() { + for _, slice := range [][]*upstreamResult{cv.general, cv.fallback, cv.private} { + for _, r := range slice { + if r.server != nil { + r.err = errors.WithDeferred(r.err, r.server.Close()) + } + } + } +} + +// status returns all the data collected during parsing, healthcheck, and +// closing of the upstreams. The returned map is keyed by the original upstream +// configuration piece and contains the corresponding error or "OK" if there was +// no error. +func (cv *upstreamConfigValidator) status() (results map[string]string) { + result := map[string]string{} + + for _, res := range cv.general { + resultToStatus("general", res, result) + } + for _, res := range cv.fallback { + resultToStatus("fallback", res, result) + } + for _, res := range cv.private { + resultToStatus("private", res, result) + } + + return result +} + +// resultToStatus puts "OK" or an error message from res into resMap. section +// is the name of the upstream configuration section, i.e. "general", +// "fallback", or "private", and only used for logging. +// +// TODO(e.burkov): Currently, the HTTP handler expects that all the results are +// put together in a single map, which may lead to collisions, see AG-27539. +// Improve the results compilation. +func resultToStatus(section string, res *upstreamResult, resMap map[string]string) { + val := "OK" + if res.err != nil { + val = res.err.Error() + } + + prevVal := resMap[res.original] + switch prevVal { + case "": + resMap[res.original] = val + case val: + log.Debug("dnsforward: duplicating %s config line %q", section, res.original) + default: + log.Debug( + "dnsforward: warning: %s config line %q (%v) had different result %v", + section, + val, + res.original, + prevVal, + ) + } +} + +// domainSpecificTestError is a wrapper for errors returned by checkDNS to mark +// the tested upstream domain-specific and therefore consider its errors +// non-critical. +// +// TODO(a.garipov): Some common mechanism of distinguishing between errors and +// warnings (non-critical errors) is desired. +type domainSpecificTestError struct { + // Err is the actual error occurred during healthcheck test. + Err error +} + +// type check +var _ error = domainSpecificTestError{} + +// Error implements the [error] interface for domainSpecificTestError. +func (err domainSpecificTestError) Error() (msg string) { + return fmt.Sprintf("WARNING: %s", err.Err) +} + +// type check +var _ errors.Wrapper = domainSpecificTestError{} + +// Unwrap implements the [errors.Wrapper] interface for domainSpecificTestError. +func (err domainSpecificTestError) Unwrap() (wrapped error) { + return err.Err +} + +// healthchecker checks the upstream's status by exchanging with it. +type healthchecker struct { + // hostname is the name of the host to put into healthcheck DNS request. + hostname string + + // qtype is the type of DNS request to use for healthcheck. + qtype uint16 + + // ansEmpty defines if the answer section within the response is expected to + // be empty. + ansEmpty bool +} + +// check exchanges with u and validates the response. +func (h *healthchecker) check(u upstream.Upstream) (err error) { + req := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{ + Name: h.hostname, + Qtype: h.qtype, + Qclass: dns.ClassINET, + }}, + } + + reply, err := u.Exchange(req) + if err != nil { + return fmt.Errorf("couldn't communicate with upstream: %w", err) + } else if h.ansEmpty && len(reply.Answer) > 0 { + return errWrongResponse + } + + return nil +} diff --git a/internal/dnsforward/dialcontext.go b/internal/dnsforward/dialcontext.go index c2ffe1e7..edb96b14 100644 --- a/internal/dnsforward/dialcontext.go +++ b/internal/dnsforward/dialcontext.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net" + "net/netip" + "strconv" "time" "github.com/AdguardTeam/golibs/errors" @@ -11,10 +13,12 @@ import ( ) // DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames. +// addr should be a valid host:port address, where host could be a domain name +// or an IP address. func (s *Server) DialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) { log.Debug("dnsforward: dialing %q for network %q", addr, network) - host, port, err := net.SplitHostPort(addr) + host, portStr, err := net.SplitHostPort(addr) if err != nil { return nil, err } @@ -28,21 +32,24 @@ func (s *Server) DialContext(ctx context.Context, network, addr string) (conn ne return dialer.DialContext(ctx, network, addr) } - addrs, err := s.Resolve(host) + port, err := strconv.Atoi(portStr) if err != nil { - return nil, fmt.Errorf("resolving %q: %w", host, err) + return nil, fmt.Errorf("invalid port %s: %w", portStr, err) } - log.Debug("dnsforward: resolving %q: %v", host, addrs) - - if len(addrs) == 0 { + ips, err := s.Resolve(ctx, network, host) + if err != nil { + return nil, fmt.Errorf("resolving %q: %w", host, err) + } else if len(ips) == 0 { return nil, fmt.Errorf("no addresses for host %q", host) } + log.Debug("dnsforward: resolved %q: %v", host, ips) + var dialErrs []error - for _, a := range addrs { - addr = net.JoinHostPort(a.String(), port) - conn, err = dialer.DialContext(ctx, network, addr) + for _, ip := range ips { + addrPort := netip.AddrPortFrom(ip, uint16(port)) + conn, err = dialer.DialContext(ctx, network, addrPort.String()) if err != nil { dialErrs = append(dialErrs, err) diff --git a/internal/dnsforward/dns64_test.go b/internal/dnsforward/dns64_test.go index 53a18c4e..55c08db7 100644 --- a/internal/dnsforward/dns64_test.go +++ b/internal/dnsforward/dns64_test.go @@ -292,6 +292,7 @@ func TestServer_HandleDNSRequest_dns64(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, localUps) t.Run(tc.name, func(t *testing.T) { diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index d4c61205..8afbd3df 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -2,7 +2,9 @@ package dnsforward import ( + "context" "fmt" + "io" "net" "net/http" "net/netip" @@ -34,6 +36,11 @@ import ( // DefaultTimeout is the default upstream timeout const DefaultTimeout = 10 * time.Second +// defaultLocalTimeout is the default timeout for resolving addresses from +// locally-served networks. It is assumed that local resolvers should work much +// faster than ordinary upstreams. +const defaultLocalTimeout = 1 * time.Second + // defaultClientIDCacheCount is the default count of items in the LRU ClientID // cache. The assumption here is that there won't be more than this many // requests between the BeforeRequestHandler stage and the actual processing. @@ -108,7 +115,7 @@ type Server struct { // stats is the statistics collector for client's DNS usage data. stats stats.Interface - // access drops unallowed clients. + // access drops disallowed clients. access *accessManager // localDomainSuffix is the suffix used to detect internal hosts. It @@ -135,8 +142,21 @@ type Server struct { // PTR resolving. sysResolvers SystemResolvers - // recDetector is a cache for recursive requests. It is used to detect - // and prevent recursive requests only for private upstreams. + // etcHosts contains the data from the system's hosts files. + etcHosts upstream.Resolver + + // bootstrap is the resolver for upstreams' hostnames. + bootstrap upstream.Resolver + + // bootResolvers are the resolvers that should be used for + // bootstrapping along with [etcHosts]. + // + // TODO(e.burkov): Use [proxy.UpstreamConfig] when it will implement the + // [upstream.Resolver] interface. + bootResolvers []*upstream.UpstreamResolver + + // recDetector is a cache for recursive requests. It is used to detect and + // prevent recursive requests only for private upstreams. // // See https://github.com/adguardTeam/adGuardHome/issues/3185#issuecomment-851048135. recDetector *recursionDetector @@ -153,8 +173,8 @@ type Server struct { // during the BeforeRequestHandler stage. clientIDCache cache.Cache - // DNS proxy instance for internal usage - // We don't Start() it and so no listen port is required. + // internalProxy resolves internal requests from the application itself. It + // isn't started and so no listen ports are required. internalProxy *proxy.Proxy // isRunning is true if the DNS server is running. @@ -185,6 +205,7 @@ type DNSCreateParams struct { DHCPServer DHCP PrivateNets netutil.SubnetSet Anonymizer *aghnet.IPMut + EtcHosts *aghnet.HostsContainer LocalDomain string } @@ -217,8 +238,10 @@ func NewServer(p DNSCreateParams) (s *Server, err error) { if p.Anonymizer == nil { p.Anonymizer = aghnet.NewIPMut(nil) } + s = &Server{ dnsFilter: p.DNSFilter, + dhcpServer: p.DHCPServer, stats: p.Stats, queryLog: p.QueryLog, privateNets: p.PrivateNets, @@ -230,6 +253,12 @@ func NewServer(p DNSCreateParams) (s *Server, err error) { MaxCount: defaultClientIDCacheCount, }), anonymizer: p.Anonymizer, + conf: ServerConfig{ + ServePlainDNS: true, + }, + } + if p.EtcHosts != nil { + s.etcHosts = p.EtcHosts } s.sysResolvers, err = sysresolv.NewSystemResolvers(nil, defaultPlainDNSPort) @@ -237,8 +266,6 @@ func NewServer(p DNSCreateParams) (s *Server, err error) { return nil, fmt.Errorf("initializing system resolvers: %w", err) } - s.dhcpServer = p.DHCPServer - if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" { // Use plain DNS on MIPS, encryption is too slow defaultDNS = defaultBootstrap @@ -274,7 +301,7 @@ func (s *Server) WriteDiskConfig(c *Config) { sc := s.conf.Config *c = sc - c.RatelimitWhitelist = stringutil.CloneSlice(sc.RatelimitWhitelist) + c.RatelimitWhitelist = slices.Clone(sc.RatelimitWhitelist) c.BootstrapDNS = stringutil.CloneSlice(sc.BootstrapDNS) c.FallbackDNS = stringutil.CloneSlice(sc.FallbackDNS) c.AllowedClients = stringutil.CloneSlice(sc.AllowedClients) @@ -305,15 +332,14 @@ func (s *Server) AddrProcConfig() (c *client.DefaultAddrProcConfig) { } } -// Resolve - get IP addresses by host name from an upstream server. -// No request/response filtering is performed. -// Query log and Stats are not updated. -// This method may be called before Start(). -func (s *Server) Resolve(host string) ([]net.IPAddr, error) { +// Resolve gets IP addresses by host name from an upstream server. No +// request/response filtering is performed. Query log and Stats are not +// updated. This method may be called before [Server.Start]. +func (s *Server) Resolve(ctx context.Context, net, host string) (addr []netip.Addr, err error) { s.serverLock.RLock() defer s.serverLock.RUnlock() - return s.internalProxy.LookupIPAddr(host) + return s.internalProxy.LookupNetIP(ctx, net, host) } const ( @@ -421,7 +447,7 @@ func hostFromPTR(resp *dns.Msg) (host string, ttl time.Duration, err error) { return "", 0, ErrRDNSNoData } -// Start starts the DNS server. +// Start starts the DNS server. It must only be called after [Server.Prepare]. func (s *Server) Start() error { s.serverLock.Lock() defer s.serverLock.Unlock() @@ -429,48 +455,42 @@ func (s *Server) Start() error { return s.startLocked() } -// startLocked starts the DNS server without locking. For internal use only. +// startLocked starts the DNS server without locking. s.serverLock is expected +// to be locked. func (s *Server) startLocked() error { err := s.dnsProxy.Start() if err == nil { s.isRunning = true } + return err } -// defaultLocalTimeout is the default timeout for resolving addresses from -// locally-served networks. It is assumed that local resolvers should work much -// faster than ordinary upstreams. -const defaultLocalTimeout = 1 * time.Second - -// setupLocalResolvers initializes the resolvers for local addresses. For -// internal use only. -func (s *Server) setupLocalResolvers() (err error) { - matcher, err := s.conf.ourAddrsMatcher() +// setupLocalResolvers initializes the resolvers for local addresses. It +// assumes s.serverLock is locked or the Server not running. +func (s *Server) setupLocalResolvers(boot upstream.Resolver) (err error) { + set, err := s.conf.ourAddrsSet() if err != nil { // Don't wrap the error because it's informative enough as is. return err } - bootstraps := s.conf.BootstrapDNS resolvers := s.conf.LocalPTRResolvers - filterConfig := false - - if len(resolvers) == 0 { - sysResolvers := slices.DeleteFunc(s.sysResolvers.Addrs(), matcher) + confNeedsFiltering := len(resolvers) > 0 + if confNeedsFiltering { + resolvers = stringutil.FilterOut(resolvers, IsCommentOrEmpty) + } else { + sysResolvers := slices.DeleteFunc(slices.Clone(s.sysResolvers.Addrs()), set.Has) resolvers = make([]string, 0, len(sysResolvers)) for _, r := range sysResolvers { resolvers = append(resolvers, r.String()) } - } else { - resolvers = stringutil.FilterOut(resolvers, IsCommentOrEmpty) - filterConfig = true } log.Debug("dnsforward: upstreams to resolve ptr for local addresses: %v", resolvers) uc, err := s.prepareUpstreamConfig(resolvers, nil, &upstream.Options{ - Bootstrap: bootstraps, + Bootstrap: boot, Timeout: defaultLocalTimeout, // TODO(e.burkov): Should we verify server's certificates? PreferIPv6: s.conf.BootstrapPreferIPv6, @@ -479,8 +499,9 @@ func (s *Server) setupLocalResolvers() (err error) { return fmt.Errorf("preparing private upstreams: %w", err) } - if filterConfig { - if err = matcher.filterOut(uc); err != nil { + if confNeedsFiltering { + err = filterOutAddrs(uc, set) + if err != nil { return fmt.Errorf("filtering private upstreams: %w", err) } } @@ -491,6 +512,7 @@ func (s *Server) setupLocalResolvers() (err error) { }, } + // TODO(e.burkov): Should we also consider the DNS64 usage? if s.conf.UsePrivateRDNS && // Only set the upstream config if there are any upstreams. It's safe // to put nil into [proxy.Config.PrivateRDNSUpstreamConfig]. @@ -517,31 +539,19 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) { s.initDefaultSettings() - err = s.prepareIpsetListSettings() - if err != nil { - // Don't wrap the error, because it's informative enough as is. - return fmt.Errorf("preparing ipset settings: %w", err) - } - - err = s.prepareUpstreamSettings() + boot, err := s.prepareInternalDNS() if err != nil { // Don't wrap the error, because it's informative enough as is. return err } - var proxyConfig proxy.Config - proxyConfig, err = s.createProxyConfig() + proxyConfig, err := s.newProxyConfig() if err != nil { return fmt.Errorf("preparing proxy: %w", err) } s.setupDNS64() - err = s.prepareInternalProxy() - if err != nil { - return fmt.Errorf("preparing internal proxy: %w", err) - } - s.access, err = newAccessCtx( s.conf.AllowedClients, s.conf.DisallowedClients, @@ -554,9 +564,9 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) { // Set the proxy here because [setupLocalResolvers] sets its values. // // TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy. - s.dnsProxy = &proxy.Proxy{Config: proxyConfig} + s.dnsProxy = &proxy.Proxy{Config: *proxyConfig} - err = s.setupLocalResolvers() + err = s.setupLocalResolvers(boot) if err != nil { return fmt.Errorf("setting up resolvers: %w", err) } @@ -575,6 +585,38 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) { return nil } +// prepareInternalDNS initializes the internal state of s before initializing +// the primary DNS proxy instance. It assumes s.serverLock is locked or the +// Server not running. +func (s *Server) prepareInternalDNS() (boot upstream.Resolver, err error) { + err = s.prepareIpsetListSettings() + if err != nil { + return nil, fmt.Errorf("preparing ipset settings: %w", err) + } + + s.bootstrap, s.bootResolvers, err = s.createBootstrap(s.conf.BootstrapDNS, &upstream.Options{ + Timeout: DefaultTimeout, + HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams), + }) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, err + } + + err = s.prepareUpstreamSettings(s.bootstrap) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return s.bootstrap, err + } + + err = s.prepareInternalProxy() + if err != nil { + return s.bootstrap, fmt.Errorf("preparing internal proxy: %w", err) + } + + return s.bootstrap, nil +} + // setupFallbackDNS initializes the fallback DNS servers. func (s *Server) setupFallbackDNS() (err error) { fallbacks := s.conf.FallbackDNS @@ -598,7 +640,8 @@ func (s *Server) setupFallbackDNS() (err error) { return nil } -// setupAddrProc initializes the address processor. For internal use only. +// setupAddrProc initializes the address processor. It assumes s.serverLock is +// locked or the Server not running. func (s *Server) setupAddrProc() { // TODO(a.garipov): This is a crutch for tests; remove. if s.conf.AddrProcConf == nil { @@ -687,7 +730,8 @@ func (s *Server) Stop() error { return s.stopLocked() } -// stopLocked stops the DNS server without locking. For internal use only. +// stopLocked stops the DNS server without locking. s.serverLock is expected to +// be locked. func (s *Server) stopLocked() (err error) { // TODO(e.burkov, a.garipov): Return critical errors, not just log them. // This will require filtering all the non-critical errors in @@ -700,18 +744,11 @@ func (s *Server) stopLocked() (err error) { } } - if upsConf := s.internalProxy.UpstreamConfig; upsConf != nil { - err = upsConf.Close() - if err != nil { - log.Error("dnsforward: closing internal resolvers: %s", err) - } - } + logCloserErr(s.internalProxy.UpstreamConfig, "dnsforward: closing internal resolvers: %s") + logCloserErr(s.localResolvers.UpstreamConfig, "dnsforward: closing local resolvers: %s") - if upsConf := s.localResolvers.UpstreamConfig; upsConf != nil { - err = upsConf.Close() - if err != nil { - log.Error("dnsforward: closing local resolvers: %s", err) - } + for _, b := range s.bootResolvers { + logCloserErr(b, "dnsforward: closing bootstrap %s: %s", b.Address()) } s.isRunning = false @@ -719,6 +756,18 @@ func (s *Server) stopLocked() (err error) { return nil } +// logCloserErr logs the error returned by c, if any. +func logCloserErr(c io.Closer, format string, args ...any) { + if c == nil { + return + } + + err := c.Close() + if err != nil { + log.Error(format, append(args, err)...) + } +} + // IsRunning returns true if the DNS server is running. func (s *Server) IsRunning() bool { s.serverLock.RLock() diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go index f315a661..2c30ff6c 100644 --- a/internal/dnsforward/dnsforward_test.go +++ b/internal/dnsforward/dnsforward_test.go @@ -54,13 +54,10 @@ const ( testMessagesCount = 10 ) -// testClientAddr is the common net.Addr for tests. +// testClientAddrPort is the common net.Addr for tests. // // TODO(a.garipov): Use more. -var testClientAddr net.Addr = &net.TCPAddr{ - IP: net.IP{1, 2, 3, 4}, - Port: 12345, -} +var testClientAddrPort = netip.MustParseAddrPort("1.2.3.4:12345") func startDeferStop(t *testing.T, s *Server) { t.Helper() @@ -182,6 +179,7 @@ func createTestTLS(t *testing.T, tlsConf TLSConfig) (s *Server, certPem []byte) Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, nil) tlsConf.CertificateChainData, tlsConf.PrivateKeyData = certPem, keyPem @@ -309,6 +307,7 @@ func TestServer(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, nil) s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} startDeferStop(t, s) @@ -347,6 +346,7 @@ func TestServer_timeout(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, } s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)}) @@ -381,6 +381,7 @@ func TestServer_Prepare_fallbacks(t *testing.T) { }, EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, } s, err := NewServer(DNSCreateParams{}) @@ -402,6 +403,7 @@ func TestServerWithProtectionDisabled(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, nil) s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} startDeferStop(t, s) @@ -479,6 +481,7 @@ func TestServerRace(t *testing.T) { UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, }, ConfigModified: func() {}, + ServePlainDNS: true, } s := createTestServer(t, filterConf, forwardConf, nil) s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()} @@ -532,6 +535,7 @@ func TestSafeSearch(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, filterConf, forwardConf, nil) startDeferStop(t, s) @@ -594,6 +598,7 @@ func TestInvalidRequest(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, }, nil) startDeferStop(t, s) @@ -622,6 +627,7 @@ func TestBlockedRequest(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ ProtectionEnabled: true, @@ -644,45 +650,71 @@ func TestBlockedRequest(t *testing.T) { } func TestServerCustomClientUpstream(t *testing.T) { + const defaultCacheSize = 1024 * 1024 + + var upsCalledCounter uint32 + forwardConf := ServerConfig{ UDPListenAddrs: []*net.UDPAddr{{}}, TCPListenAddrs: []*net.TCPAddr{{}}, Config: Config{ + CacheSize: defaultCacheSize, EDNSClientSubnet: &EDNSClientSubnet{ Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ BlockingMode: filtering.BlockingModeDefault, }, forwardConf, nil) - s.conf.GetCustomUpstreamByClient = func(_ string) (conf *proxy.UpstreamConfig, err error) { - ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { - return aghalg.Coalesce( - aghtest.MatchedResponse(req, dns.TypeA, "host", "192.168.0.1"), - new(dns.Msg).SetRcode(req, dns.RcodeNameError), - ), nil - }) - return &proxy.UpstreamConfig{ + ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { + atomic.AddUint32(&upsCalledCounter, 1) + + return aghalg.Coalesce( + aghtest.MatchedResponse(req, dns.TypeA, "host", "192.168.0.1"), + new(dns.Msg).SetRcode(req, dns.RcodeNameError), + ), nil + }) + + customUpsConf := proxy.NewCustomUpstreamConfig( + &proxy.UpstreamConfig{ Upstreams: []upstream.Upstream{ups}, - }, nil + }, + true, + defaultCacheSize, + forwardConf.EDNSClientSubnet.Enabled, + ) + + s.conf.ClientsContainer = &aghtest.ClientsContainer{ + OnUpstreamConfigByID: func( + _ string, + _ upstream.Resolver, + ) (conf *proxy.CustomUpstreamConfig, err error) { + return customUpsConf, nil + }, } + startDeferStop(t, s) - addr := s.dnsProxy.Addr(proxy.ProtoUDP) + addr := s.dnsProxy.Addr(proxy.ProtoUDP).String() // Send test request. req := createTestMessage("host.") - reply, err := dns.Exchange(req, addr.String()) + reply, err := dns.Exchange(req, addr) require.NoError(t, err) + require.NotEmpty(t, reply.Answer) + require.Len(t, reply.Answer, 1) assert.Equal(t, dns.RcodeSuccess, reply.Rcode) - require.NotEmpty(t, reply.Answer) - - require.Len(t, reply.Answer, 1) assert.Equal(t, net.IP{192, 168, 0, 1}, reply.Answer[0].(*dns.A).A) + assert.Equal(t, uint32(1), atomic.LoadUint32(&upsCalledCounter)) + + _, err = dns.Exchange(req, addr) + require.NoError(t, err) + assert.Equal(t, uint32(1), atomic.LoadUint32(&upsCalledCounter)) } // testCNAMEs is a map of names and CNAMEs necessary for the TestUpstream work. @@ -708,6 +740,7 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, }, nil) testUpstm := &aghtest.Upstream{ CName: testCNAMEs, @@ -740,6 +773,7 @@ func TestBlockCNAME(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ ProtectionEnabled: true, @@ -814,6 +848,7 @@ func TestClientRulesForCNAMEMatching(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ BlockingMode: filtering.BlockingModeDefault, @@ -858,6 +893,7 @@ func TestNullBlockedRequest(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ ProtectionEnabled: true, @@ -923,6 +959,7 @@ func TestBlockedCustomIP(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } // Invalid BlockingIPv4. @@ -974,6 +1011,7 @@ func TestBlockedByHosts(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ @@ -1024,6 +1062,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } s := createTestServer(t, filterConf, forwardConf, nil) startDeferStop(t, s) @@ -1082,6 +1121,7 @@ func TestRewrite(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, })) ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { diff --git a/internal/dnsforward/dnsrewrite_test.go b/internal/dnsforward/dnsrewrite_test.go index 79aecdef..1022388f 100644 --- a/internal/dnsforward/dnsrewrite_test.go +++ b/internal/dnsforward/dnsrewrite_test.go @@ -40,6 +40,7 @@ func TestServer_FilterDNSRewrite(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, nil) makeQ := func(qtype rules.RRType) (req *dns.Msg) { diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go index d80f022e..e627122e 100644 --- a/internal/dnsforward/filter.go +++ b/internal/dnsforward/filter.go @@ -10,7 +10,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/golibs/log" - "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/urlfilter/rules" "github.com/miekg/dns" "golang.org/x/exp/slices" @@ -28,8 +27,7 @@ func (s *Server) beforeRequestHandler( return false, fmt.Errorf("getting clientid: %w", err) } - addrPort := netutil.NetAddrToAddrPort(pctx.Addr) - blocked, _ := s.IsBlockedClient(addrPort.Addr(), clientID) + blocked, _ := s.IsBlockedClient(pctx.Addr.Addr(), clientID) if blocked { return s.preBlockedResponse(pctx) } @@ -60,8 +58,7 @@ func (s *Server) clientRequestFilteringSettings(dctx *dnsContext) (setts *filter setts = s.dnsFilter.Settings() setts.ProtectionEnabled = dctx.protectionEnabled if s.conf.FilterHandler != nil { - addrPort := netutil.NetAddrToAddrPort(dctx.proxyCtx.Addr) - s.conf.FilterHandler(addrPort.Addr(), dctx.clientID, setts) + s.conf.FilterHandler(dctx.proxyCtx.Addr.Addr(), dctx.clientID, setts) } return setts diff --git a/internal/dnsforward/filter_test.go b/internal/dnsforward/filter_test.go index fe64cdf0..6559b308 100644 --- a/internal/dnsforward/filter_test.go +++ b/internal/dnsforward/filter_test.go @@ -35,6 +35,7 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) { Enabled: false, }, }, + ServePlainDNS: true, } filters := []filtering.Filter{{ ID: 0, Data: []byte(rules), @@ -186,7 +187,7 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) { dctx := &proxy.DNSContext{ Proto: proxy.ProtoUDP, Req: tc.req, - Addr: &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 1}, + Addr: testClientAddrPort, } t.Run(tc.name, func(t *testing.T) { @@ -325,7 +326,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) { Proto: proxy.ProtoUDP, Req: tc.req, Res: resp, - Addr: &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 1}, + Addr: testClientAddrPort, } dctx := &dnsContext{ diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go index 19f06f37..ac82ea76 100644 --- a/internal/dnsforward/http.go +++ b/internal/dnsforward/http.go @@ -6,20 +6,15 @@ import ( "io" "net/http" "net/netip" - "strings" - "sync" "time" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/filtering" - "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/stringutil" - "github.com/miekg/dns" - "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) @@ -45,8 +40,19 @@ type jsonDNSConfig struct { // ProtectionEnabled defines if protection is enabled. ProtectionEnabled *bool `json:"protection_enabled"` - // RateLimit is the number of requests per second allowed per client. - RateLimit *uint32 `json:"ratelimit"` + // Ratelimit is the number of requests per second allowed per client. + Ratelimit *uint32 `json:"ratelimit"` + + // RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for + // rate limiting requests. + RatelimitSubnetLenIPv4 *int `json:"ratelimit_subnet_len_ipv4"` + + // RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for + // rate limiting requests. + RatelimitSubnetLenIPv6 *int `json:"ratelimit_subnet_len_ipv6"` + + // RatelimitWhitelist is a list of IP addresses excluded from rate limiting. + RatelimitWhitelist *[]netip.Addr `json:"ratelimit_whitelist"` // BlockingMode defines the way blocked responses are constructed. BlockingMode *filtering.BlockingMode `json:"blocking_mode"` @@ -121,6 +127,9 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) { blockingMode, blockingIPv4, blockingIPv6 := s.dnsFilter.BlockingMode() blockedResponseTTL := s.dnsFilter.BlockedResponseTTL() ratelimit := s.conf.Ratelimit + ratelimitSubnetLenIPv4 := s.conf.RatelimitSubnetLenIPv4 + ratelimitSubnetLenIPv6 := s.conf.RatelimitSubnetLenIPv6 + ratelimitWhitelist := append([]netip.Addr{}, s.conf.RatelimitWhitelist...) customIP := s.conf.EDNSClientSubnet.CustomIP enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled @@ -157,7 +166,10 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) { BlockingMode: &blockingMode, BlockingIPv4: blockingIPv4, BlockingIPv6: blockingIPv6, - RateLimit: &ratelimit, + Ratelimit: &ratelimit, + RatelimitSubnetLenIPv4: &ratelimitSubnetLenIPv4, + RatelimitSubnetLenIPv6: &ratelimitSubnetLenIPv6, + RatelimitWhitelist: &ratelimitWhitelist, EDNSCSCustomIP: customIP, EDNSCSEnabled: &enableEDNSClientSubnet, EDNSCSUseCustom: &useCustom, @@ -180,13 +192,13 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) { // defaultLocalPTRUpstreams returns the list of default local PTR resolvers // filtered of AdGuard Home's own DNS server addresses. It may appear empty. func (s *Server) defaultLocalPTRUpstreams() (ups []string, err error) { - matcher, err := s.conf.ourAddrsMatcher() + matcher, err := s.conf.ourAddrsSet() if err != nil { // Don't wrap the error because it's informative enough as is. return nil, err } - sysResolvers := slices.DeleteFunc(s.sysResolvers.Addrs(), matcher) + sysResolvers := slices.DeleteFunc(s.sysResolvers.Addrs(), matcher.Has) ups = make([]string, 0, len(sysResolvers)) for _, r := range sysResolvers { ups = append(ups, r.String()) @@ -201,6 +213,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { aghhttp.WriteJSONResponseOK(w, r, resp) } +// checkBlockingMode returns an error if blocking mode is invalid. func (req *jsonDNSConfig) checkBlockingMode() (err error) { if req.BlockingMode == nil { return nil @@ -209,12 +222,21 @@ func (req *jsonDNSConfig) checkBlockingMode() (err error) { return validateBlockingMode(*req.BlockingMode, req.BlockingIPv4, req.BlockingIPv6) } -func (req *jsonDNSConfig) checkUpstreamsMode() bool { - valid := []string{"", "fastest_addr", "parallel"} +// checkUpstreamsMode returns an error if the upstream mode is invalid. +func (req *jsonDNSConfig) checkUpstreamsMode() (err error) { + if req.UpstreamMode == nil { + return nil + } - return req.UpstreamMode == nil || stringutil.InSlice(valid, *req.UpstreamMode) + mode := *req.UpstreamMode + if ok := slices.Contains([]string{"", "fastest_addr", "parallel"}, mode); !ok { + return fmt.Errorf("upstream_mode: incorrect value %q", mode) + } + + return nil } +// checkBootstrap returns an error if any bootstrap address is invalid. func (req *jsonDNSConfig) checkBootstrap() (err error) { if req.Bootstraps == nil { return nil @@ -229,6 +251,7 @@ func (req *jsonDNSConfig) checkBootstrap() (err error) { } if _, err = upstream.NewUpstreamResolver(b, nil); err != nil { + // Don't wrap the error because it's informative enough as is. return err } } @@ -244,67 +267,136 @@ func (req *jsonDNSConfig) checkFallbacks() (err error) { err = ValidateUpstreams(*req.Fallbacks) if err != nil { - return fmt.Errorf("validating fallback servers: %w", err) + return fmt.Errorf("fallback servers: %w", err) } return nil } // validate returns an error if any field of req is invalid. +// +// TODO(s.chzhen): Parse, don't validate. func (req *jsonDNSConfig) validate(privateNets netutil.SubnetSet) (err error) { + defer func() { err = errors.Annotate(err, "validating dns config: %w") }() + + err = req.validateUpstreamDNSServers(privateNets) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + err = req.checkRatelimitSubnetMaskLen() + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + err = req.checkBlockingMode() + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + err = req.checkUpstreamsMode() + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + err = req.checkCacheTTL() + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + return nil +} + +// validateUpstreamDNSServers returns an error if any field of req is invalid. +func (req *jsonDNSConfig) validateUpstreamDNSServers(privateNets netutil.SubnetSet) (err error) { if req.Upstreams != nil { err = ValidateUpstreams(*req.Upstreams) if err != nil { - return fmt.Errorf("validating upstream servers: %w", err) + return fmt.Errorf("upstream servers: %w", err) } } if req.LocalPTRUpstreams != nil { err = ValidateUpstreamsPrivate(*req.LocalPTRUpstreams, privateNets) if err != nil { - return fmt.Errorf("validating private upstream servers: %w", err) + return fmt.Errorf("private upstream servers: %w", err) } } err = req.checkBootstrap() if err != nil { + // Don't wrap the error since it's informative enough as is. return err } err = req.checkFallbacks() if err != nil { + // Don't wrap the error since it's informative enough as is. return err } - err = req.checkBlockingMode() - if err != nil { - return err - } - - switch { - case !req.checkUpstreamsMode(): - return errors.Error("upstream_mode: incorrect value") - case !req.checkCacheTTL(): - return errors.Error("cache_ttl_min must be less or equal than cache_ttl_max") - default: - return nil - } + return nil } -func (req *jsonDNSConfig) checkCacheTTL() bool { +// checkCacheTTL returns an error if the configuration of the cache TTL is +// invalid. +func (req *jsonDNSConfig) checkCacheTTL() (err error) { if req.CacheMinTTL == nil && req.CacheMaxTTL == nil { - return true + return nil } - var min, max uint32 + var minTTL, maxTTL uint32 if req.CacheMinTTL != nil { - min = *req.CacheMinTTL + minTTL = *req.CacheMinTTL } if req.CacheMaxTTL != nil { - max = *req.CacheMaxTTL + maxTTL = *req.CacheMaxTTL } - return min <= max + if minTTL <= maxTTL { + return nil + } + + return errors.Error("cache_ttl_min must be less or equal than cache_ttl_max") +} + +// checkRatelimitSubnetMaskLen returns an error if the length of the subnet mask +// for IPv4 or IPv6 addresses is invalid. +func (req *jsonDNSConfig) checkRatelimitSubnetMaskLen() (err error) { + err = checkInclusion(req.RatelimitSubnetLenIPv4, 0, netutil.IPv4BitLen) + if err != nil { + return fmt.Errorf("ratelimit_subnet_len_ipv4 is invalid: %w", err) + } + + err = checkInclusion(req.RatelimitSubnetLenIPv6, 0, netutil.IPv6BitLen) + if err != nil { + return fmt.Errorf("ratelimit_subnet_len_ipv6 is invalid: %w", err) + } + + return nil +} + +// checkInclusion returns an error if a ptr is not nil and points to value, +// that not in the inclusive range between minN and maxN. +func checkInclusion(ptr *int, minN, maxN int) (err error) { + if ptr == nil { + return nil + } + + n := *ptr + switch { + case n < minN: + return fmt.Errorf("value %d less than min %d", n, minN) + case n > maxN: + return fmt.Errorf("value %d greater than max %d", n, maxN) + } + + return nil } // handleSetConfig handles requests to the POST /control/dns_config endpoint. @@ -401,6 +493,9 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) { setIfNotNil(&s.conf.CacheOptimistic, dc.CacheOptimistic), setIfNotNil(&s.conf.AddrProcConf.UseRDNS, dc.ResolveClients), setIfNotNil(&s.conf.UsePrivateRDNS, dc.UsePrivateRDNS), + setIfNotNil(&s.conf.RatelimitSubnetLenIPv4, dc.RatelimitSubnetLenIPv4), + setIfNotNil(&s.conf.RatelimitSubnetLenIPv6, dc.RatelimitSubnetLenIPv6), + setIfNotNil(&s.conf.RatelimitWhitelist, dc.RatelimitWhitelist), } { shouldRestart = shouldRestart || hasSet if shouldRestart { @@ -408,8 +503,8 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) { } } - if dc.RateLimit != nil && s.conf.Ratelimit != *dc.RateLimit { - s.conf.Ratelimit = *dc.RateLimit + if dc.Ratelimit != nil && s.conf.Ratelimit != *dc.Ratelimit { + s.conf.Ratelimit = *dc.Ratelimit shouldRestart = true } @@ -424,374 +519,11 @@ type upstreamJSON struct { PrivateUpstreams []string `json:"private_upstream"` } -// IsCommentOrEmpty returns true if s starts with a "#" character or is empty. -// This function is useful for filtering out non-upstream lines from upstream -// configs. -func IsCommentOrEmpty(s string) (ok bool) { - return len(s) == 0 || s[0] == '#' -} - -// newUpstreamConfig validates upstreams and returns an appropriate upstream -// configuration or nil if it can't be built. -// -// TODO(e.burkov): Perhaps proxy.ParseUpstreamsConfig should validate upstreams -// slice already so that this function may be considered useless. -func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err error) { - // No need to validate comments and empty lines. - upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty) - if len(upstreams) == 0 { - // Consider this case valid since it means the default server should be - // used. - return nil, nil +// closeBoots closes all the provided bootstrap servers and logs errors if any. +func closeBoots(boots []*upstream.UpstreamResolver) { + for _, c := range boots { + logCloserErr(c, "dnsforward: closing bootstrap %s: %s", c.Address()) } - - err = validateUpstreamConfig(upstreams) - if err != nil { - // Don't wrap the error since it's informative enough as is. - return nil, err - } - - conf, err = proxy.ParseUpstreamsConfig( - upstreams, - &upstream.Options{ - Bootstrap: []string{}, - Timeout: DefaultTimeout, - }, - ) - if err != nil { - // Don't wrap the error since it's informative enough as is. - return nil, err - } else if len(conf.Upstreams) == 0 { - return nil, errors.Error("no default upstreams specified") - } - - return conf, nil -} - -// validateUpstreamConfig validates each upstream from the upstream -// configuration and returns an error if any upstream is invalid. -// -// TODO(e.burkov): Move into aghnet or even into dnsproxy. -func validateUpstreamConfig(conf []string) (err error) { - for _, u := range conf { - var ups []string - var domains []string - ups, domains, err = separateUpstream(u) - if err != nil { - // Don't wrap the error since it's informative enough as is. - return err - } - - for _, addr := range ups { - _, err = validateUpstream(addr, domains) - if err != nil { - return fmt.Errorf("validating upstream %q: %w", addr, err) - } - } - } - - return nil -} - -// ValidateUpstreams validates each upstream and returns an error if any -// upstream is invalid or if there are no default upstreams specified. -// -// TODO(e.burkov): Move into aghnet or even into dnsproxy. -func ValidateUpstreams(upstreams []string) (err error) { - _, err = newUpstreamConfig(upstreams) - - return err -} - -// ValidateUpstreamsPrivate validates each upstream and returns an error if any -// upstream is invalid or if there are no default upstreams specified. It also -// checks each domain of domain-specific upstreams for being ARPA pointing to -// a locally-served network. privateNets must not be nil. -func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet) (err error) { - conf, err := newUpstreamConfig(upstreams) - if err != nil { - return fmt.Errorf("creating config: %w", err) - } - - if conf == nil { - return nil - } - - keys := maps.Keys(conf.DomainReservedUpstreams) - slices.Sort(keys) - - var errs []error - for _, domain := range keys { - var subnet netip.Prefix - subnet, err = extractARPASubnet(domain) - if err != nil { - errs = append(errs, err) - - continue - } - - if !privateNets.Contains(subnet.Addr().AsSlice()) { - errs = append( - errs, - fmt.Errorf("arpa domain %q should point to a locally-served network", domain), - ) - } - } - - return errors.Annotate(errors.Join(errs...), "checking domain-specific upstreams: %w") -} - -var protocols = []string{ - "h3://", - "https://", - "quic://", - "sdns://", - "tcp://", - "tls://", - "udp://", -} - -// validateUpstream returns an error if u alongside with domains is not a valid -// upstream configuration. useDefault is true if the upstream is -// domain-specific and is configured to point at the default upstream server -// which is validated separately. The upstream is considered domain-specific -// only if domains is at least not nil. -func validateUpstream(u string, domains []string) (useDefault bool, err error) { - // The special server address '#' means that default server must be used. - if useDefault = u == "#" && domains != nil; useDefault { - return useDefault, nil - } - - // Check if the upstream has a valid protocol prefix. - // - // TODO(e.burkov): Validate the domain name. - for _, proto := range protocols { - if strings.HasPrefix(u, proto) { - return false, nil - } - } - - if proto, _, ok := strings.Cut(u, "://"); ok { - return false, fmt.Errorf("bad protocol %q", proto) - } - - // Check if upstream is either an IP or IP with port. - if _, err = netip.ParseAddr(u); err == nil { - return false, nil - } else if _, err = netip.ParseAddrPort(u); err == nil { - return false, nil - } - - return false, err -} - -// separateUpstream returns the upstreams and the specified domains. domains -// is nil when the upstream is not domains-specific. Otherwise it may also be -// empty. -func separateUpstream(upstreamStr string) (upstreams, domains []string, err error) { - if !strings.HasPrefix(upstreamStr, "[/") { - return []string{upstreamStr}, nil, nil - } - - defer func() { err = errors.Annotate(err, "bad upstream for domain %q: %w", upstreamStr) }() - - parts := strings.Split(upstreamStr[2:], "/]") - switch len(parts) { - case 2: - // Go on. - case 1: - return nil, nil, errors.Error("missing separator") - default: - return nil, nil, errors.Error("duplicated separator") - } - - for i, host := range strings.Split(parts[0], "/") { - if host == "" { - continue - } - - err = netutil.ValidateDomainName(strings.TrimPrefix(host, "*.")) - if err != nil { - return nil, nil, fmt.Errorf("domain at index %d: %w", i, err) - } - - domains = append(domains, host) - } - - return strings.Fields(parts[1]), domains, nil -} - -// healthCheckFunc is a signature of function to check if upstream exchanges -// properly. -type healthCheckFunc func(u upstream.Upstream) (err error) - -// checkDNSUpstreamExc checks if the DNS upstream exchanges correctly. -func checkDNSUpstreamExc(u upstream.Upstream) (err error) { - // testTLD is the special-use fully-qualified domain name for testing the - // DNS server reachability. - // - // See https://datatracker.ietf.org/doc/html/rfc6761#section-6.2. - const testTLD = "test." - - req := &dns.Msg{ - MsgHdr: dns.MsgHdr{ - Id: dns.Id(), - RecursionDesired: true, - }, - Question: []dns.Question{{ - Name: testTLD, - Qtype: dns.TypeA, - Qclass: dns.ClassINET, - }}, - } - - var reply *dns.Msg - reply, err = u.Exchange(req) - if err != nil { - return fmt.Errorf("couldn't communicate with upstream: %w", err) - } else if len(reply.Answer) != 0 { - return errors.Error("wrong response") - } - - return nil -} - -// checkPrivateUpstreamExc checks if the upstream for resolving private -// addresses exchanges correctly. -// -// TODO(e.burkov): Think about testing the ip6.arpa. as well. -func checkPrivateUpstreamExc(u upstream.Upstream) (err error) { - // inAddrArpaTLD is the special-use fully-qualified domain name for PTR IP - // address resolution. - // - // See https://datatracker.ietf.org/doc/html/rfc1035#section-3.5. - const inAddrArpaTLD = "in-addr.arpa." - - req := &dns.Msg{ - MsgHdr: dns.MsgHdr{ - Id: dns.Id(), - RecursionDesired: true, - }, - Question: []dns.Question{{ - Name: inAddrArpaTLD, - Qtype: dns.TypePTR, - Qclass: dns.ClassINET, - }}, - } - - if _, err = u.Exchange(req); err != nil { - return fmt.Errorf("couldn't communicate with upstream: %w", err) - } - - return nil -} - -// domainSpecificTestError is a wrapper for errors returned by checkDNS to mark -// the tested upstream domain-specific and therefore consider its errors -// non-critical. -// -// TODO(a.garipov): Some common mechanism of distinguishing between errors and -// warnings (non-critical errors) is desired. -type domainSpecificTestError struct { - error -} - -// Error implements the [error] interface for domainSpecificTestError. -func (err domainSpecificTestError) Error() (msg string) { - return fmt.Sprintf("WARNING: %s", err.error) -} - -// checkDNS parses line, creates DNS upstreams using opts, and checks if the -// upstreams are exchanging correctly. It saves the result into a sync.Map -// where key is an upstream address and value is "OK", if the upstream -// exchanges correctly, or text of the error. It is intended to be used as a -// goroutine. -// -// TODO(s.chzhen): Separate to a different structure/file. -func (s *Server) checkDNS( - line string, - opts *upstream.Options, - check healthCheckFunc, - wg *sync.WaitGroup, - m *sync.Map, -) { - defer wg.Done() - defer log.OnPanic("dnsforward: checking upstreams") - - upstreams, domains, err := separateUpstream(line) - if err != nil { - err = fmt.Errorf("wrong upstream format: %w", err) - m.Store(line, err.Error()) - - return - } - - specific := len(domains) > 0 - - for _, upstreamAddr := range upstreams { - var useDefault bool - useDefault, err = validateUpstream(upstreamAddr, domains) - if err != nil { - err = fmt.Errorf("wrong upstream format: %w", err) - m.Store(upstreamAddr, err.Error()) - - continue - } - - if useDefault { - continue - } - - log.Debug("dnsforward: checking if upstream %q works", upstreamAddr) - - err = s.checkUpstreamAddr(upstreamAddr, specific, opts, check) - if err != nil { - m.Store(upstreamAddr, err.Error()) - } else { - m.Store(upstreamAddr, "OK") - } - } -} - -// checkUpstreamAddr creates the DNS upstream using opts and information from -// [s.dnsFilter.EtcHosts]. Checks if the DNS upstream exchanges correctly. It -// returns an error if addr is not valid DNS upstream address or the upstream -// is not exchanging correctly. -func (s *Server) checkUpstreamAddr( - addr string, - specific bool, - opts *upstream.Options, - check healthCheckFunc, -) (err error) { - defer func() { - if err != nil && specific { - err = domainSpecificTestError{error: err} - } - }() - - opts = &upstream.Options{ - Bootstrap: opts.Bootstrap, - Timeout: opts.Timeout, - PreferIPv6: opts.PreferIPv6, - } - - // dnsFilter can be nil during application update. - if s.dnsFilter != nil { - recs := s.dnsFilter.EtcHostsRecords(extractUpstreamHost(addr)) - for _, rec := range recs { - opts.ServerIPAddrs = append(opts.ServerIPAddrs, rec.Addr.AsSlice()) - } - sortNetIPAddrs(opts.ServerIPAddrs, opts.PreferIPv6) - } - - u, err := upstream.AddressToUpstream(addr, opts) - if err != nil { - return fmt.Errorf("creating upstream for %q: %w", addr, err) - } - - defer func() { err = errors.WithDeferred(err, u.Close()) }() - - return check(u) } // handleTestUpstreamDNS handles requests to the POST /control/test_upstream_dns @@ -808,46 +540,27 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) { req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty) req.FallbackDNS = stringutil.FilterOut(req.FallbackDNS, IsCommentOrEmpty) req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty) + req.BootstrapDNS = stringutil.FilterOut(req.BootstrapDNS, IsCommentOrEmpty) opts := &upstream.Options{ - Bootstrap: req.BootstrapDNS, Timeout: s.conf.UpstreamTimeout, PreferIPv6: s.conf.BootstrapPreferIPv6, } - if len(opts.Bootstrap) == 0 { - opts.Bootstrap = defaultBootstrap + + var boots []*upstream.UpstreamResolver + opts.Bootstrap, boots, err = s.createBootstrap(req.BootstrapDNS, opts) + if err != nil { + aghhttp.Error(r, w, http.StatusBadRequest, "Failed to parse bootstrap servers: %s", err) + + return } + defer closeBoots(boots) - wg := &sync.WaitGroup{} - m := &sync.Map{} + cv := newUpstreamConfigValidator(req.Upstreams, req.FallbackDNS, req.PrivateUpstreams, opts) + cv.check() + cv.close() - wg.Add(len(req.Upstreams) + len(req.FallbackDNS) + len(req.PrivateUpstreams)) - - for _, ups := range req.Upstreams { - go s.checkDNS(ups, opts, checkDNSUpstreamExc, wg, m) - } - for _, ups := range req.FallbackDNS { - go s.checkDNS(ups, opts, checkDNSUpstreamExc, wg, m) - } - for _, ups := range req.PrivateUpstreams { - go s.checkDNS(ups, opts, checkPrivateUpstreamExc, wg, m) - } - - wg.Wait() - - result := map[string]string{} - m.Range(func(k, v any) bool { - // TODO(e.burkov): The upstreams used for both common and private - // resolving should be reported separately. - ups := k.(string) - status := v.(string) - - result[ups] = status - - return true - }) - - aghhttp.WriteJSONResponseOK(w, r, result) + aghhttp.WriteJSONResponseOK(w, r, cv.status()) } // handleCacheClear is the handler for the POST /control/cache_clear HTTP API. diff --git a/internal/dnsforward/http_test.go b/internal/dnsforward/http_test.go index 1418a4a1..b7696cdc 100644 --- a/internal/dnsforward/http_test.go +++ b/internal/dnsforward/http_test.go @@ -72,11 +72,14 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) { UDPListenAddrs: []*net.UDPAddr{}, TCPListenAddrs: []*net.TCPAddr{}, Config: Config{ - UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, - FallbackDNS: []string{"9.9.9.10"}, - EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, + UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, + FallbackDNS: []string{"9.9.9.10"}, + RatelimitSubnetLenIPv4: 24, + RatelimitSubnetLenIPv6: 56, + EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, ConfigModified: func() {}, + ServePlainDNS: true, } s := createTestServer(t, filterConf, forwardConf, nil) s.sysResolvers = &emptySysResolvers{} @@ -150,10 +153,13 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) { UDPListenAddrs: []*net.UDPAddr{}, TCPListenAddrs: []*net.TCPAddr{}, Config: Config{ - UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, - EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, + UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, + RatelimitSubnetLenIPv4: 24, + RatelimitSubnetLenIPv6: 56, + EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, ConfigModified: func() {}, + ServePlainDNS: true, } s := createTestServer(t, filterConf, forwardConf, nil) s.sysResolvers = &emptySysResolvers{} @@ -179,11 +185,18 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) { name: "blocking_mode_good", wantSet: "", }, { - name: "blocking_mode_bad", - wantSet: "blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode", + name: "blocking_mode_bad", + wantSet: "validating dns config: " + + "blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode", }, { name: "ratelimit", wantSet: "", + }, { + name: "ratelimit_subnet_len", + wantSet: "", + }, { + name: "ratelimit_whitelist_not_ip", + wantSet: `decoding request: ParseAddr("not.ip"): unexpected character (at "not.ip")`, }, { name: "edns_cs_enabled", wantSet: "", @@ -206,24 +219,26 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) { name: "upstream_mode_fastest_addr", wantSet: "", }, { - name: "upstream_dns_bad", - wantSet: `validating upstream servers: validating upstream "!!!": not an ip:port`, + name: "upstream_dns_bad", + wantSet: `validating dns config: ` + + `upstream servers: validating upstream "!!!": not an ip:port`, }, { name: "bootstraps_bad", - wantSet: `checking bootstrap a: invalid address: bootstrap a:53: ` + + wantSet: `validating dns config: checking bootstrap a: invalid address: not a bootstrap: ` + `ParseAddr("a"): unable to parse IP`, }, { name: "cache_bad_ttl", - wantSet: `cache_ttl_min must be less or equal than cache_ttl_max`, + wantSet: `validating dns config: cache_ttl_min must be less or equal than cache_ttl_max`, }, { name: "upstream_mode_bad", - wantSet: `upstream_mode: incorrect value`, + wantSet: `validating dns config: upstream_mode: incorrect value "somethingelse"`, }, { name: "local_ptr_upstreams_good", wantSet: "", }, { name: "local_ptr_upstreams_bad", - wantSet: `validating private upstream servers: checking domain-specific upstreams: ` + + wantSet: `validating dns config: ` + + `private upstream servers: checking domain-specific upstreams: ` + `bad arpa domain name "non.arpa.": not a reversed ip network`, }, { name: "local_ptr_upstreams_null", @@ -347,7 +362,7 @@ func TestValidateUpstreams(t *testing.T) { set: []string{"123.3.7m"}, }, { name: "invalid", - wantErr: `bad upstream for domain "[/host.com]tls://dns.adguard.com": ` + + wantErr: `splitting upstream line "[/host.com]tls://dns.adguard.com": ` + `missing separator`, set: []string{"[/host.com]tls://dns.adguard.com"}, }, { @@ -373,7 +388,7 @@ func TestValidateUpstreams(t *testing.T) { }, }, { name: "bad_domain", - wantErr: `bad upstream for domain "[/!/]8.8.8.8": domain at index 0: ` + + wantErr: `splitting upstream line "[/!/]8.8.8.8": domain at index 0: ` + `bad domain name "!": bad top-level domain name label "!": ` + `bad top-level domain name label rune '!'`, set: []string{"[/!/]8.8.8.8"}, @@ -461,25 +476,15 @@ func newLocalUpstreamListener(t *testing.T, port uint16, handler dns.Handler) (r } func TestServer_HandleTestUpstreamDNS(t *testing.T) { - goodHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { + hdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { err := w.WriteMsg(new(dns.Msg).SetReply(m)) require.NoError(testutil.PanicT{}, err) }) - badHandler := dns.HandlerFunc(func(w dns.ResponseWriter, _ *dns.Msg) { - err := w.WriteMsg(new(dns.Msg)) - require.NoError(testutil.PanicT{}, err) - }) - goodUps := (&url.URL{ + ups := (&url.URL{ Scheme: "tcp", - Host: newLocalUpstreamListener(t, 0, goodHandler).String(), + Host: newLocalUpstreamListener(t, 0, hdlr).String(), }).String() - badUps := (&url.URL{ - Scheme: "tcp", - Host: newLocalUpstreamListener(t, 0, badHandler).String(), - }).String() - - goodAndBadUps := strings.Join([]string{goodUps, badUps}, " ") const ( upsTimeout = 100 * time.Millisecond @@ -488,7 +493,7 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) { upstreamHost = "custom.localhost" ) - hostsListener := newLocalUpstreamListener(t, 0, goodHandler) + hostsListener := newLocalUpstreamListener(t, 0, hdlr) hostsUps := (&url.URL{ Scheme: "tcp", Host: netutil.JoinHostPort(upstreamHost, hostsListener.Port()), @@ -519,7 +524,9 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, nil) + srv.etcHosts = hc startDeferStop(t, srv) testCases := []struct { @@ -527,43 +534,6 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) { wantResp map[string]any name string }{{ - body: map[string]any{ - "upstream_dns": []string{goodUps}, - }, - wantResp: map[string]any{ - goodUps: "OK", - }, - name: "success", - }, { - body: map[string]any{ - "upstream_dns": []string{badUps}, - }, - wantResp: map[string]any{ - badUps: `couldn't communicate with upstream: exchanging with ` + - badUps + ` over tcp: dns: id mismatch`, - }, - name: "broken", - }, { - body: map[string]any{ - "upstream_dns": []string{goodUps, badUps}, - }, - wantResp: map[string]any{ - goodUps: "OK", - badUps: `couldn't communicate with upstream: exchanging with ` + - badUps + ` over tcp: dns: id mismatch`, - }, - name: "both", - }, { - body: map[string]any{ - "upstream_dns": []string{"[/domain.example/]" + badUps}, - }, - wantResp: map[string]any{ - badUps: `WARNING: couldn't communicate ` + - `with upstream: exchanging with ` + badUps + ` over tcp: ` + - `dns: id mismatch`, - }, - name: "domain_specific_error", - }, { body: map[string]any{ "upstream_dns": []string{hostsUps}, }, @@ -573,63 +543,12 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) { name: "etc_hosts", }, { body: map[string]any{ - "fallback_dns": []string{goodUps}, + "upstream_dns": []string{ups, "#this.is.comment"}, }, wantResp: map[string]any{ - goodUps: "OK", + ups: "OK", }, - name: "fallback_success", - }, { - body: map[string]any{ - "fallback_dns": []string{badUps}, - }, - wantResp: map[string]any{ - badUps: `couldn't communicate with upstream: exchanging with ` + - badUps + ` over tcp: dns: id mismatch`, - }, - name: "fallback_broken", - }, { - body: map[string]any{ - "fallback_dns": []string{goodUps, "#this.is.comment"}, - }, - wantResp: map[string]any{ - goodUps: "OK", - }, - name: "fallback_comment_mix", - }, { - body: map[string]any{ - "upstream_dns": []string{"[/domain.example/]" + goodUps + " " + badUps}, - }, - wantResp: map[string]any{ - goodUps: "OK", - badUps: `WARNING: couldn't communicate ` + - `with upstream: exchanging with ` + badUps + ` over tcp: ` + - `dns: id mismatch`, - }, - name: "multiple_domain_specific_upstreams", - }, { - body: map[string]any{ - "upstream_dns": []string{"[/domain.example/]/]1.2.3.4"}, - }, - wantResp: map[string]any{ - "[/domain.example/]/]1.2.3.4": `wrong upstream format: ` + - `bad upstream for domain "[/domain.example/]/]1.2.3.4": ` + - `duplicated separator`, - }, - name: "bad_specification", - }, { - body: map[string]any{ - "upstream_dns": []string{"[/domain.example/]" + goodAndBadUps}, - "fallback_dns": []string{"[/domain.example/]" + goodAndBadUps}, - "private_upstream": []string{"[/domain.example/]" + goodAndBadUps}, - }, - wantResp: map[string]any{ - goodUps: "OK", - badUps: `WARNING: couldn't communicate ` + - `with upstream: exchanging with ` + badUps + ` over tcp: ` + - `dns: id mismatch`, - }, - name: "all_different", + name: "comment_mix", }} for _, tc := range testCases { diff --git a/internal/dnsforward/msg.go b/internal/dnsforward/msg.go index 8a4ecd80..6685c861 100644 --- a/internal/dnsforward/msg.go +++ b/internal/dnsforward/msg.go @@ -91,7 +91,7 @@ func (s *Server) genForBlockingMode(req *dns.Msg, ips []netip.Addr) (resp *dns.M case filtering.BlockingModeREFUSED: return s.makeResponseREFUSED(req) default: - log.Error("dns: invalid blocking mode %q", mode) + log.Error("dnsforward: invalid blocking mode %q", mode) return s.makeResponse(req) } @@ -112,7 +112,7 @@ func (s *Server) makeResponseCustomIP( default: // Generally shouldn't happen, since the types are checked in // genDNSFilterMessage. - log.Error("dns: invalid msg type %s for custom IP blocking mode", dns.Type(qt)) + log.Error("dnsforward: invalid msg type %s for custom IP blocking mode", dns.Type(qt)) return s.makeResponse(req) } @@ -207,15 +207,7 @@ func (s *Server) genResponseWithIPs(req *dns.Msg, ips []netip.Addr) (resp *dns.M var ans []dns.RR switch req.Question[0].Qtype { case dns.TypeA: - for _, ip := range ips { - if ip.Is4() { - ans = append(ans, s.genAnswerA(req, ip)) - } else { - ans = nil - - break - } - } + ans = s.genAnswersWithIPv4s(req, ips) case dns.TypeAAAA: for _, ip := range ips { if ip.Is6() { @@ -232,6 +224,23 @@ func (s *Server) genResponseWithIPs(req *dns.Msg, ips []netip.Addr) (resp *dns.M return resp } +// genAnswersWithIPv4s generates DNS A answers provided IPv4 addresses. If any +// of the IPs isn't an IPv4 address, genAnswersWithIPv4s logs a warning and +// returns nil, +func (s *Server) genAnswersWithIPv4s(req *dns.Msg, ips []netip.Addr) (ans []dns.RR) { + for _, ip := range ips { + if !ip.Is4() { + log.Info("dnsforward: warning: ip %s is not ipv4 address", ip) + + return nil + } + + ans = append(ans, s.genAnswerA(req, ip)) + } + + return ans +} + // makeResponseNullIP creates a response with 0.0.0.0 for A requests, :: for // AAAA requests, and an empty response for other types. func (s *Server) makeResponseNullIP(req *dns.Msg) (resp *dns.Msg) { @@ -253,7 +262,7 @@ func (s *Server) makeResponseNullIP(req *dns.Msg) (resp *dns.Msg) { func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg { if newAddr == "" { - log.Printf("block host is not specified.") + log.Info("dnsforward: block host is not specified") return s.genServerFailure(request) } @@ -276,14 +285,14 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo prx := s.proxy() if prx == nil { - log.Debug("dns: %s", srvClosedErr) + log.Debug("dnsforward: %s", srvClosedErr) return s.genServerFailure(request) } err = prx.Resolve(newContext) if err != nil { - log.Printf("couldn't look up replacement host %q: %s", newAddr, err) + log.Info("dnsforward: looking up replacement host %q: %s", newAddr, err) return s.genServerFailure(request) } diff --git a/internal/dnsforward/process.go b/internal/dnsforward/process.go index 0b572d8b..c9aea322 100644 --- a/internal/dnsforward/process.go +++ b/internal/dnsforward/process.go @@ -191,7 +191,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) { defer log.Debug("dnsforward: finished processing initial") pctx := dctx.proxyCtx - s.processClientIP(pctx.Addr) + s.processClientIP(pctx.Addr.Addr()) q := pctx.Req.Question[0] qt := q.Qtype @@ -228,9 +228,8 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) { } // processClientIP sends the client IP address to s.addrProc, if needed. -func (s *Server) processClientIP(addr net.Addr) { - clientIP := netutil.NetAddrToAddrPort(addr).Addr() - if clientIP == (netip.Addr{}) { +func (s *Server) processClientIP(addr netip.Addr) { + if !addr.IsValid() { log.Info("dnsforward: warning: bad client addr %q", addr) return @@ -241,7 +240,7 @@ func (s *Server) processClientIP(addr net.Addr) { s.serverLock.RLock() defer s.serverLock.RUnlock() - s.addrProc.Process(clientIP) + s.addrProc.Process(addr) } // processDDRQuery responds to Discovery of Designated Resolvers (DDR) SVCB @@ -351,12 +350,7 @@ func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) { rc = resultCodeSuccess - var ip net.IP - if ip, _ = netutil.IPAndPortFromAddr(dctx.proxyCtx.Addr); ip == nil { - return rc - } - - dctx.isLocalClient = s.privateNets.Contains(ip) + dctx.isLocalClient = s.privateNets.Contains(dctx.proxyCtx.Addr.Addr().AsSlice()) return rc } @@ -831,14 +825,13 @@ func (s *Server) dhcpHostFromRequest(q *dns.Question) (reqHost string) { // setCustomUpstream sets custom upstream settings in pctx, if necessary. func (s *Server) setCustomUpstream(pctx *proxy.DNSContext, clientID string) { - customUpsByClient := s.conf.GetCustomUpstreamByClient - if pctx.Addr == nil || customUpsByClient == nil { + if !pctx.Addr.IsValid() || s.conf.ClientsContainer == nil { return } // Use the ClientID first, since it has a higher priority. - id := stringutil.Coalesce(clientID, ipStringFromAddr(pctx.Addr)) - upsConf, err := customUpsByClient(id) + id := stringutil.Coalesce(clientID, pctx.Addr.Addr().String()) + upsConf, err := s.conf.ClientsContainer.UpstreamConfigByID(id, s.bootstrap) if err != nil { log.Error("dnsforward: getting custom upstreams for client %s: %s", id, err) @@ -847,9 +840,9 @@ func (s *Server) setCustomUpstream(pctx *proxy.DNSContext, clientID string) { if upsConf != nil { log.Debug("dnsforward: using custom upstreams for client %s", id) - } - pctx.CustomUpstreamConfig = upsConf + pctx.CustomUpstreamConfig = upsConf + } } // Apply filtering logic after we have received response from upstream servers diff --git a/internal/dnsforward/process_internal_test.go b/internal/dnsforward/process_internal_test.go index 168a97a1..18b04b3f 100644 --- a/internal/dnsforward/process_internal_test.go +++ b/internal/dnsforward/process_internal_test.go @@ -81,6 +81,7 @@ func TestServer_ProcessInitial(t *testing.T) { AAAADisabled: tc.aaaaDisabled, EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ @@ -96,14 +97,14 @@ func TestServer_ProcessInitial(t *testing.T) { dctx := &dnsContext{ proxyCtx: &proxy.DNSContext{ Req: createTestMessageWithType(tc.target, tc.qType), - Addr: testClientAddr, + Addr: testClientAddrPort, RequestID: 1234, }, } gotRC := s.processInitial(dctx) assert.Equal(t, tc.wantRC, gotRC) - assert.Equal(t, netutil.NetAddrToAddrPort(testClientAddr).Addr(), gotAddr) + assert.Equal(t, testClientAddrPort.Addr(), gotAddr) if tc.wantRCode > 0 { gotResp := dctx.proxyCtx.Res @@ -180,6 +181,7 @@ func TestServer_ProcessFilteringAfterResponse(t *testing.T) { AAAADisabled: tc.aaaaDisabled, EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, } s := createTestServer(t, &filtering.Config{ @@ -199,7 +201,7 @@ func TestServer_ProcessFilteringAfterResponse(t *testing.T) { Proto: proxy.ProtoUDP, Req: tc.req, Res: resp, - Addr: testClientAddr, + Addr: testClientAddrPort, }, } @@ -369,6 +371,7 @@ func prepareTestServer(t *testing.T, portDoH, portDoT, portDoQ int, ddrEnabled b TLSConfig: TLSConfig{ ServerName: ddrTestDomainName, }, + ServePlainDNS: true, }, } @@ -394,33 +397,27 @@ func TestServer_ProcessDetermineLocal(t *testing.T) { } testCases := []struct { - want assert.BoolAssertionFunc - name string - cliIP net.IP + want assert.BoolAssertionFunc + name string + cliAddr netip.AddrPort }{{ - want: assert.True, - name: "local", - cliIP: net.IP{192, 168, 0, 1}, + want: assert.True, + name: "local", + cliAddr: netip.MustParseAddrPort("192.168.0.1:1"), }, { - want: assert.False, - name: "external", - cliIP: net.IP{250, 249, 0, 1}, + want: assert.False, + name: "external", + cliAddr: netip.MustParseAddrPort("250.249.0.1:1"), }, { - want: assert.False, - name: "invalid", - cliIP: net.IP{1, 2, 3, 4, 5}, - }, { - want: assert.False, - name: "nil", - cliIP: nil, + want: assert.False, + name: "invalid", + cliAddr: netip.AddrPort{}, }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { proxyCtx := &proxy.DNSContext{ - Addr: &net.TCPAddr{ - IP: tc.cliIP, - }, + Addr: tc.cliAddr, } dctx := &dnsContext{ proxyCtx: proxyCtx, @@ -699,6 +696,7 @@ func TestServer_ProcessRestrictLocal(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, ups) s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups} startDeferStop(t, s) @@ -707,31 +705,31 @@ func TestServer_ProcessRestrictLocal(t *testing.T) { name string want string question net.IP - cliIP net.IP + cliAddr netip.AddrPort wantLen int }{{ name: "from_local_to_external", want: "host1.example.net.", question: net.IP{254, 253, 252, 251}, - cliIP: net.IP{192, 168, 10, 10}, + cliAddr: netip.MustParseAddrPort("192.168.10.10:1"), wantLen: 1, }, { name: "from_external_for_local", want: "", question: net.IP{192, 168, 1, 1}, - cliIP: net.IP{254, 253, 252, 251}, + cliAddr: netip.MustParseAddrPort("254.253.252.251:1"), wantLen: 0, }, { name: "from_local_for_local", want: "some.local-client.", question: net.IP{192, 168, 1, 1}, - cliIP: net.IP{192, 168, 1, 2}, + cliAddr: netip.MustParseAddrPort("192.168.1.2:1"), wantLen: 1, }, { name: "from_external_for_external", want: "host1.example.net.", question: net.IP{254, 253, 252, 251}, - cliIP: net.IP{254, 253, 252, 255}, + cliAddr: netip.MustParseAddrPort("254.253.252.255:1"), wantLen: 1, }} @@ -743,9 +741,7 @@ func TestServer_ProcessRestrictLocal(t *testing.T) { pctx := &proxy.DNSContext{ Proto: proxy.ProtoTCP, Req: req, - Addr: &net.TCPAddr{ - IP: tc.cliIP, - }, + Addr: tc.cliAddr, } t.Run(tc.name, func(t *testing.T) { @@ -776,6 +772,7 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) { return aghalg.Coalesce( @@ -789,7 +786,7 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) { var dnsCtx *dnsContext setup := func(use bool) { proxyCtx = &proxy.DNSContext{ - Addr: testClientAddr, + Addr: testClientAddrPort, Req: createTestMessageWithType(reqAddr, dns.TypePTR), } dnsCtx = &dnsContext{ diff --git a/internal/dnsforward/stats.go b/internal/dnsforward/stats.go index 52f83af5..f67e5ad8 100644 --- a/internal/dnsforward/stats.go +++ b/internal/dnsforward/stats.go @@ -10,9 +10,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/golibs/log" - "github.com/AdguardTeam/golibs/netutil" "github.com/miekg/dns" - "golang.org/x/exp/slices" ) // Write Stats data and logs @@ -25,13 +23,12 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) { host := aghnet.NormalizeDomain(q.Name) processingTime := time.Since(dctx.startTime) - ip, _ := netutil.IPAndPortFromAddr(pctx.Addr) - ip = slices.Clone(ip) + ip := pctx.Addr.Addr().AsSlice() s.anonymizer.Load()(ip) log.Debug("dnsforward: client ip for stats and querylog: %s", ip) - ipStr := ip.String() + ipStr := pctx.Addr.Addr().String() ids := []string{ipStr, dctx.clientID} qt, cl := q.Qtype, q.Qclass diff --git a/internal/dnsforward/stats_test.go b/internal/dnsforward/stats_test.go index 0b8cae2e..668b885b 100644 --- a/internal/dnsforward/stats_test.go +++ b/internal/dnsforward/stats_test.go @@ -1,7 +1,7 @@ package dnsforward import ( - "net" + "net/netip" "testing" "time" @@ -65,7 +65,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name string domain string proto proxy.Proto - addr net.Addr + addr netip.AddrPort clientID string wantLogProto querylog.ClientProto wantStatClient string @@ -76,7 +76,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_udp", domain: domain, proto: proxy.ProtoUDP, - addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: "", wantStatClient: "1.2.3.4", @@ -87,7 +87,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_tls_clientid", domain: domain, proto: proxy.ProtoTLS, - addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "cli42", wantLogProto: querylog.ClientProtoDoT, wantStatClient: "cli42", @@ -98,7 +98,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_tls", domain: domain, proto: proxy.ProtoTLS, - addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: querylog.ClientProtoDoT, wantStatClient: "1.2.3.4", @@ -109,7 +109,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_quic", domain: domain, proto: proxy.ProtoQUIC, - addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: querylog.ClientProtoDoQ, wantStatClient: "1.2.3.4", @@ -120,7 +120,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_https", domain: domain, proto: proxy.ProtoHTTPS, - addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: querylog.ClientProtoDoH, wantStatClient: "1.2.3.4", @@ -131,7 +131,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_dnscrypt", domain: domain, proto: proxy.ProtoDNSCrypt, - addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: querylog.ClientProtoDNSCrypt, wantStatClient: "1.2.3.4", @@ -142,7 +142,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_udp_filtered", domain: domain, proto: proxy.ProtoUDP, - addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: "", wantStatClient: "1.2.3.4", @@ -153,7 +153,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_udp_sb", domain: domain, proto: proxy.ProtoUDP, - addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: "", wantStatClient: "1.2.3.4", @@ -164,7 +164,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_udp_ss", domain: domain, proto: proxy.ProtoUDP, - addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: "", wantStatClient: "1.2.3.4", @@ -175,7 +175,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_udp_pc", domain: domain, proto: proxy.ProtoUDP, - addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + addr: testClientAddrPort, clientID: "", wantLogProto: "", wantStatClient: "1.2.3.4", @@ -186,10 +186,10 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) { name: "success_udp_pc_empty_fqdn", domain: ".", proto: proxy.ProtoUDP, - addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 5}, Port: 1234}, + addr: netip.MustParseAddrPort("4.3.2.1:1234"), clientID: "", wantLogProto: "", - wantStatClient: "1.2.3.5", + wantStatClient: "4.3.2.1", wantCode: resultCodeSuccess, reason: filtering.FilteredParental, wantStatResult: stats.RParental, diff --git a/internal/dnsforward/svcbmsg_test.go b/internal/dnsforward/svcbmsg_test.go index 83645b32..58275ef4 100644 --- a/internal/dnsforward/svcbmsg_test.go +++ b/internal/dnsforward/svcbmsg_test.go @@ -19,6 +19,7 @@ func TestGenAnswerHTTPS_andSVCB(t *testing.T) { Config: Config{ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false}, }, + ServePlainDNS: true, }, nil) req := &dns.Msg{ diff --git a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json index f618de72..32265d2d 100644 --- a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json +++ b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleGetConfig.json @@ -17,6 +17,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -53,6 +56,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -89,6 +95,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", diff --git a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json index b94bb82f..2ef81f35 100644 --- a/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json +++ b/internal/dnsforward/testdata/TestDNSForwardHTTP_handleSetConfig.json @@ -22,6 +22,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -60,6 +63,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -99,6 +105,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "refused", "blocking_ipv4": "", "blocking_ipv6": "", @@ -138,6 +147,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -177,6 +189,98 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 6, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], + "blocking_mode": "default", + "blocking_ipv4": "", + "blocking_ipv6": "", + "blocked_response_ttl": 10, + "edns_cs_enabled": false, + "dnssec_enabled": false, + "disable_ipv6": false, + "upstream_mode": "", + "cache_size": 0, + "cache_ttl_min": 0, + "cache_ttl_max": 0, + "cache_optimistic": false, + "resolve_clients": false, + "use_private_ptr_resolvers": false, + "local_ptr_upstreams": [], + "edns_cs_use_custom": false, + "edns_cs_custom_ip": "" + } + }, + "ratelimit_subnet_len": { + "req": { + "ratelimit": 12, + "ratelimit_subnet_len_ipv4": 32, + "ratelimit_subnet_len_ipv6": 128 + }, + "want": { + "upstream_dns": [ + "8.8.8.8:53", + "8.8.4.4:53" + ], + "upstream_dns_file": "", + "bootstrap_dns": [ + "9.9.9.10", + "149.112.112.10", + "2620:fe::10", + "2620:fe::fe:10" + ], + "fallback_dns": [], + "protection_enabled": true, + "protection_disabled_until": null, + "ratelimit": 12, + "ratelimit_subnet_len_ipv4": 32, + "ratelimit_subnet_len_ipv6": 128, + "ratelimit_whitelist": [], + "blocking_mode": "default", + "blocking_ipv4": "", + "blocking_ipv6": "", + "blocked_response_ttl": 10, + "edns_cs_enabled": false, + "dnssec_enabled": false, + "disable_ipv6": false, + "upstream_mode": "", + "cache_size": 0, + "cache_ttl_min": 0, + "cache_ttl_max": 0, + "cache_optimistic": false, + "resolve_clients": false, + "use_private_ptr_resolvers": false, + "local_ptr_upstreams": [], + "edns_cs_use_custom": false, + "edns_cs_custom_ip": "" + } + }, + "ratelimit_whitelist_not_ip": { + "req": { + "ratelimit_whitelist": [ + "1.2.3.4", + "not.ip" + ] + }, + "want": { + "upstream_dns": [ + "8.8.8.8:53", + "8.8.4.4:53" + ], + "upstream_dns_file": "", + "bootstrap_dns": [ + "9.9.9.10", + "149.112.112.10", + "2620:fe::10", + "2620:fe::fe:10" + ], + "fallback_dns": [], + "protection_enabled": true, + "protection_disabled_until": null, + "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -216,6 +320,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -257,6 +364,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -298,6 +408,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -337,6 +450,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -376,6 +492,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -415,6 +534,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -454,6 +576,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -495,6 +620,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -536,6 +664,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -576,6 +707,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -615,6 +749,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -656,6 +793,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -700,6 +840,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -739,6 +882,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -782,6 +928,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -821,6 +970,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", @@ -863,6 +1015,9 @@ "protection_enabled": true, "protection_disabled_until": null, "ratelimit": 0, + "ratelimit_subnet_len_ipv4": 24, + "ratelimit_subnet_len_ipv6": 56, + "ratelimit_whitelist": [], "blocking_mode": "default", "blocking_ipv4": "", "blocking_ipv6": "", diff --git a/internal/dnsforward/upstreams.go b/internal/dnsforward/upstreams.go index f30dce69..3f877ac7 100644 --- a/internal/dnsforward/upstreams.go +++ b/internal/dnsforward/upstreams.go @@ -1,16 +1,17 @@ package dnsforward import ( - "bytes" "fmt" "net" - "net/url" + "net/netip" "os" "strings" "time" + "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/stringutil" @@ -18,6 +19,28 @@ import ( "golang.org/x/exp/slices" ) +const ( + // errNotDomainSpecific is returned when the upstream should be + // domain-specific, but isn't. + errNotDomainSpecific errors.Error = "not a domain-specific upstream" + + // errMissingSeparator is returned when the domain-specific part of the + // upstream configuration line isn't closed. + errMissingSeparator errors.Error = "missing separator" + + // errDupSeparator is returned when the domain-specific part of the upstream + // configuration line contains more than one ending separator. + errDupSeparator errors.Error = "duplicated separator" + + // errNoDefaultUpstreams is returned when there are no default upstreams + // specified in the upstream configuration. + errNoDefaultUpstreams errors.Error = "no default upstreams specified" + + // errWrongResponse is returned when the checked upstream replies in an + // unexpected way. + errWrongResponse errors.Error = "wrong response" +) + // loadUpstreams parses upstream DNS servers from the configured file or from // the configuration itself. func (s *Server) loadUpstreams() (upstreams []string, err error) { @@ -39,7 +62,7 @@ func (s *Server) loadUpstreams() (upstreams []string, err error) { } // prepareUpstreamSettings sets upstream DNS server settings. -func (s *Server) prepareUpstreamSettings() (err error) { +func (s *Server) prepareUpstreamSettings(boot upstream.Resolver) (err error) { // Load upstreams either from the file, or from the settings var upstreams []string upstreams, err = s.loadUpstreams() @@ -48,7 +71,7 @@ func (s *Server) prepareUpstreamSettings() (err error) { } s.conf.UpstreamConfig, err = s.prepareUpstreamConfig(upstreams, defaultDNS, &upstream.Options{ - Bootstrap: s.conf.BootstrapDNS, + Bootstrap: boot, Timeout: s.conf.UpstreamTimeout, HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams), PreferIPv6: s.conf.BootstrapPreferIPv6, @@ -92,178 +115,9 @@ func (s *Server) prepareUpstreamConfig( uc.Upstreams = defaultUpstreamConfig.Upstreams } - // dnsFilter can be nil during application update. - if s.dnsFilter != nil { - err = s.replaceUpstreamsWithHosts(uc, opts) - if err != nil { - return nil, fmt.Errorf("resolving upstreams with hosts: %w", err) - } - } - return uc, nil } -// replaceUpstreamsWithHosts replaces unique upstreams with their resolved -// versions based on the system hosts file. -// -// TODO(e.burkov): This should be performed inside dnsproxy, which should -// actually consider /etc/hosts. See TODO on [aghnet.HostsContainer]. -func (s *Server) replaceUpstreamsWithHosts( - upsConf *proxy.UpstreamConfig, - opts *upstream.Options, -) (err error) { - resolved := map[string]*upstream.Options{} - - err = s.resolveUpstreamsWithHosts(resolved, upsConf.Upstreams, opts) - if err != nil { - return fmt.Errorf("resolving upstreams: %w", err) - } - - hosts := maps.Keys(upsConf.DomainReservedUpstreams) - // TODO(e.burkov): Think of extracting sorted range into an util function. - slices.Sort(hosts) - for _, host := range hosts { - err = s.resolveUpstreamsWithHosts(resolved, upsConf.DomainReservedUpstreams[host], opts) - if err != nil { - return fmt.Errorf("resolving upstreams reserved for %s: %w", host, err) - } - } - - hosts = maps.Keys(upsConf.SpecifiedDomainUpstreams) - slices.Sort(hosts) - for _, host := range hosts { - err = s.resolveUpstreamsWithHosts(resolved, upsConf.SpecifiedDomainUpstreams[host], opts) - if err != nil { - return fmt.Errorf("resolving upstreams specific for %s: %w", host, err) - } - } - - return nil -} - -// resolveUpstreamsWithHosts resolves the IP addresses of each of the upstreams -// and replaces those both in upstreams and resolved. Upstreams that failed to -// resolve are placed to resolved as-is. This function only returns error of -// upstreams closing. -func (s *Server) resolveUpstreamsWithHosts( - resolved map[string]*upstream.Options, - upstreams []upstream.Upstream, - opts *upstream.Options, -) (err error) { - for i := range upstreams { - u := upstreams[i] - addr := u.Address() - host := extractUpstreamHost(addr) - - withIPs, ok := resolved[host] - if !ok { - recs := s.dnsFilter.EtcHostsRecords(host) - if len(recs) == 0 { - resolved[host] = nil - - return nil - } - - withIPs = opts.Clone() - withIPs.ServerIPAddrs = make([]net.IP, 0, len(recs)) - for _, rec := range recs { - withIPs.ServerIPAddrs = append(withIPs.ServerIPAddrs, rec.Addr.AsSlice()) - } - - sortNetIPAddrs(withIPs.ServerIPAddrs, opts.PreferIPv6) - resolved[host] = withIPs - } else if withIPs == nil { - continue - } - - if err = u.Close(); err != nil { - return fmt.Errorf("closing upstream %s: %w", addr, err) - } - - upstreams[i], err = upstream.AddressToUpstream(addr, withIPs) - if err != nil { - return fmt.Errorf("replacing upstream %s with resolved %s: %w", addr, host, err) - } - - log.Debug("dnsforward: using %s for %s", withIPs.ServerIPAddrs, upstreams[i].Address()) - } - - return nil -} - -// extractUpstreamHost returns the hostname of addr without port with an -// assumption that any address passed here has already been successfully parsed -// by [upstream.AddressToUpstream]. This function essentially mirrors the logic -// of [upstream.AddressToUpstream], see TODO on [replaceUpstreamsWithHosts]. -func extractUpstreamHost(addr string) (host string) { - var err error - if strings.Contains(addr, "://") { - var u *url.URL - u, err = url.Parse(addr) - if err != nil { - log.Debug("dnsforward: parsing upstream %s: %s", addr, err) - - return addr - } - - return u.Hostname() - } - - // Probably, plain UDP upstream defined by address or address:port. - host, err = netutil.SplitHost(addr) - if err != nil { - return addr - } - - return host -} - -// sortNetIPAddrs sorts addrs in accordance with the protocol preferences. -// Invalid addresses are sorted near the end. -// -// TODO(e.burkov): This function taken from dnsproxy, which also already -// contains a few similar functions. Think of moving to golibs. -func sortNetIPAddrs(addrs []net.IP, preferIPv6 bool) { - l := len(addrs) - if l <= 1 { - return - } - - slices.SortStableFunc(addrs, func(addrA, addrB net.IP) (res int) { - switch len(addrA) { - case net.IPv4len, net.IPv6len: - switch len(addrB) { - case net.IPv4len, net.IPv6len: - // Go on. - default: - return -1 - } - default: - return 1 - } - - // Treat IPv6-mapped IPv4 addresses as IPv6 addresses. - aIs4, bIs4 := addrA.To4() != nil, addrB.To4() != nil - if aIs4 == bIs4 { - return bytes.Compare(addrA, addrB) - } - - if aIs4 { - if preferIPv6 { - return 1 - } - - return -1 - } - - if preferIPv6 { - return -1 - } - - return 1 - }) -} - // UpstreamHTTPVersions returns the HTTP versions for upstream configuration // depending on configuration. func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) { @@ -295,3 +149,221 @@ func setProxyUpstreamMode( conf.UpstreamMode = proxy.UModeLoadBalance } } + +// createBootstrap returns a bootstrap resolver based on the configuration of s. +// boots are the upstream resolvers that should be closed after use. r is the +// actual bootstrap resolver, which may include the system hosts. +// +// TODO(e.burkov): This function currently returns a resolver and a slice of +// the upstream resolvers, which are essentially the same. boots are returned +// for being able to close them afterwards, but it introduces an implicit +// contract that r could only be used before that. Anyway, this code should +// improve when the [proxy.UpstreamConfig] will become an [upstream.Resolver] +// and be used here. +func (s *Server) createBootstrap( + addrs []string, + opts *upstream.Options, +) (r upstream.Resolver, boots []*upstream.UpstreamResolver, err error) { + if len(addrs) == 0 { + addrs = defaultBootstrap + } + + boots, err = aghnet.ParseBootstraps(addrs, opts) + if err != nil { + // Don't wrap the error, since it's informative enough as is. + return nil, nil, err + } + + var parallel upstream.ParallelResolver + for _, b := range boots { + parallel = append(parallel, b) + } + + if s.etcHosts != nil { + r = upstream.ConsequentResolver{s.etcHosts, parallel} + } else { + r = parallel + } + + return r, boots, nil +} + +// IsCommentOrEmpty returns true if s starts with a "#" character or is empty. +// This function is useful for filtering out non-upstream lines from upstream +// configs. +func IsCommentOrEmpty(s string) (ok bool) { + return len(s) == 0 || s[0] == '#' +} + +// newUpstreamConfig validates upstreams and returns an appropriate upstream +// configuration or nil if it can't be built. +// +// TODO(e.burkov): Perhaps proxy.ParseUpstreamsConfig should validate upstreams +// slice already so that this function may be considered useless. +func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err error) { + // No need to validate comments and empty lines. + upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty) + if len(upstreams) == 0 { + // Consider this case valid since it means the default server should be + // used. + return nil, nil + } + + err = validateUpstreamConfig(upstreams) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } + + conf, err = proxy.ParseUpstreamsConfig( + upstreams, + &upstream.Options{ + Bootstrap: net.DefaultResolver, + Timeout: DefaultTimeout, + }, + ) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } else if len(conf.Upstreams) == 0 { + return nil, errNoDefaultUpstreams + } + + return conf, nil +} + +// validateUpstreamConfig validates each upstream from the upstream +// configuration and returns an error if any upstream is invalid. +// +// TODO(e.burkov): Merge with [upstreamConfigValidator] somehow. +func validateUpstreamConfig(conf []string) (err error) { + for _, u := range conf { + var ups []string + var isSpecific bool + ups, isSpecific, err = splitUpstreamLine(u) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return err + } + + for _, addr := range ups { + _, err = validateUpstream(addr, isSpecific) + if err != nil { + return fmt.Errorf("validating upstream %q: %w", addr, err) + } + } + } + + return nil +} + +// ValidateUpstreams validates each upstream and returns an error if any +// upstream is invalid or if there are no default upstreams specified. +// +// TODO(e.burkov): Merge with [upstreamConfigValidator] somehow. +func ValidateUpstreams(upstreams []string) (err error) { + _, err = newUpstreamConfig(upstreams) + + return err +} + +// ValidateUpstreamsPrivate validates each upstream and returns an error if any +// upstream is invalid or if there are no default upstreams specified. It also +// checks each domain of domain-specific upstreams for being ARPA pointing to +// a locally-served network. privateNets must not be nil. +func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet) (err error) { + conf, err := newUpstreamConfig(upstreams) + if err != nil { + return fmt.Errorf("creating config: %w", err) + } + + if conf == nil { + return nil + } + + keys := maps.Keys(conf.DomainReservedUpstreams) + slices.Sort(keys) + + var errs []error + for _, domain := range keys { + var subnet netip.Prefix + subnet, err = extractARPASubnet(domain) + if err != nil { + errs = append(errs, err) + + continue + } + + if !privateNets.Contains(subnet.Addr().AsSlice()) { + errs = append( + errs, + fmt.Errorf("arpa domain %q should point to a locally-served network", domain), + ) + } + } + + return errors.Annotate(errors.Join(errs...), "checking domain-specific upstreams: %w") +} + +// protocols are the supported URL schemes for upstreams. +var protocols = []string{"h3", "https", "quic", "sdns", "tcp", "tls", "udp"} + +// validateUpstream returns an error if u alongside with domains is not a valid +// upstream configuration. useDefault is true if the upstream is +// domain-specific and is configured to point at the default upstream server +// which is validated separately. The upstream is considered domain-specific +// only if domains is at least not nil. +func validateUpstream(u string, isSpecific bool) (useDefault bool, err error) { + // The special server address '#' means that default server must be used. + if useDefault = u == "#" && isSpecific; useDefault { + return useDefault, nil + } + + // Check if the upstream has a valid protocol prefix. + // + // TODO(e.burkov): Validate the domain name. + if proto, _, ok := strings.Cut(u, "://"); ok { + if !slices.Contains(protocols, proto) { + return false, fmt.Errorf("bad protocol %q", proto) + } + } else if _, err = netip.ParseAddr(u); err == nil { + return false, nil + } else if _, err = netip.ParseAddrPort(u); err == nil { + return false, nil + } + + return false, err +} + +// splitUpstreamLine returns the upstreams and the specified domains. domains +// is nil when the upstream is not domains-specific. Otherwise it may also be +// empty. +func splitUpstreamLine(upstreamStr string) (upstreams []string, isSpecific bool, err error) { + if !strings.HasPrefix(upstreamStr, "[/") { + return []string{upstreamStr}, false, nil + } + + defer func() { err = errors.Annotate(err, "splitting upstream line %q: %w", upstreamStr) }() + + doms, ups, found := strings.Cut(upstreamStr[2:], "/]") + if !found { + return nil, false, errMissingSeparator + } else if strings.Contains(ups, "/]") { + return nil, false, errDupSeparator + } + + for i, host := range strings.Split(doms, "/") { + if host == "" { + continue + } + + err = netutil.ValidateDomainName(strings.TrimPrefix(host, "*.")) + if err != nil { + return nil, false, fmt.Errorf("domain at index %d: %w", i, err) + } + + isSpecific = true + } + + return strings.Fields(ups), isSpecific, nil +} diff --git a/internal/dnsforward/upstreams_internal_test.go b/internal/dnsforward/upstreams_internal_test.go new file mode 100644 index 00000000..9bf2f5cf --- /dev/null +++ b/internal/dnsforward/upstreams_internal_test.go @@ -0,0 +1,223 @@ +package dnsforward + +import ( + "net" + "net/url" + "strings" + "testing" + "time" + + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/AdguardTeam/golibs/testutil" + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpstreamConfigValidator(t *testing.T) { + goodHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { + err := w.WriteMsg(new(dns.Msg).SetReply(m)) + require.NoError(testutil.PanicT{}, err) + }) + badHandler := dns.HandlerFunc(func(w dns.ResponseWriter, _ *dns.Msg) { + err := w.WriteMsg(new(dns.Msg)) + require.NoError(testutil.PanicT{}, err) + }) + + goodUps := (&url.URL{ + Scheme: "tcp", + Host: newLocalUpstreamListener(t, 0, goodHandler).String(), + }).String() + badUps := (&url.URL{ + Scheme: "tcp", + Host: newLocalUpstreamListener(t, 0, badHandler).String(), + }).String() + + goodAndBadUps := strings.Join([]string{goodUps, badUps}, " ") + + // upsTimeout restricts the checking process to prevent the test from + // hanging. + const upsTimeout = 100 * time.Millisecond + + testCases := []struct { + want map[string]string + name string + general []string + fallback []string + private []string + }{{ + name: "success", + general: []string{goodUps}, + want: map[string]string{ + goodUps: "OK", + }, + }, { + name: "broken", + general: []string{badUps}, + want: map[string]string{ + badUps: `couldn't communicate with upstream: exchanging with ` + + badUps + ` over tcp: dns: id mismatch`, + }, + }, { + name: "both", + general: []string{goodUps, badUps, goodUps}, + want: map[string]string{ + goodUps: "OK", + badUps: `couldn't communicate with upstream: exchanging with ` + + badUps + ` over tcp: dns: id mismatch`, + }, + }, { + name: "domain_specific_error", + general: []string{"[/domain.example/]" + badUps}, + want: map[string]string{ + badUps: `WARNING: couldn't communicate ` + + `with upstream: exchanging with ` + badUps + ` over tcp: ` + + `dns: id mismatch`, + }, + }, { + name: "fallback_success", + fallback: []string{goodUps}, + want: map[string]string{ + goodUps: "OK", + }, + }, { + name: "fallback_broken", + fallback: []string{badUps}, + want: map[string]string{ + badUps: `couldn't communicate with upstream: exchanging with ` + + badUps + ` over tcp: dns: id mismatch`, + }, + }, { + name: "multiple_domain_specific_upstreams", + general: []string{"[/domain.example/]" + goodAndBadUps}, + want: map[string]string{ + goodUps: "OK", + badUps: `WARNING: couldn't communicate ` + + `with upstream: exchanging with ` + badUps + ` over tcp: ` + + `dns: id mismatch`, + }, + }, { + name: "bad_specification", + general: []string{"[/domain.example/]/]1.2.3.4"}, + want: map[string]string{ + "[/domain.example/]/]1.2.3.4": `splitting upstream line ` + + `"[/domain.example/]/]1.2.3.4": duplicated separator`, + }, + }, { + name: "all_different", + general: []string{"[/domain.example/]" + goodAndBadUps}, + fallback: []string{"[/domain.example/]" + goodAndBadUps}, + private: []string{"[/domain.example/]" + goodAndBadUps}, + want: map[string]string{ + goodUps: "OK", + badUps: `WARNING: couldn't communicate ` + + `with upstream: exchanging with ` + badUps + ` over tcp: ` + + `dns: id mismatch`, + }, + }, { + name: "bad_specific_domains", + general: []string{"[/example/]/]" + goodUps}, + fallback: []string{"[/example/" + goodUps}, + private: []string{"[/example//bad.123/]" + goodUps}, + want: map[string]string{ + `[/example/]/]` + goodUps: `splitting upstream line ` + + `"[/example/]/]` + goodUps + `": duplicated separator`, + `[/example/` + goodUps: `splitting upstream line ` + + `"[/example/` + goodUps + `": missing separator`, + `[/example//bad.123/]` + goodUps: `splitting upstream line ` + + `"[/example//bad.123/]` + goodUps + `": domain at index 2: ` + + `bad domain name "bad.123": ` + + `bad top-level domain name label "123": all octets are numeric`, + }, + }, { + name: "non-specific_default", + general: []string{ + "#", + "[/example/]#", + }, + want: map[string]string{ + "#": "not a domain-specific upstream", + }, + }, { + name: "bad_proto", + general: []string{ + "bad://1.2.3.4", + }, + want: map[string]string{ + "bad://1.2.3.4": `bad protocol "bad"`, + }, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cv := newUpstreamConfigValidator(tc.general, tc.fallback, tc.private, &upstream.Options{ + Timeout: upsTimeout, + Bootstrap: net.DefaultResolver, + }) + cv.check() + cv.close() + + assert.Equal(t, tc.want, cv.status()) + }) + } +} + +func TestUpstreamConfigValidator_Check_once(t *testing.T) { + type signal = struct{} + + reqCh := make(chan signal) + hdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { + pt := testutil.PanicT{} + + err := w.WriteMsg(new(dns.Msg).SetReply(m)) + require.NoError(pt, err) + + testutil.RequireSend(pt, reqCh, signal{}, testTimeout) + }) + + addr := (&url.URL{ + Scheme: "tcp", + Host: newLocalUpstreamListener(t, 0, hdlr).String(), + }).String() + twoAddrs := strings.Join([]string{addr, addr}, " ") + + wantStatus := map[string]string{ + addr: "OK", + } + + testCases := []struct { + name string + ups []string + }{{ + name: "common", + ups: []string{addr, addr, addr}, + }, { + name: "domain-specific", + ups: []string{"[/one.example/]" + addr, "[/two.example/]" + twoAddrs}, + }, { + name: "both", + ups: []string{addr, "[/one.example/]" + addr, addr, "[/two.example/]" + twoAddrs}, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cv := newUpstreamConfigValidator(tc.ups, nil, nil, &upstream.Options{ + Timeout: testTimeout, + }) + + go func() { + cv.check() + testutil.RequireSend(testutil.PanicT{}, reqCh, signal{}, testTimeout) + }() + + // Wait for the only request to be sent. + testutil.RequireReceive(t, reqCh, testTimeout) + + // Wait for the check to finish. + testutil.RequireReceive(t, reqCh, testTimeout) + + cv.close() + require.Equal(t, wantStatus, cv.status()) + }) + } +} diff --git a/internal/filtering/filtering.go b/internal/filtering/filtering.go index 6fae9c60..703f6c71 100644 --- a/internal/filtering/filtering.go +++ b/internal/filtering/filtering.go @@ -98,6 +98,8 @@ type Config struct { // EtcHosts is a container of IP-hostname pairs taken from the operating // system configuration files (e.g. /etc/hosts). + // + // TODO(e.burkov): Move it to dnsforward entirely. EtcHosts *aghnet.HostsContainer `yaml:"-"` // Called when the configuration is changed by HTTP request diff --git a/internal/filtering/servicelist.go b/internal/filtering/servicelist.go index 03c51ac8..ec893ecf 100644 --- a/internal/filtering/servicelist.go +++ b/internal/filtering/servicelist.go @@ -509,6 +509,15 @@ var blockedServices = []blockedService{{ "||clubhouse.com^", "||clubhouseapi.com^", }, +}, { + ID: "coolapk", + Name: "CoolApk", + IconSVG: []byte(""), + Rules: []string{ + "||coolapk.com^", + "||coolapkmarket.com^", + "||coolapkmarket.net^", + }, }, { ID: "crunchyroll", Name: "Crunchyroll", @@ -1923,6 +1932,14 @@ var blockedServices = []blockedService{{ Rules: []string{ "||ok.ru^", }, +}, { + ID: "olvid", + Name: "Olvid", + IconSVG: []byte(""), + Rules: []string{ + "||olvid-attachment-chunks.s3.eu-west-3.amazonaws.com^", + "||olvid.io^", + }, }, { ID: "onlyfans", Name: "OnlyFans", diff --git a/internal/home/client.go b/internal/home/client.go index 751bde6d..70ce112e 100644 --- a/internal/home/client.go +++ b/internal/home/client.go @@ -14,12 +14,13 @@ import ( // Client contains information about persistent clients. type Client struct { - // upstreamConfig is the custom upstream config for this client. If - // it's nil, it has not been initialized yet. If it's non-nil and - // empty, there are no valid upstreams. If it's non-nil and non-empty, - // these upstream must be used. - upstreamConfig *proxy.UpstreamConfig + // upstreamConfig is the custom upstream configuration for this client. If + // it's nil, it has not been initialized yet. If it's non-nil and empty, + // there are no valid upstreams. If it's non-nil and non-empty, these + // upstream must be used. + upstreamConfig *proxy.CustomUpstreamConfig + // TODO(d.kolyshev): Make safeSearchConf a pointer. safeSearchConf filtering.SafeSearchConfig SafeSearch filtering.SafeSearch @@ -32,6 +33,9 @@ type Client struct { Tags []string Upstreams []string + UpstreamsCacheSize uint32 + UpstreamsCacheEnabled bool + UseOwnSettings bool FilteringEnabled bool SafeBrowsingEnabled bool @@ -57,8 +61,7 @@ func (c *Client) ShallowClone() (sh *Client) { // closeUpstreams closes the client-specific upstream config of c if any. func (c *Client) closeUpstreams() (err error) { if c.upstreamConfig != nil { - err = c.upstreamConfig.Close() - if err != nil { + if err = c.upstreamConfig.Close(); err != nil { return fmt.Errorf("closing upstreams of client %q: %w", c.Name, err) } } diff --git a/internal/home/clients.go b/internal/home/clients.go index 68e0b7d1..6d3a6d23 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -126,7 +126,13 @@ func (clients *clientsContainer) Init( return nil } - if clients.etcHosts != nil { + // The clients.etcHosts may be nil even if config.Clients.Sources.HostsFile + // is true, because of the deprecated option --no-etc-hosts. + // + // TODO(e.burkov): The option should probably be returned, since hosts file + // currently used not only for clients' information enrichment, but also in + // the filtering module and upstream addresses resolution. + if config.Clients.Sources.HostsFile && clients.etcHosts != nil { go clients.handleHostsUpdates() } @@ -179,6 +185,14 @@ type clientObject struct { Tags []string `yaml:"tags"` Upstreams []string `yaml:"upstreams"` + // UpstreamsCacheSize is the DNS cache size (in bytes). + // + // TODO(d.kolyshev): Use [datasize.Bytesize]. + UpstreamsCacheSize uint32 `yaml:"upstreams_cache_size"` + + // UpstreamsCacheEnabled indicates if the DNS cache is enabled. + UpstreamsCacheEnabled bool `yaml:"upstreams_cache_enabled"` + UseGlobalSettings bool `yaml:"use_global_settings"` FilteringEnabled bool `yaml:"filtering_enabled"` ParentalEnabled bool `yaml:"parental_enabled"` @@ -210,6 +224,8 @@ func (clients *clientsContainer) addFromConfig( UseOwnBlockedServices: !o.UseGlobalBlockedServices, IgnoreQueryLog: o.IgnoreQueryLog, IgnoreStatistics: o.IgnoreStatistics, + UpstreamsCacheEnabled: o.UpstreamsCacheEnabled, + UpstreamsCacheSize: o.UpstreamsCacheSize, } if o.SafeSearchConf.Enabled { @@ -278,6 +294,8 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) { UseGlobalBlockedServices: !cli.UseOwnBlockedServices, IgnoreQueryLog: cli.IgnoreQueryLog, IgnoreStatistics: cli.IgnoreStatistics, + UpstreamsCacheEnabled: cli.UpstreamsCacheEnabled, + UpstreamsCacheSize: cli.UpstreamsCacheSize, } objs = append(objs, o) @@ -419,18 +437,24 @@ func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) { return true } -// findUpstreams returns upstreams configured for the client, identified either -// by its IP address or its ClientID. upsConf is nil if the client isn't found -// or if the client has no custom upstreams. -func (clients *clientsContainer) findUpstreams( +// type check +var _ dnsforward.ClientsContainer = (*clientsContainer)(nil) + +// UpstreamConfigByID implements the [dnsforward.ClientsContainer] interface for +// *clientsContainer. upsConf is nil if the client isn't found or if the client +// has no custom upstreams. +func (clients *clientsContainer) UpstreamConfigByID( id string, -) (upsConf *proxy.UpstreamConfig, err error) { + bootstrap upstream.Resolver, +) (conf *proxy.CustomUpstreamConfig, err error) { clients.lock.Lock() defer clients.lock.Unlock() c, ok := clients.findLocked(id) if !ok { return nil, nil + } else if c.upstreamConfig != nil { + return c.upstreamConfig, nil } upstreams := stringutil.FilterOut(c.Upstreams, dnsforward.IsCommentOrEmpty) @@ -438,24 +462,27 @@ func (clients *clientsContainer) findUpstreams( return nil, nil } - if c.upstreamConfig != nil { - return c.upstreamConfig, nil - } - - var conf *proxy.UpstreamConfig - conf, err = proxy.ParseUpstreamsConfig( + var upsConf *proxy.UpstreamConfig + upsConf, err = proxy.ParseUpstreamsConfig( upstreams, &upstream.Options{ - Bootstrap: config.DNS.BootstrapDNS, + Bootstrap: bootstrap, Timeout: config.DNS.UpstreamTimeout.Duration, HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams), PreferIPv6: config.DNS.BootstrapPreferIPv6, }, ) if err != nil { + // Don't wrap the error since it's informative enough as is. return nil, err } + conf = proxy.NewCustomUpstreamConfig( + upsConf, + c.UpstreamsCacheEnabled, + int(c.UpstreamsCacheSize), + config.DNS.EDNSClientSubnet.Enabled, + ) c.upstreamConfig = conf return conf, nil @@ -672,10 +699,6 @@ func (clients *clientsContainer) Del(name string) (ok bool) { return false } - if err := c.closeUpstreams(); err != nil { - log.Error("client container: removing client %s: %s", name, err) - } - clients.del(c) return true @@ -683,10 +706,14 @@ func (clients *clientsContainer) Del(name string) (ok bool) { // del removes c from the indexes. clients.lock is expected to be locked. func (clients *clientsContainer) del(c *Client) { - // update Name index + if err := c.closeUpstreams(); err != nil { + log.Error("client container: removing client %s: %s", c.Name, err) + } + + // Update the name index. delete(clients.list, c.Name) - // update ID index + // Update the ID index. for _, id := range c.IDs { delete(clients.idIndex, id) } diff --git a/internal/home/clients_internal_test.go b/internal/home/clients_internal_test.go index b8ef598f..30d735bb 100644 --- a/internal/home/clients_internal_test.go +++ b/internal/home/clients_internal_test.go @@ -314,7 +314,7 @@ func TestClientsAddExisting(t *testing.T) { clients.dhcp = dhcpServer - err = dhcpServer.AddStaticLease(&dhcpd.Lease{ + err = dhcpServer.AddStaticLease(&dhcpsvc.Lease{ HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, IP: ip, Hostname: "testhost", @@ -355,13 +355,11 @@ func TestClientsCustomUpstream(t *testing.T) { require.NoError(t, err) assert.True(t, ok) - config, err := clients.findUpstreams("1.2.3.4") - assert.Nil(t, config) + upsConf, err := clients.UpstreamConfigByID("1.2.3.4", net.DefaultResolver) + assert.Nil(t, upsConf) assert.NoError(t, err) - config, err = clients.findUpstreams("1.1.1.1") - require.NotNil(t, config) + upsConf, err = clients.UpstreamConfigByID("1.1.1.1", net.DefaultResolver) + require.NotNil(t, upsConf) assert.NoError(t, err) - assert.Len(t, config.Upstreams, 1) - assert.Len(t, config.DomainReservedUpstreams, 1) } diff --git a/internal/home/clientshttp.go b/internal/home/clientshttp.go index 62a46e78..ad51e944 100644 --- a/internal/home/clientshttp.go +++ b/internal/home/clientshttp.go @@ -56,34 +56,9 @@ type clientJSON struct { IgnoreQueryLog aghalg.NullBool `json:"ignore_querylog"` IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"` -} -// copySettings returns a copy of specific settings from JSON or a previous -// client. -func (j *clientJSON) copySettings( - prev *Client, -) (weekly *schedule.Weekly, ignoreQueryLog, ignoreStatistics bool) { - if j.Schedule != nil { - weekly = j.Schedule.Clone() - } else if prev != nil && prev.BlockedServices != nil { - weekly = prev.BlockedServices.Schedule.Clone() - } else { - weekly = schedule.EmptyWeekly() - } - - if j.IgnoreQueryLog != aghalg.NBNull { - ignoreQueryLog = j.IgnoreQueryLog == aghalg.NBTrue - } else if prev != nil { - ignoreQueryLog = prev.IgnoreQueryLog - } - - if j.IgnoreStatistics != aghalg.NBNull { - ignoreStatistics = j.IgnoreStatistics == aghalg.NBTrue - } else if prev != nil { - ignoreStatistics = prev.IgnoreStatistics - } - - return weekly, ignoreQueryLog, ignoreStatistics + UpstreamsCacheSize uint32 `json:"upstreams_cache_size"` + UpstreamsCacheEnabled aghalg.NullBool `json:"upstreams_cache_enabled"` } type runtimeClientJSON struct { @@ -142,36 +117,35 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http // jsonToClient converts JSON object to Client object. func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *Client, err error) { - var safeSearchConf filtering.SafeSearchConfig - if cj.SafeSearchConf != nil { - safeSearchConf = *cj.SafeSearchConf - } else { - // TODO(d.kolyshev): Remove after cleaning the deprecated - // [clientJSON.SafeSearchEnabled] field. - safeSearchConf = filtering.SafeSearchConfig{ - Enabled: cj.SafeSearchEnabled, - } + safeSearchConf := copySafeSearch(cj.SafeSearchConf, cj.SafeSearchEnabled) - // Set default service flags for enabled safesearch. - if safeSearchConf.Enabled { - safeSearchConf.Bing = true - safeSearchConf.DuckDuckGo = true - safeSearchConf.Google = true - safeSearchConf.Pixabay = true - safeSearchConf.Yandex = true - safeSearchConf.YouTube = true - } + var ignoreQueryLog bool + if cj.IgnoreQueryLog != aghalg.NBNull { + ignoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue + } else if prev != nil { + ignoreQueryLog = prev.IgnoreQueryLog } - weekly, ignoreQueryLog, ignoreStatistics := cj.copySettings(prev) - - bs := &filtering.BlockedServices{ - Schedule: weekly, - IDs: cj.BlockedServices, + var ignoreStatistics bool + if cj.IgnoreStatistics != aghalg.NBNull { + ignoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue + } else if prev != nil { + ignoreStatistics = prev.IgnoreStatistics } - err = bs.Validate() + + var upsCacheEnabled bool + var upsCacheSize uint32 + if cj.UpstreamsCacheEnabled != aghalg.NBNull { + upsCacheEnabled = cj.UpstreamsCacheEnabled == aghalg.NBTrue + upsCacheSize = cj.UpstreamsCacheSize + } else if prev != nil { + upsCacheEnabled = prev.UpstreamsCacheEnabled + upsCacheSize = prev.UpstreamsCacheSize + } + + svcs, err := copyBlockedServices(cj.Schedule, cj.BlockedServices, prev) if err != nil { - return nil, fmt.Errorf("validating blocked services: %w", err) + return nil, fmt.Errorf("invalid blocked services: %w", err) } c = &Client{ @@ -179,7 +153,7 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C Name: cj.Name, - BlockedServices: bs, + BlockedServices: svcs, IDs: cj.IDs, Tags: cj.Tags, @@ -192,6 +166,8 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C UseOwnBlockedServices: !cj.UseGlobalBlockedServices, IgnoreQueryLog: ignoreQueryLog, IgnoreStatistics: ignoreStatistics, + UpstreamsCacheEnabled: upsCacheEnabled, + UpstreamsCacheSize: upsCacheSize, } if safeSearchConf.Enabled { @@ -208,6 +184,63 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *C return c, nil } +// copySafeSearch returns safe search config created from provided parameters. +func copySafeSearch( + jsonConf *filtering.SafeSearchConfig, + enabled bool, +) (conf filtering.SafeSearchConfig) { + if jsonConf != nil { + return *jsonConf + } + + // TODO(d.kolyshev): Remove after cleaning the deprecated + // [clientJSON.SafeSearchEnabled] field. + conf = filtering.SafeSearchConfig{ + Enabled: enabled, + } + + // Set default service flags for enabled safesearch. + if conf.Enabled { + conf.Bing = true + conf.DuckDuckGo = true + conf.Google = true + conf.Pixabay = true + conf.Yandex = true + conf.YouTube = true + } + + return conf +} + +// copyBlockedServices converts a json blocked services to an internal blocked +// services. +func copyBlockedServices( + sch *schedule.Weekly, + svcStrs []string, + prev *Client, +) (svcs *filtering.BlockedServices, err error) { + var weekly *schedule.Weekly + if sch != nil { + weekly = sch.Clone() + } else if prev != nil && prev.BlockedServices != nil { + weekly = prev.BlockedServices.Schedule.Clone() + } else { + weekly = schedule.EmptyWeekly() + } + + svcs = &filtering.BlockedServices{ + Schedule: weekly, + IDs: svcStrs, + } + + err = svcs.Validate() + if err != nil { + return nil, fmt.Errorf("validating blocked services: %w", err) + } + + return svcs, nil +} + // clientToJSON converts Client object to JSON. func clientToJSON(c *Client) (cj *clientJSON) { // TODO(d.kolyshev): Remove after cleaning the deprecated @@ -235,6 +268,9 @@ func clientToJSON(c *Client) (cj *clientJSON) { IgnoreQueryLog: aghalg.BoolToNullBool(c.IgnoreQueryLog), IgnoreStatistics: aghalg.BoolToNullBool(c.IgnoreStatistics), + + UpstreamsCacheSize: c.UpstreamsCacheSize, + UpstreamsCacheEnabled: aghalg.BoolToNullBool(c.UpstreamsCacheEnabled), } } diff --git a/internal/home/config.go b/internal/home/config.go index c53dacb7..e78bf8e9 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -115,6 +115,8 @@ type configuration struct { // Theme is a UI theme for current user. Theme Theme `yaml:"theme"` + // TODO(a.garipov): Make DNS and the fields below pointers and validate + // and/or reset on explicit nulling. DNS dnsConfig `yaml:"dns"` TLS tlsConfigSettings `yaml:"tls"` QueryLog queryLogConfig `yaml:"querylog"` @@ -214,18 +216,21 @@ type dnsConfig struct { // DNS64Prefixes is the list of NAT64 prefixes to be used for DNS64. DNS64Prefixes []netip.Prefix `yaml:"dns64_prefixes"` - // ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests. + // ServeHTTP3 defines if HTTP/3 is allowed for incoming requests. // // TODO(a.garipov): Add to the UI when HTTP/3 support is no longer // experimental. ServeHTTP3 bool `yaml:"serve_http3"` - // UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS + // UseHTTP3Upstreams defines if HTTP/3 is allowed for DNS-over-HTTPS // upstreams. // // TODO(a.garipov): Add to the UI when HTTP/3 support is no longer // experimental. UseHTTP3Upstreams bool `yaml:"use_http3_upstreams"` + + // ServePlainDNS defines if plain DNS is allowed for incoming requests. + ServePlainDNS bool `yaml:"serve_plain_dns"` } type tlsConfigSettings struct { @@ -333,6 +338,7 @@ var config = &configuration{ }, UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout}, UsePrivateRDNS: true, + ServePlainDNS: true, }, TLS: tlsConfigSettings{ PortHTTPS: defaultPortHTTPS, diff --git a/internal/home/dns.go b/internal/home/dns.go index d26dd0d8..59d3fa62 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -138,28 +138,28 @@ func initDNSServer( QueryLog: qlog, PrivateNets: privateNets, Anonymizer: anonymizer, - LocalDomain: config.DHCP.LocalDomainName, DHCPServer: dhcpSrv, + EtcHosts: Context.etcHosts, + LocalDomain: config.DHCP.LocalDomainName, }) + defer func() { + if err != nil { + closeDNSServer() + } + }() if err != nil { - closeDNSServer() - return fmt.Errorf("dnsforward.NewServer: %w", err) } Context.clients.dnsServer = Context.dnsServer - dnsConf, err := newServerConfig(tlsConf, httpReg) + dnsConf, err := newServerConfig(&config.DNS, config.Clients.Sources, tlsConf, httpReg) if err != nil { - closeDNSServer() - return fmt.Errorf("newServerConfig: %w", err) } err = Context.dnsServer.Prepare(dnsConf) if err != nil { - closeDNSServer() - return fmt.Errorf("dnsServer.Prepare: %w", err) } @@ -222,21 +222,37 @@ func ipsToUDPAddrs(ips []netip.Addr, port uint16) (udpAddrs []*net.UDPAddr) { return udpAddrs } +// newServerConfig converts values from the configuration file into the internal +// DNS server configuration. All arguments must not be nil. func newServerConfig( + dnsConf *dnsConfig, + clientSrcConf *clientSourcesConfig, tlsConf *tlsConfigSettings, httpReg aghhttp.RegisterFunc, ) (newConf *dnsforward.ServerConfig, err error) { - dnsConf := config.DNS hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()}) + fwdConf := dnsConf.Config + fwdConf.FilterHandler = applyAdditionalFiltering + fwdConf.ClientsContainer = &Context.clients + newConf = &dnsforward.ServerConfig{ - UDPListenAddrs: ipsToUDPAddrs(hosts, dnsConf.Port), - TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port), - Config: dnsConf.Config, - ConfigModified: onConfigModified, - HTTPRegister: httpReg, - UseDNS64: config.DNS.UseDNS64, - DNS64Prefixes: config.DNS.DNS64Prefixes, + UDPListenAddrs: ipsToUDPAddrs(hosts, dnsConf.Port), + TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port), + Config: fwdConf, + TLSConfig: newDNSTLSConfig(tlsConf, hosts), + TLSAllowUnencryptedDoH: tlsConf.AllowUnencryptedDoH, + UpstreamTimeout: dnsConf.UpstreamTimeout.Duration, + TLSv12Roots: Context.tlsRoots, + ConfigModified: onConfigModified, + HTTPRegister: httpReg, + LocalPTRResolvers: dnsConf.LocalPTRResolvers, + UseDNS64: dnsConf.UseDNS64, + DNS64Prefixes: dnsConf.DNS64Prefixes, + UsePrivateRDNS: dnsConf.UsePrivateRDNS, + ServeHTTP3: dnsConf.ServeHTTP3, + UseHTTP3Upstreams: dnsConf.UseHTTP3Upstreams, + ServePlainDNS: dnsConf.ServePlainDNS, } var initialAddresses []netip.Addr @@ -254,79 +270,81 @@ func newServerConfig( AddressUpdater: &Context.clients, InitialAddresses: initialAddresses, CatchPanics: true, - UseRDNS: config.Clients.Sources.RDNS, - UseWHOIS: config.Clients.Sources.WHOIS, + UseRDNS: clientSrcConf.RDNS, + UseWHOIS: clientSrcConf.WHOIS, } - if tlsConf.Enabled { - newConf.TLSConfig = tlsConf.TLSConfig - newConf.TLSConfig.ServerName = tlsConf.ServerName - - if tlsConf.PortHTTPS != 0 { - newConf.HTTPSListenAddrs = ipsToTCPAddrs(hosts, tlsConf.PortHTTPS) - } - - if tlsConf.PortDNSOverTLS != 0 { - newConf.TLSListenAddrs = ipsToTCPAddrs(hosts, tlsConf.PortDNSOverTLS) - } - - if tlsConf.PortDNSOverQUIC != 0 { - newConf.QUICListenAddrs = ipsToUDPAddrs(hosts, tlsConf.PortDNSOverQUIC) - } - - if tlsConf.PortDNSCrypt != 0 { - newConf.DNSCryptConfig, err = newDNSCrypt(hosts, *tlsConf) - if err != nil { - // Don't wrap the error, because it's already wrapped by - // newDNSCrypt. - return nil, err - } - } + newConf.DNSCryptConfig, err = newDNSCryptConfig(tlsConf, hosts) + if err != nil { + // Don't wrap the error, because it's already wrapped by + // newDNSCryptConfig. + return nil, err } - newConf.TLSv12Roots = Context.tlsRoots - newConf.TLSAllowUnencryptedDoH = tlsConf.AllowUnencryptedDoH - - newConf.FilterHandler = applyAdditionalFiltering - newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams - - newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers - newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration - - newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS - newConf.ServeHTTP3 = dnsConf.ServeHTTP3 - newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams - return newConf, nil } -func newDNSCrypt(hosts []netip.Addr, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) { - if tlsConf.DNSCryptConfigFile == "" { - return dnscc, errors.Error("no dnscrypt_config_file") +// newDNSTLSConfig converts values from the configuration file into the internal +// TLS settings for the DNS server. tlsConf must not be nil. +func newDNSTLSConfig(conf *tlsConfigSettings, addrs []netip.Addr) (dnsConf dnsforward.TLSConfig) { + if !conf.Enabled { + return dnsforward.TLSConfig{} } - f, err := os.Open(tlsConf.DNSCryptConfigFile) + dnsConf = conf.TLSConfig + dnsConf.ServerName = conf.ServerName + + if conf.PortHTTPS != 0 { + dnsConf.HTTPSListenAddrs = ipsToTCPAddrs(addrs, conf.PortHTTPS) + } + + if conf.PortDNSOverTLS != 0 { + dnsConf.TLSListenAddrs = ipsToTCPAddrs(addrs, conf.PortDNSOverTLS) + } + + if conf.PortDNSOverQUIC != 0 { + dnsConf.QUICListenAddrs = ipsToUDPAddrs(addrs, conf.PortDNSOverQUIC) + } + + return dnsConf +} + +// newDNSCryptConfig converts values from the configuration file into the +// internal DNSCrypt settings for the DNS server. conf must not be nil. +func newDNSCryptConfig( + conf *tlsConfigSettings, + addrs []netip.Addr, +) (dnsCryptConf dnsforward.DNSCryptConfig, err error) { + if !conf.Enabled || conf.PortDNSCrypt == 0 { + return dnsforward.DNSCryptConfig{}, nil + } + + if conf.DNSCryptConfigFile == "" { + return dnsforward.DNSCryptConfig{}, errors.Error("no dnscrypt_config_file") + } + + f, err := os.Open(conf.DNSCryptConfigFile) if err != nil { - return dnscc, fmt.Errorf("opening dnscrypt config: %w", err) + return dnsforward.DNSCryptConfig{}, fmt.Errorf("opening dnscrypt config: %w", err) } defer func() { err = errors.WithDeferred(err, f.Close()) }() rc := &dnscrypt.ResolverConfig{} err = yaml.NewDecoder(f).Decode(rc) if err != nil { - return dnscc, fmt.Errorf("decoding dnscrypt config: %w", err) + return dnsforward.DNSCryptConfig{}, fmt.Errorf("decoding dnscrypt config: %w", err) } cert, err := rc.CreateCert() if err != nil { - return dnscc, fmt.Errorf("creating dnscrypt cert: %w", err) + return dnsforward.DNSCryptConfig{}, fmt.Errorf("creating dnscrypt cert: %w", err) } return dnsforward.DNSCryptConfig{ ResolverCert: cert, ProviderName: rc.ProviderName, - UDPListenAddrs: ipsToUDPAddrs(hosts, tlsConf.PortDNSCrypt), - TCPListenAddrs: ipsToTCPAddrs(hosts, tlsConf.PortDNSCrypt), + UDPListenAddrs: ipsToUDPAddrs(addrs, conf.PortDNSCrypt), + TCPListenAddrs: ipsToTCPAddrs(addrs, conf.PortDNSCrypt), Enabled: true, }, nil } @@ -342,34 +360,36 @@ func getDNSEncryption() (de dnsEncryption) { Context.tls.WriteDiskConfig(&tlsConf) - if tlsConf.Enabled && len(tlsConf.ServerName) != 0 { - hostname := tlsConf.ServerName - if tlsConf.PortHTTPS != 0 { - addr := hostname - if p := tlsConf.PortHTTPS; p != defaultPortHTTPS { - addr = netutil.JoinHostPort(addr, p) - } + if !tlsConf.Enabled || len(tlsConf.ServerName) == 0 { + return dnsEncryption{} + } - de.https = (&url.URL{ - Scheme: "https", - Host: addr, - Path: "/dns-query", - }).String() + hostname := tlsConf.ServerName + if tlsConf.PortHTTPS != 0 { + addr := hostname + if p := tlsConf.PortHTTPS; p != defaultPortHTTPS { + addr = netutil.JoinHostPort(addr, p) } - if p := tlsConf.PortDNSOverTLS; p != 0 { - de.tls = (&url.URL{ - Scheme: "tls", - Host: netutil.JoinHostPort(hostname, p), - }).String() - } + de.https = (&url.URL{ + Scheme: "https", + Host: addr, + Path: "/dns-query", + }).String() + } - if p := tlsConf.PortDNSOverQUIC; p != 0 { - de.quic = (&url.URL{ - Scheme: "quic", - Host: netutil.JoinHostPort(hostname, p), - }).String() - } + if p := tlsConf.PortDNSOverTLS; p != 0 { + de.tls = (&url.URL{ + Scheme: "tls", + Host: netutil.JoinHostPort(hostname, p), + }).String() + } + + if p := tlsConf.PortDNSOverQUIC; p != 0 { + de.quic = (&url.URL{ + Scheme: "quic", + Host: netutil.JoinHostPort(hostname, p), + }).String() } return de @@ -454,7 +474,7 @@ func reconfigureDNSServer() (err error) { tlsConf := &tlsConfigSettings{} Context.tls.WriteDiskConfig(tlsConf) - newConf, err := newServerConfig(tlsConf, httpRegister) + newConf, err := newServerConfig(&config.DNS, config.Clients.Sources, tlsConf, httpRegister) if err != nil { return fmt.Errorf("generating forwarding dns server config: %w", err) } @@ -520,11 +540,13 @@ type safeSearchResolver struct{} var _ filtering.Resolver = safeSearchResolver{} // LookupIP implements [filtering.Resolver] interface for safeSearchResolver. -// It returns the slice of net.IP with IPv4 and IPv6 instances. -// -// TODO(a.garipov): Support network. -func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) { - addrs, err := Context.dnsServer.Resolve(host) +// It returns the slice of net.Addr with IPv4 and IPv6 instances. +func (r safeSearchResolver) LookupIP( + ctx context.Context, + network string, + host string, +) (ips []net.IP, err error) { + addrs, err := Context.dnsServer.Resolve(ctx, network, host) if err != nil { return nil, err } @@ -534,7 +556,7 @@ func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []n } for _, a := range addrs { - ips = append(ips, a.IP) + ips = append(ips, a.AsSlice()) } return ips, nil diff --git a/internal/home/home.go b/internal/home/home.go index 7e57652e..ab2d83a2 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -6,7 +6,6 @@ import ( "crypto/x509" "fmt" "io/fs" - "net" "net/http" "net/netip" "net/url" @@ -160,7 +159,7 @@ func setupContext(opts options) (err error) { os.Exit(0) } - if !opts.noEtcHosts && config.Clients.Sources.HostsFile { + if !opts.noEtcHosts { err = setupHostsContainer() if err != nil { // Don't wrap the error, because it's informative enough as is. @@ -239,13 +238,13 @@ func setupHostsContainer() (err error) { ) if err != nil { closeErr := hostsWatcher.Close() - if errors.Is(err, aghnet.ErrNoHostsPaths) && closeErr == nil { + if errors.Is(err, aghnet.ErrNoHostsPaths) { log.Info("warning: initing hosts container: %s", err) - return nil + return closeErr } - return errors.WithDeferred(fmt.Errorf("initing hosts container: %w", err), closeErr) + return errors.Join(fmt.Errorf("initializing hosts container: %w", err), closeErr) } return nil @@ -294,19 +293,13 @@ func initContextClients() (err error) { arpDB = arpdb.New() } - err = Context.clients.Init( + return Context.clients.Init( config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpDB, config.Filtering, ) - if err != nil { - // Don't wrap the error, because it's informative enough as is. - return err - } - - return nil } // setupBindOpts overrides bind host/port from the opts. @@ -376,11 +369,15 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) { upsOpts := &upstream.Options{ Timeout: dnsTimeout, - ServerIPAddrs: []net.IP{ - {94, 140, 14, 15}, - {94, 140, 15, 16}, - net.ParseIP("2a10:50c0::bad1:ff"), - net.ParseIP("2a10:50c0::bad2:ff"), + Bootstrap: upstream.StaticResolver{ + // 94.140.14.15. + netip.AddrFrom4([4]byte{94, 140, 14, 15}), + // 94.140.14.16. + netip.AddrFrom4([4]byte{94, 140, 14, 16}), + // 2a10:50c0::bad1:ff. + netip.AddrFrom16([16]byte{42, 16, 80, 192, 12: 186, 209, 0, 255}), + // 2a10:50c0::bad2:ff. + netip.AddrFrom16([16]byte{42, 16, 80, 192, 12: 186, 210, 0, 255}), }, } diff --git a/internal/ipset/ipset_linux.go b/internal/ipset/ipset_linux.go index e1c3f3fa..4cf1fe12 100644 --- a/internal/ipset/ipset_linux.go +++ b/internal/ipset/ipset_linux.go @@ -101,6 +101,7 @@ func (qc *queryConn) listAll() (sets []props, err error) { type ipsetConn interface { Add(name string, entries ...*ipset.Entry) (err error) Close() (err error) + Header(name string) (p *ipset.HeaderPolicy, err error) listAll() (sets []props, err error) } @@ -112,6 +113,9 @@ type props struct { // name of the ipset. name string + // typeName of the ipset. + typeName string + // family of the IP addresses in the ipset. family netfilter.ProtoFamily @@ -148,6 +152,8 @@ func (p *props) parseAttribute(a netfilter.Attribute) { case ipset.AttrSetName: // Trim the null character. p.name = string(bytes.Trim(a.Data, "\x00")) + case ipset.AttrTypeName: + p.typeName = string(bytes.Trim(a.Data, "\x00")) case ipset.AttrFamily: p.family = netfilter.ProtoFamily(a.Data[0]) default: @@ -263,8 +269,9 @@ func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) { return err } + currentlyKnown := map[string]props{} for _, p := range all { - m.nameToIpset[p.name] = p + currentlyKnown[p.name] = p } for i, confStr := range ipsetConf { @@ -275,7 +282,7 @@ func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) { } var ipsets []props - ipsets, err = m.ipsets(ipsetNames) + ipsets, err = m.ipsets(ipsetNames, currentlyKnown) if err != nil { return fmt.Errorf("getting ipsets from config line at idx %d: %w", i, err) } @@ -288,14 +295,64 @@ func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) { return nil } -// ipsets returns currently known ipsets. -func (m *manager) ipsets(names []string) (sets []props, err error) { +// ipsetProps returns the properties of an ipset with the given name. +// +// Additional header data query. See https://github.com/AdguardTeam/AdGuardHome/issues/6420. +// +// TODO(s.chzhen): Use *props. +func (m *manager) ipsetProps(name string) (p props, err error) { + // The family doesn't seem to matter when we use a header query, so + // query only the IPv4 one. + // + // TODO(a.garipov): Find out if this is a bug or a feature. + var res *ipset.HeaderPolicy + res, err = m.ipv4Conn.Header(name) + if err != nil { + return props{}, err + } + + if res == nil || res.Family == nil { + return props{}, errors.Error("empty response or no family data") + } + + family := netfilter.ProtoFamily(res.Family.Value) + if family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 { + return props{}, fmt.Errorf("unexpected ipset family %q", family) + } + + typeName := res.TypeName.Get() + + return props{ + name: name, + typeName: typeName, + family: family, + isPersistent: false, + }, nil +} + +// ipsets returns ipset properties of currently known ipsets. It also makes an +// additional ipset header data query if needed. +func (m *manager) ipsets(names []string, currentlyKnown map[string]props) (sets []props, err error) { for _, n := range names { - p, ok := m.nameToIpset[n] + p, ok := currentlyKnown[n] if !ok { return nil, fmt.Errorf("unknown ipset %q", n) } + if p.family != netfilter.ProtoIPv4 && p.family != netfilter.ProtoIPv6 { + log.Debug("ipset: getting properties: %q %q unexpected ipset family %q", + p.name, + p.typeName, + p.family, + ) + + p, err = m.ipsetProps(n) + if err != nil { + return nil, fmt.Errorf("%q %q making header query: %w", p.name, p.typeName, err) + } + } + + m.nameToIpset[n] = p sets = append(sets, p) } @@ -336,6 +393,8 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err return nil, fmt.Errorf("getting ipsets: %w", err) } + log.Debug("ipset: initialized") + return m, nil } @@ -404,7 +463,7 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error err = conn.Add(set.name, entries...) if err != nil { - return 0, fmt.Errorf("adding %q%s to ipset %q: %w", host, ips, set.name, err) + return 0, fmt.Errorf("adding %q%s to %q %q: %w", host, ips, set.name, set.typeName, err) } // Only add these to the cache once we're sure that all of them were @@ -440,10 +499,10 @@ func (m *manager) addToSets( return n, err } default: - return n, fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name) + return n, fmt.Errorf("%q %q unexpected family %q", set.name, set.typeName, set.family) } - log.Debug("ipset: added %d ips to set %s", nn, set.name) + log.Debug("ipset: added %d ips to set %q %q", nn, set.name, set.typeName) n += nn } diff --git a/internal/ipset/ipset_linux_internal_test.go b/internal/ipset/ipset_linux_internal_test.go index 84e25650..f22d93c1 100644 --- a/internal/ipset/ipset_linux_internal_test.go +++ b/internal/ipset/ipset_linux_internal_test.go @@ -47,6 +47,11 @@ func (c *fakeConn) Close() (err error) { return nil } +// Header implements the [ipsetConn] interface for *fakeConn. +func (c *fakeConn) Header(_ string) (_ *ipset.HeaderPolicy, _ error) { + return nil, nil +} + // listAll implements the [ipsetConn] interface for *fakeConn. func (c *fakeConn) listAll() (sets []props, err error) { return c.sets, nil diff --git a/internal/next/AdGuardHome.example.yaml b/internal/next/AdGuardHome.example.yaml new file mode 100644 index 00000000..1bdd8313 --- /dev/null +++ b/internal/next/AdGuardHome.example.yaml @@ -0,0 +1,30 @@ +# This is a file showing example configuration for AdGuard Home. +# +# TODO(a.garipov): Move to the top level once the rewrite is over. + +dns: + addresses: + - '0.0.0.0:53' + bootstrap_dns: + - '9.9.9.10' + - '149.112.112.10' + - '2620:fe::10' + - '2620:fe::fe:10' + upstream_dns: + - '8.8.8.8' + dns64_prefixes: + - '1234::/64' + upstream_timeout: 1s + bootstrap_prefer_ipv6: true + use_dns64: true +http: + pprof: + enabled: true + port: 6060 + addresses: + - '0.0.0.0:3000' + secure_addresses: [] + timeout: 5s + force_https: true +log: + verbose: true diff --git a/internal/next/changelog.md b/internal/next/changelog.md new file mode 100644 index 00000000..224184bc --- /dev/null +++ b/internal/next/changelog.md @@ -0,0 +1,42 @@ +# AdGuard Home v0.108.0 Changelog DRAFT + +This changelog should be merged into the main one once the next API matures +enough. + +## [v0.108.0] - TODO + +### Added + +- The ability to change the port of the pprof debug API. +- The ability to log to stderr using `--logFile=stderr`. +- The new `--web-addr` flag to set the Web UI address in a `host:port` form. +- `SIGHUP` now reloads all configuration from the configuration file ([#5676]). + +### Changed + +#### New HTTP API + +**TODO(a.garipov):** Describe the new API and add a link to the new OpenAPI doc. + +#### Other changes + +- `-h` is now an alias for `--help` instead of the removed `--host`, see below. + Use `--web-addr=host:port` to set an address on which to serve the Web UI. + +### Fixed + +- `--check-config` breaking the configuration file ([#4067]). +- Inconsistent application of `--work-dir/-w` ([#2598], [#2902]). +- The order of `-v/--verbose` and `--version` being significant ([#2893]). + +### Removed + +- The deprecated `--no-mem-optimization` and `--no-etc-hosts` flags. +- `--host` and `-p/--port` flags. Use `--web-addr=host:port` to set an address + on which to serve the Web UI. `-h` is now an alias for `--help`, see above. + +[#2598]: https://github.com/AdguardTeam/AdGuardHome/issues/2598 +[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893 +[#2902]: https://github.com/AdguardTeam/AdGuardHome/issues/2902 +[#4067]: https://github.com/AdguardTeam/AdGuardHome/issues/4067 +[#5676]: https://github.com/AdguardTeam/AdGuardHome/issues/5676 diff --git a/internal/next/cmd/cmd.go b/internal/next/cmd/cmd.go new file mode 100644 index 00000000..1c118bdb --- /dev/null +++ b/internal/next/cmd/cmd.go @@ -0,0 +1,95 @@ +// Package cmd is the AdGuard Home entry point. It assembles the configuration +// file manager, sets up signal processing logic, and so on. +// +// TODO(a.garipov): Move to the upper-level internal/. +package cmd + +import ( + "context" + "io/fs" + "os" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/next/configmgr" + "github.com/AdguardTeam/AdGuardHome/internal/version" + "github.com/AdguardTeam/golibs/log" +) + +// Main is the entry point of AdGuard Home. +func Main(embeddedFrontend fs.FS) { + start := time.Now() + + cmdName := os.Args[0] + opts, err := parseOptions(cmdName, os.Args[1:]) + exitCode, needExit := processOptions(opts, cmdName, err) + if needExit { + os.Exit(exitCode) + } + + err = setLog(opts) + check(err) + + log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid()) + + if opts.workDir != "" { + log.Info("changing working directory to %q", opts.workDir) + err = os.Chdir(opts.workDir) + check(err) + } + + frontend, err := frontendFromOpts(opts, embeddedFrontend) + check(err) + + confMgrConf := &configmgr.Config{ + Frontend: frontend, + WebAddr: opts.webAddr, + Start: start, + FileName: opts.confFile, + } + + confMgr, err := newConfigMgr(confMgrConf) + check(err) + + web := confMgr.Web() + err = web.Start() + check(err) + + dns := confMgr.DNS() + err = dns.Start() + check(err) + + sigHdlr := newSignalHandler( + confMgrConf, + opts.pidFile, + web, + dns, + ) + + sigHdlr.handle() +} + +// defaultTimeout is the timeout used for some operations where another timeout +// hasn't been defined yet. +const defaultTimeout = 5 * time.Second + +// ctxWithDefaultTimeout is a helper function that returns a context with +// timeout set to defaultTimeout. +func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) { + return context.WithTimeout(context.Background(), defaultTimeout) +} + +// newConfigMgr returns a new configuration manager using defaultTimeout as the +// context timeout. +func newConfigMgr(c *configmgr.Config) (m *configmgr.Manager, err error) { + ctx, cancel := ctxWithDefaultTimeout() + defer cancel() + + return configmgr.New(ctx, c) +} + +// check is a simple error-checking helper. It must only be used within Main. +func check(err error) { + if err != nil { + panic(err) + } +} diff --git a/internal/next/cmd/log.go b/internal/next/cmd/log.go new file mode 100644 index 00000000..3aa2a0e5 --- /dev/null +++ b/internal/next/cmd/log.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/AdguardTeam/AdGuardHome/internal/aghos" + "github.com/AdguardTeam/golibs/log" +) + +// syslogServiceName is the name of the AdGuard Home service used for writing +// logs to the system log. +const syslogServiceName = "AdGuardHome" + +// setLog sets up the text logging. +// +// TODO(a.garipov): Add parameters from configuration file. +func setLog(opts *options) (err error) { + switch opts.confFile { + case "stdout": + log.SetOutput(os.Stdout) + case "stderr": + log.SetOutput(os.Stderr) + case "syslog": + err = aghos.ConfigureSyslog(syslogServiceName) + if err != nil { + return fmt.Errorf("initializing syslog: %w", err) + } + default: + // TODO(a.garipov): Use the path. + } + + if opts.verbose { + log.SetLevel(log.DEBUG) + log.Debug("verbose logging enabled") + } + + return nil +} diff --git a/internal/next/cmd/opt.go b/internal/next/cmd/opt.go new file mode 100644 index 00000000..fdbea2a0 --- /dev/null +++ b/internal/next/cmd/opt.go @@ -0,0 +1,418 @@ +package cmd + +import ( + "encoding" + "flag" + "fmt" + "io" + "io/fs" + "net/netip" + "os" + "strings" + + "github.com/AdguardTeam/AdGuardHome/internal/next/configmgr" + "github.com/AdguardTeam/AdGuardHome/internal/version" + "github.com/AdguardTeam/golibs/log" + "golang.org/x/exp/slices" +) + +// options contains all command-line options for the AdGuardHome(.exe) binary. +type options struct { + // confFile is the path to the configuration file. + confFile string + + // logFile is the path to the log file. Special values: + // + // - "stdout": Write to stdout (the default). + // - "stderr": Write to stderr. + // - "syslog": Write to the system log. + logFile string + + // pidFile is the path to the file where to store the PID. + pidFile string + + // serviceAction is the service control action to perform: + // + // - "install": Installs AdGuard Home as a system service. + // - "uninstall": Uninstalls it. + // - "status": Prints the service status. + // - "start": Starts the previously installed service. + // - "stop": Stops the previously installed service. + // - "restart": Restarts the previously installed service. + // - "reload": Reloads the configuration. + // - "run": This is a special command that is not supposed to be used + // directly it is specified when we register a service, and it indicates + // to the app that it is being run as a service. + // + // TODO(a.garipov): Use. + serviceAction string + + // workDir is the path to the working directory. It is applied before all + // other configuration is read, so all relative paths are relative to it. + workDir string + + // webAddr contains the address on which to serve the web UI. + webAddr netip.AddrPort + + // checkConfig, if true, instructs AdGuard Home to check the configuration + // file, optionally print an error message to stdout, and exit with a + // corresponding exit code. + checkConfig bool + + // disableUpdate, if true, prevents AdGuard Home from automatically checking + // for updates. + // + // TODO(a.garipov): Use. + disableUpdate bool + + // glinetMode enables the GL-Inet compatibility mode. + // + // TODO(a.garipov): Use. + glinetMode bool + + // help, if true, instructs AdGuard Home to print the command-line option + // help message and quit with a successful exit-code. + help bool + + // localFrontend, if true, instructs AdGuard Home to use the local frontend + // directory instead of the files compiled into the binary. + // + // TODO(a.garipov): Use. + localFrontend bool + + // performUpdate, if true, instructs AdGuard Home to update the current + // binary and restart the service in case it's installed. + // + // TODO(a.garipov): Use. + performUpdate bool + + // verbose, if true, instructs AdGuard Home to enable verbose logging. + verbose bool + + // version, if true, instructs AdGuard Home to print the version to stdout + // and quit with a successful exit-code. If verbose is also true, print a + // more detailed version description. + version bool +} + +// Indexes to help with the [commandLineOptions] initialization. +const ( + confFileIdx = iota + logFileIdx + pidFileIdx + serviceActionIdx + workDirIdx + webAddrIdx + checkConfigIdx + disableUpdateIdx + glinetModeIdx + helpIdx + localFrontend + performUpdateIdx + verboseIdx + versionIdx +) + +// commandLineOption contains information about a command-line option: its long +// and, if there is one, short forms, the value type, the description, and the +// default value. +type commandLineOption struct { + defaultValue any + description string + long string + short string + valueType string +} + +// commandLineOptions are all command-line options currently supported by +// AdGuard Home. +var commandLineOptions = []*commandLineOption{ + confFileIdx: { + // TODO(a.garipov): Remove the directory when the new code is ready. + defaultValue: "internal/next/AdGuardHome.yaml", + description: "Path to the config file.", + long: "config", + short: "c", + valueType: "path", + }, + + logFileIdx: { + defaultValue: "stdout", + description: `Path to log file. Special values include "stdout", "stderr", and "syslog".`, + long: "logfile", + short: "l", + valueType: "path", + }, + + pidFileIdx: { + defaultValue: "", + description: "Path to the file where to store the PID.", + long: "pidfile", + short: "", + valueType: "path", + }, + + serviceActionIdx: { + defaultValue: "", + description: `Service control action: "status", "install" (as a service), ` + + `"uninstall" (as a service), "start", "stop", "restart", "reload" (configuration).`, + long: "service", + short: "s", + valueType: "action", + }, + + workDirIdx: { + defaultValue: "", + description: `Path to the working directory. ` + + `It is applied before all other configuration is read, ` + + `so all relative paths are relative to it.`, + long: "work-dir", + short: "w", + valueType: "path", + }, + + webAddrIdx: { + defaultValue: netip.AddrPort{}, + description: `Address to serve the web UI on, in the host:port format.`, + long: "web-addr", + short: "", + valueType: "host:port", + }, + + checkConfigIdx: { + defaultValue: false, + description: "Check configuration, print errors to stdout, and quit.", + long: "check-config", + short: "", + valueType: "", + }, + + disableUpdateIdx: { + defaultValue: false, + description: "Disable automatic update checking.", + long: "no-check-update", + short: "", + valueType: "", + }, + + glinetModeIdx: { + defaultValue: false, + description: "Run in GL-Inet compatibility mode.", + long: "glinet", + short: "", + valueType: "", + }, + + helpIdx: { + defaultValue: false, + description: "Print this help message and quit.", + long: "help", + short: "h", + valueType: "", + }, + + localFrontend: { + defaultValue: false, + description: "Use local frontend directories.", + long: "local-frontend", + short: "", + valueType: "", + }, + + performUpdateIdx: { + defaultValue: false, + description: "Update the current binary and restart the service in case it's installed.", + long: "update", + short: "", + valueType: "", + }, + + verboseIdx: { + defaultValue: false, + description: "Enable verbose logging.", + long: "verbose", + short: "v", + valueType: "", + }, + + versionIdx: { + defaultValue: false, + description: `Print the version to stdout and quit. ` + + `Print a more detailed version description with -v.`, + long: "version", + short: "", + valueType: "", + }, +} + +// parseOptions parses the command-line options for AdGuardHome. +func parseOptions(cmdName string, args []string) (opts *options, err error) { + flags := flag.NewFlagSet(cmdName, flag.ContinueOnError) + + opts = &options{} + for i, fieldPtr := range []any{ + confFileIdx: &opts.confFile, + logFileIdx: &opts.logFile, + pidFileIdx: &opts.pidFile, + serviceActionIdx: &opts.serviceAction, + workDirIdx: &opts.workDir, + webAddrIdx: &opts.webAddr, + checkConfigIdx: &opts.checkConfig, + disableUpdateIdx: &opts.disableUpdate, + glinetModeIdx: &opts.glinetMode, + helpIdx: &opts.help, + localFrontend: &opts.localFrontend, + performUpdateIdx: &opts.performUpdate, + verboseIdx: &opts.verbose, + versionIdx: &opts.version, + } { + addOption(flags, fieldPtr, commandLineOptions[i]) + } + + flags.Usage = func() { usage(cmdName, os.Stderr) } + + err = flags.Parse(args) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, err + } + + return opts, nil +} + +// addOption adds the command-line option described by o to flags using fieldPtr +// as the pointer to the value. +func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) { + switch fieldPtr := fieldPtr.(type) { + case *string: + flags.StringVar(fieldPtr, o.long, o.defaultValue.(string), o.description) + if o.short != "" { + flags.StringVar(fieldPtr, o.short, o.defaultValue.(string), o.description) + } + case *bool: + flags.BoolVar(fieldPtr, o.long, o.defaultValue.(bool), o.description) + if o.short != "" { + flags.BoolVar(fieldPtr, o.short, o.defaultValue.(bool), o.description) + } + case encoding.TextUnmarshaler: + flags.TextVar(fieldPtr, o.long, o.defaultValue.(encoding.TextMarshaler), o.description) + if o.short != "" { + flags.TextVar(fieldPtr, o.short, o.defaultValue.(encoding.TextMarshaler), o.description) + } + default: + panic(fmt.Errorf("unexpected field pointer type %T", fieldPtr)) + } +} + +// usage prints a usage message similar to the one printed by package flag but +// taking long vs. short versions into account as well as using more informative +// value hints. +func usage(cmdName string, output io.Writer) { + options := slices.Clone(commandLineOptions) + slices.SortStableFunc(options, func(a, b *commandLineOption) (res int) { + return strings.Compare(a.long, b.long) + }) + + b := &strings.Builder{} + _, _ = fmt.Fprintf(b, "Usage of %s:\n", cmdName) + + for _, o := range options { + writeUsageLine(b, o) + + // Use four spaces before the tab to trigger good alignment for both 4- + // and 8-space tab stops. + if shouldIncludeDefault(o.defaultValue) { + _, _ = fmt.Fprintf(b, " \t%s (Default value: %q)\n", o.description, o.defaultValue) + } else { + _, _ = fmt.Fprintf(b, " \t%s\n", o.description) + } + } + + _, _ = io.WriteString(output, b.String()) +} + +// shouldIncludeDefault returns true if this default value should be printed. +func shouldIncludeDefault(v any) (ok bool) { + switch v := v.(type) { + case bool: + return v + case string: + return v != "" + default: + return v == nil + } +} + +// writeUsageLine writes the usage line for the provided command-line option. +func writeUsageLine(b *strings.Builder, o *commandLineOption) { + if o.short == "" { + if o.valueType == "" { + _, _ = fmt.Fprintf(b, " --%s\n", o.long) + } else { + _, _ = fmt.Fprintf(b, " --%s=%s\n", o.long, o.valueType) + } + + return + } + + if o.valueType == "" { + _, _ = fmt.Fprintf(b, " --%s/-%s\n", o.long, o.short) + } else { + _, _ = fmt.Fprintf(b, " --%[1]s=%[3]s/-%[2]s %[3]s\n", o.long, o.short, o.valueType) + } +} + +// processOptions decides if AdGuard Home should exit depending on the results +// of command-line option parsing. +func processOptions( + opts *options, + cmdName string, + parseErr error, +) (exitCode int, needExit bool) { + if parseErr != nil { + // Assume that usage has already been printed. + return statusArgumentError, true + } + + if opts.help { + usage(cmdName, os.Stdout) + + return statusSuccess, true + } + + if opts.version { + if opts.verbose { + fmt.Println(version.Verbose()) + } else { + fmt.Printf("AdGuard Home %s\n", version.Version()) + } + + return statusSuccess, true + } + + if opts.checkConfig { + err := configmgr.Validate(opts.confFile) + if err != nil { + _, _ = io.WriteString(os.Stdout, err.Error()+"\n") + + return statusError, true + } + + return statusSuccess, true + } + + return 0, false +} + +// frontendFromOpts returns the frontend to use based on the options. +func frontendFromOpts(opts *options, embeddedFrontend fs.FS) (frontend fs.FS, err error) { + const frontendSubdir = "build/static" + + if opts.localFrontend { + log.Info("warning: using local frontend files") + + return os.DirFS(frontendSubdir), nil + } + + return fs.Sub(embeddedFrontend, frontendSubdir) +} diff --git a/internal/next/cmd/signal.go b/internal/next/cmd/signal.go new file mode 100644 index 00000000..b3bae338 --- /dev/null +++ b/internal/next/cmd/signal.go @@ -0,0 +1,167 @@ +package cmd + +import ( + "os" + "strconv" + + "github.com/AdguardTeam/AdGuardHome/internal/aghos" + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/configmgr" + "github.com/AdguardTeam/golibs/log" + "github.com/google/renameio/v2/maybe" +) + +// signalHandler processes incoming signals and shuts services down. +type signalHandler struct { + // confMgrConf contains the configuration parameters for the configuration + // manager. + confMgrConf *configmgr.Config + + // signal is the channel to which OS signals are sent. + signal chan os.Signal + + // pidFile is the path to the file where to store the PID, if any. + pidFile string + + // services are the services that are shut down before application exiting. + services []agh.Service +} + +// handle processes OS signals. +func (h *signalHandler) handle() { + defer log.OnPanic("signalHandler.handle") + + h.writePID() + + for sig := range h.signal { + log.Info("sighdlr: received signal %q", sig) + + if aghos.IsReconfigureSignal(sig) { + h.reconfigure() + } else if aghos.IsShutdownSignal(sig) { + status := h.shutdown() + h.removePID() + + log.Info("sighdlr: exiting with status %d", status) + + os.Exit(status) + } + } +} + +// reconfigure rereads the configuration file and updates and restarts services. +func (h *signalHandler) reconfigure() { + log.Info("sighdlr: reconfiguring adguard home") + + status := h.shutdown() + if status != statusSuccess { + log.Info("sighdlr: reconfiguring: exiting with status %d", status) + + os.Exit(status) + } + + // TODO(a.garipov): This is a very rough way to do it. Some services can be + // reconfigured without the full shutdown, and the error handling is + // currently not the best. + + confMgr, err := newConfigMgr(h.confMgrConf) + check(err) + + web := confMgr.Web() + err = web.Start() + check(err) + + dns := confMgr.DNS() + err = dns.Start() + check(err) + + h.services = []agh.Service{ + dns, + web, + } + + log.Info("sighdlr: successfully reconfigured adguard home") +} + +// Exit status constants. +const ( + statusSuccess = 0 + statusError = 1 + statusArgumentError = 2 +) + +// shutdown gracefully shuts down all services. +func (h *signalHandler) shutdown() (status int) { + ctx, cancel := ctxWithDefaultTimeout() + defer cancel() + + status = statusSuccess + + log.Info("sighdlr: shutting down services") + for i, service := range h.services { + err := service.Shutdown(ctx) + if err != nil { + log.Error("sighdlr: shutting down service at index %d: %s", i, err) + status = statusError + } + } + + return status +} + +// newSignalHandler returns a new signalHandler that shuts down svcs. +func newSignalHandler( + confMgrConf *configmgr.Config, + pidFile string, + svcs ...agh.Service, +) (h *signalHandler) { + h = &signalHandler{ + confMgrConf: confMgrConf, + signal: make(chan os.Signal, 1), + pidFile: pidFile, + services: svcs, + } + + aghos.NotifyShutdownSignal(h.signal) + aghos.NotifyReconfigureSignal(h.signal) + + return h +} + +// writePID writes the PID to the file, if needed. Any errors are reported to +// log. +func (h *signalHandler) writePID() { + if h.pidFile == "" { + return + } + + // Use 8, since most PIDs will fit. + data := make([]byte, 0, 8) + data = strconv.AppendInt(data, int64(os.Getpid()), 10) + data = append(data, '\n') + + err := maybe.WriteFile(h.pidFile, data, 0o644) + if err != nil { + log.Error("sighdlr: writing pidfile: %s", err) + + return + } + + log.Debug("sighdlr: wrote pid to %q", h.pidFile) +} + +// removePID removes the PID file, if any. +func (h *signalHandler) removePID() { + if h.pidFile == "" { + return + } + + err := os.Remove(h.pidFile) + if err != nil { + log.Error("sighdlr: removing pidfile: %s", err) + + return + } + + log.Debug("sighdlr: removed pid at %q", h.pidFile) +} diff --git a/internal/next/configmgr/config.go b/internal/next/configmgr/config.go new file mode 100644 index 00000000..5d67a372 --- /dev/null +++ b/internal/next/configmgr/config.go @@ -0,0 +1,138 @@ +package configmgr + +import ( + "fmt" + "net/netip" + + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/timeutil" +) + +// Configuration Structures + +// config is the top-level on-disk configuration structure. +type config struct { + DNS *dnsConfig `yaml:"dns"` + HTTP *httpConfig `yaml:"http"` + Log *logConfig `yaml:"log"` + // TODO(a.garipov): Use. + SchemaVersion int `yaml:"schema_version"` +} + +const errNoConf errors.Error = "configuration not found" + +// validate returns an error if the configuration structure is invalid. +func (c *config) validate() (err error) { + if c == nil { + return errNoConf + } + + // TODO(a.garipov): Add more validations. + + // Keep this in the same order as the fields in the config. + validators := []struct { + validate func() (err error) + name string + }{{ + validate: c.DNS.validate, + name: "dns", + }, { + validate: c.HTTP.validate, + name: "http", + }, { + validate: c.Log.validate, + name: "log", + }} + + for _, v := range validators { + err = v.validate() + if err != nil { + return fmt.Errorf("%s: %w", v.name, err) + } + } + + return nil +} + +// dnsConfig is the on-disk DNS configuration. +type dnsConfig struct { + Addresses []netip.AddrPort `yaml:"addresses"` + BootstrapDNS []string `yaml:"bootstrap_dns"` + UpstreamDNS []string `yaml:"upstream_dns"` + DNS64Prefixes []netip.Prefix `yaml:"dns64_prefixes"` + UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"` + BootstrapPreferIPv6 bool `yaml:"bootstrap_prefer_ipv6"` + UseDNS64 bool `yaml:"use_dns64"` +} + +// validate returns an error if the DNS configuration structure is invalid. +// +// TODO(a.garipov): Add more validations. +func (c *dnsConfig) validate() (err error) { + // TODO(a.garipov): Add more validations. + switch { + case c == nil: + return errNoConf + case c.UpstreamTimeout.Duration <= 0: + return newMustBePositiveError("upstream_timeout", c.UpstreamTimeout) + default: + return nil + } +} + +// httpConfig is the on-disk web API configuration. +type httpConfig struct { + Pprof *httpPprofConfig `yaml:"pprof"` + + // TODO(a.garipov): Document the configuration change. + Addresses []netip.AddrPort `yaml:"addresses"` + SecureAddresses []netip.AddrPort `yaml:"secure_addresses"` + Timeout timeutil.Duration `yaml:"timeout"` + ForceHTTPS bool `yaml:"force_https"` +} + +// validate returns an error if the HTTP configuration structure is invalid. +// +// TODO(a.garipov): Add more validations. +func (c *httpConfig) validate() (err error) { + switch { + case c == nil: + return errNoConf + case c.Timeout.Duration <= 0: + return newMustBePositiveError("timeout", c.Timeout) + default: + return c.Pprof.validate() + } +} + +// httpPprofConfig is the on-disk pprof configuration. +type httpPprofConfig struct { + Port uint16 `yaml:"port"` + Enabled bool `yaml:"enabled"` +} + +// validate returns an error if the pprof configuration structure is invalid. +func (c *httpPprofConfig) validate() (err error) { + if c == nil { + return errNoConf + } + + return nil +} + +// logConfig is the on-disk web API configuration. +type logConfig struct { + // TODO(a.garipov): Use. + Verbose bool `yaml:"verbose"` +} + +// validate returns an error if the HTTP configuration structure is invalid. +// +// TODO(a.garipov): Add more validations. +func (c *logConfig) validate() (err error) { + if c == nil { + return errNoConf + } + + return nil +} diff --git a/internal/next/configmgr/configmgr.go b/internal/next/configmgr/configmgr.go new file mode 100644 index 00000000..a361c87d --- /dev/null +++ b/internal/next/configmgr/configmgr.go @@ -0,0 +1,301 @@ +// Package configmgr defines the AdGuard Home on-disk configuration entities and +// configuration manager. +// +// TODO(a.garipov): Add tests. +package configmgr + +import ( + "context" + "fmt" + "io/fs" + "net/netip" + "os" + "sync" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" + "github.com/AdguardTeam/AdGuardHome/internal/next/websvc" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/timeutil" + "github.com/google/renameio/v2/maybe" + "golang.org/x/exp/slices" + "gopkg.in/yaml.v3" +) + +// Configuration Manager + +// Manager handles full and partial changes in the configuration, persisting +// them to disk if necessary. +// +// TODO(a.garipov): Support missing configs and default values. +type Manager struct { + // updMu makes sure that at most one reconfiguration is performed at a time. + // updMu protects all fields below. + updMu *sync.RWMutex + + // dns is the DNS service. + dns *dnssvc.Service + + // Web is the Web API service. + web *websvc.Service + + // current is the current configuration. + current *config + + // fileName is the name of the configuration file. + fileName string +} + +// Validate returns an error if the configuration file with the given name does +// not exist or is invalid. +func Validate(fileName string) (err error) { + conf, err := read(fileName) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return err + } + + // Don't wrap the error, because it's informative enough as is. + return conf.validate() +} + +// Config contains the configuration parameters for the configuration manager. +type Config struct { + // Frontend is the filesystem with the frontend files. + Frontend fs.FS + + // WebAddr is the initial or override address for the Web UI. It is not + // written to the configuration file. + WebAddr netip.AddrPort + + // Start is the time of start of AdGuard Home. + Start time.Time + + // FileName is the path to the configuration file. + FileName string +} + +// New creates a new *Manager that persists changes to the file pointed to by +// c.FileName. It reads the configuration file and populates the service +// fields. c must not be nil. +func New(ctx context.Context, c *Config) (m *Manager, err error) { + conf, err := read(c.FileName) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, err + } + + err = conf.validate() + if err != nil { + return nil, fmt.Errorf("validating config: %w", err) + } + + m = &Manager{ + updMu: &sync.RWMutex{}, + current: conf, + fileName: c.FileName, + } + + err = m.assemble(ctx, conf, c.Frontend, c.WebAddr, c.Start) + if err != nil { + return nil, fmt.Errorf("creating config manager: %w", err) + } + + return m, nil +} + +// read reads and decodes configuration from the provided filename. +func read(fileName string) (conf *config, err error) { + defer func() { err = errors.Annotate(err, "reading config: %w") }() + + conf = &config{} + f, err := os.Open(fileName) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, err + } + defer func() { err = errors.WithDeferred(err, f.Close()) }() + + err = yaml.NewDecoder(f).Decode(conf) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, err + } + + return conf, nil +} + +// assemble creates all services and puts them into the corresponding fields. +// The fields of conf must not be modified after calling assemble. +func (m *Manager) assemble( + ctx context.Context, + conf *config, + frontend fs.FS, + webAddr netip.AddrPort, + start time.Time, +) (err error) { + dnsConf := &dnssvc.Config{ + Addresses: conf.DNS.Addresses, + BootstrapServers: conf.DNS.BootstrapDNS, + UpstreamServers: conf.DNS.UpstreamDNS, + DNS64Prefixes: conf.DNS.DNS64Prefixes, + UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration, + BootstrapPreferIPv6: conf.DNS.BootstrapPreferIPv6, + UseDNS64: conf.DNS.UseDNS64, + } + err = m.updateDNS(ctx, dnsConf) + if err != nil { + return fmt.Errorf("assembling dnssvc: %w", err) + } + + webSvcConf := &websvc.Config{ + Pprof: &websvc.PprofConfig{ + Port: conf.HTTP.Pprof.Port, + Enabled: conf.HTTP.Pprof.Enabled, + }, + ConfigManager: m, + Frontend: frontend, + // TODO(a.garipov): Fill from config file. + TLS: nil, + Start: start, + Addresses: conf.HTTP.Addresses, + SecureAddresses: conf.HTTP.SecureAddresses, + OverrideAddress: webAddr, + Timeout: conf.HTTP.Timeout.Duration, + ForceHTTPS: conf.HTTP.ForceHTTPS, + } + + err = m.updateWeb(ctx, webSvcConf) + if err != nil { + return fmt.Errorf("assembling websvc: %w", err) + } + + return nil +} + +// write writes the current configuration to disk. +func (m *Manager) write() (err error) { + b, err := yaml.Marshal(m.current) + if err != nil { + return fmt.Errorf("encoding: %w", err) + } + + err = maybe.WriteFile(m.fileName, b, 0o644) + if err != nil { + return fmt.Errorf("writing: %w", err) + } + + log.Info("configmgr: written to %q", m.fileName) + + return nil +} + +// DNS returns the current DNS service. It is safe for concurrent use. +func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) { + m.updMu.RLock() + defer m.updMu.RUnlock() + + return m.dns +} + +// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The +// fields of c must not be modified after calling UpdateDNS. +func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) { + m.updMu.Lock() + defer m.updMu.Unlock() + + // TODO(a.garipov): Update and write the configuration file. Return an + // error if something went wrong. + + err = m.updateDNS(ctx, c) + if err != nil { + return fmt.Errorf("reassembling dnssvc: %w", err) + } + + m.updateCurrentDNS(c) + + return m.write() +} + +// updateDNS recreates the DNS service. m.updMu is expected to be locked. +func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) { + if prev := m.dns; prev != nil { + err = prev.Shutdown(ctx) + if err != nil { + return fmt.Errorf("shutting down dns svc: %w", err) + } + } + + svc, err := dnssvc.New(c) + if err != nil { + return fmt.Errorf("creating dns svc: %w", err) + } + + m.dns = svc + + return nil +} + +// updateCurrentDNS updates the DNS configuration in the current config. +func (m *Manager) updateCurrentDNS(c *dnssvc.Config) { + m.current.DNS.Addresses = slices.Clone(c.Addresses) + m.current.DNS.BootstrapDNS = slices.Clone(c.BootstrapServers) + m.current.DNS.UpstreamDNS = slices.Clone(c.UpstreamServers) + m.current.DNS.DNS64Prefixes = slices.Clone(c.DNS64Prefixes) + m.current.DNS.UpstreamTimeout = timeutil.Duration{Duration: c.UpstreamTimeout} + m.current.DNS.BootstrapPreferIPv6 = c.BootstrapPreferIPv6 + m.current.DNS.UseDNS64 = c.UseDNS64 +} + +// Web returns the current web service. It is safe for concurrent use. +func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) { + m.updMu.RLock() + defer m.updMu.RUnlock() + + return m.web +} + +// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The +// fields of c must not be modified after calling UpdateWeb. +func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) { + m.updMu.Lock() + defer m.updMu.Unlock() + + err = m.updateWeb(ctx, c) + if err != nil { + return fmt.Errorf("reassembling websvc: %w", err) + } + + m.updateCurrentWeb(c) + + return m.write() +} + +// updateWeb recreates the web service. m.upd is expected to be locked. +func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) { + if prev := m.web; prev != nil { + err = prev.Shutdown(ctx) + if err != nil { + return fmt.Errorf("shutting down web svc: %w", err) + } + } + + m.web, err = websvc.New(c) + if err != nil { + return fmt.Errorf("creating web svc: %w", err) + } + + return nil +} + +// updateCurrentWeb updates the web configuration in the current config. +func (m *Manager) updateCurrentWeb(c *websvc.Config) { + // TODO(a.garipov): Update pprof from API? + + m.current.HTTP.Addresses = slices.Clone(c.Addresses) + m.current.HTTP.SecureAddresses = slices.Clone(c.SecureAddresses) + m.current.HTTP.Timeout = timeutil.Duration{Duration: c.Timeout} + m.current.HTTP.ForceHTTPS = c.ForceHTTPS +} diff --git a/internal/next/configmgr/error.go b/internal/next/configmgr/error.go new file mode 100644 index 00000000..b4ffb92b --- /dev/null +++ b/internal/next/configmgr/error.go @@ -0,0 +1,27 @@ +package configmgr + +import ( + "fmt" + + "github.com/AdguardTeam/golibs/timeutil" + "golang.org/x/exp/constraints" +) + +// numberOrDuration is the constraint for integer types along with +// timeutil.Duration. +type numberOrDuration interface { + constraints.Integer | timeutil.Duration +} + +// newMustBePositiveError returns an error about the value that must be positive +// but isn't. prop is the name of the property to mention in the error message. +// +// TODO(a.garipov): Consider moving such helpers to golibs and use in AdGuardDNS +// as well. +func newMustBePositiveError[T numberOrDuration](prop string, v T) (err error) { + if s, ok := any(v).(fmt.Stringer); ok { + return fmt.Errorf("%s must be positive, got %s", prop, s) + } + + return fmt.Errorf("%s must be positive, got %d", prop, v) +} diff --git a/internal/next/dnssvc/config.go b/internal/next/dnssvc/config.go new file mode 100644 index 00000000..57818c20 --- /dev/null +++ b/internal/next/dnssvc/config.go @@ -0,0 +1,35 @@ +package dnssvc + +import ( + "net/netip" + "time" +) + +// Config is the AdGuard Home DNS service configuration structure. +// +// TODO(a.garipov): Add timeout for incoming requests. +type Config struct { + // Addresses are the addresses on which to serve plain DNS queries. + Addresses []netip.AddrPort + + // BootstrapServers are the addresses of DNS servers used for bootstrapping + // the upstream DNS server addresses. + BootstrapServers []string + + // UpstreamServers are the upstream DNS server addresses to use. + UpstreamServers []string + + // DNS64Prefixes is a slice of NAT64 prefixes to be used for DNS64. See + // also [Config.UseDNS64]. + DNS64Prefixes []netip.Prefix + + // UpstreamTimeout is the timeout for upstream requests. + UpstreamTimeout time.Duration + + // BootstrapPreferIPv6, if true, instructs the bootstrapper to prefer IPv6 + // addresses to IPv4 ones when bootstrapping. + BootstrapPreferIPv6 bool + + // UseDNS64, if true, enables DNS64 protection for incoming requests. + UseDNS64 bool +} diff --git a/internal/next/dnssvc/dnssvc.go b/internal/next/dnssvc/dnssvc.go new file mode 100644 index 00000000..e0056dcb --- /dev/null +++ b/internal/next/dnssvc/dnssvc.go @@ -0,0 +1,232 @@ +// Package dnssvc contains the AdGuard Home DNS service. +// +// TODO(a.garipov): Define, if all methods of a *Service should work with a nil +// receiver. +package dnssvc + +import ( + "context" + "fmt" + "net" + "net/netip" + "sync/atomic" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + + // TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes + // and replacement of module dnsproxy. + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/AdguardTeam/golibs/errors" +) + +// Service is the AdGuard Home DNS service. A nil *Service is a valid +// [agh.Service] that does nothing. +// +// TODO(a.garipov): Consider saving a [*proxy.Config] instance for those +// fields that are only used in [New] and [Service.Config]. +type Service struct { + proxy *proxy.Proxy + bootstraps []string + bootstrapResolvers []*upstream.UpstreamResolver + upstreams []string + dns64Prefixes []netip.Prefix + upsTimeout time.Duration + running atomic.Bool + bootstrapPreferIPv6 bool + useDNS64 bool +} + +// New returns a new properly initialized *Service. If c is nil, svc is a nil +// *Service that does nothing. The fields of c must not be modified after +// calling New. +func New(c *Config) (svc *Service, err error) { + if c == nil { + return nil, nil + } + + svc = &Service{ + bootstraps: c.BootstrapServers, + upstreams: c.UpstreamServers, + dns64Prefixes: c.DNS64Prefixes, + upsTimeout: c.UpstreamTimeout, + bootstrapPreferIPv6: c.BootstrapPreferIPv6, + useDNS64: c.UseDNS64, + } + + upstreams, resolvers, err := addressesToUpstreams( + c.UpstreamServers, + c.BootstrapServers, + c.UpstreamTimeout, + c.BootstrapPreferIPv6, + ) + if err != nil { + return nil, fmt.Errorf("converting upstreams: %w", err) + } + + svc.bootstrapResolvers = resolvers + svc.proxy = &proxy.Proxy{ + Config: proxy.Config{ + UDPListenAddr: udpAddrs(c.Addresses), + TCPListenAddr: tcpAddrs(c.Addresses), + UpstreamConfig: &proxy.UpstreamConfig{ + Upstreams: upstreams, + }, + UseDNS64: c.UseDNS64, + DNS64Prefs: c.DNS64Prefixes, + }, + } + + err = svc.proxy.Init() + if err != nil { + return nil, fmt.Errorf("proxy: %w", err) + } + + return svc, nil +} + +// addressesToUpstreams is a wrapper around [upstream.AddressToUpstream]. It +// accepts a slice of addresses and other upstream parameters, and returns a +// slice of upstreams. +func addressesToUpstreams( + upsStrs []string, + bootstraps []string, + timeout time.Duration, + preferIPv6 bool, +) (upstreams []upstream.Upstream, boots []*upstream.UpstreamResolver, err error) { + opts := &upstream.Options{ + Timeout: timeout, + PreferIPv6: preferIPv6, + } + + boots, err = aghnet.ParseBootstraps(bootstraps, opts) + if err != nil { + // Don't wrap the error, since it's informative enough as is. + return nil, nil, err + } + + // TODO(e.burkov): Add system hosts resolver here. + var bootstrap upstream.ParallelResolver + for _, r := range boots { + bootstrap = append(bootstrap, r) + } + + upstreams = make([]upstream.Upstream, len(upsStrs)) + for i, upsStr := range upsStrs { + upstreams[i], err = upstream.AddressToUpstream(upsStr, &upstream.Options{ + Bootstrap: bootstrap, + Timeout: timeout, + PreferIPv6: preferIPv6, + }) + if err != nil { + return nil, boots, fmt.Errorf("upstream at index %d: %w", i, err) + } + } + + return upstreams, boots, nil +} + +// tcpAddrs converts []netip.AddrPort into []*net.TCPAddr. +func tcpAddrs(addrPorts []netip.AddrPort) (tcpAddrs []*net.TCPAddr) { + if addrPorts == nil { + return nil + } + + tcpAddrs = make([]*net.TCPAddr, len(addrPorts)) + for i, a := range addrPorts { + tcpAddrs[i] = net.TCPAddrFromAddrPort(a) + } + + return tcpAddrs +} + +// udpAddrs converts []netip.AddrPort into []*net.UDPAddr. +func udpAddrs(addrPorts []netip.AddrPort) (udpAddrs []*net.UDPAddr) { + if addrPorts == nil { + return nil + } + + udpAddrs = make([]*net.UDPAddr, len(addrPorts)) + for i, a := range addrPorts { + udpAddrs[i] = net.UDPAddrFromAddrPort(a) + } + + return udpAddrs +} + +// type check +var _ agh.Service = (*Service)(nil) + +// Start implements the [agh.Service] interface for *Service. svc may be nil. +// After Start exits, all DNS servers have tried to start, but there is no +// guarantee that they did. Errors from the servers are written to the log. +func (svc *Service) Start() (err error) { + if svc == nil { + return nil + } + + defer func() { + // TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to + // tell when all servers are actually up, so at best this is merely an + // assumption. + svc.running.Store(err == nil) + }() + + return svc.proxy.Start() +} + +// Shutdown implements the [agh.Service] interface for *Service. svc may be +// nil. +func (svc *Service) Shutdown(ctx context.Context) (err error) { + if svc == nil { + return nil + } + + errs := []error{ + svc.proxy.Stop(), + } + + for _, b := range svc.bootstrapResolvers { + errs = append(errs, errors.Annotate(b.Close(), "closing bootstrap %s: %w", b.Address())) + } + + return errors.Join(errs...) +} + +// Config returns the current configuration of the web service. Config must not +// be called simultaneously with Start. If svc was initialized with ":0" +// addresses, addrs will not return the actual bound ports until Start is +// finished. +func (svc *Service) Config() (c *Config) { + // TODO(a.garipov): Do we need to get the TCP addresses separately? + + var addrs []netip.AddrPort + if svc.running.Load() { + udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP) + addrs = make([]netip.AddrPort, len(udpAddrs)) + for i, a := range udpAddrs { + addrs[i] = a.(*net.UDPAddr).AddrPort() + } + } else { + conf := svc.proxy.Config + udpAddrs := conf.UDPListenAddr + addrs = make([]netip.AddrPort, len(udpAddrs)) + for i, a := range udpAddrs { + addrs[i] = a.AddrPort() + } + } + + c = &Config{ + Addresses: addrs, + BootstrapServers: svc.bootstraps, + UpstreamServers: svc.upstreams, + DNS64Prefixes: svc.dns64Prefixes, + UpstreamTimeout: svc.upsTimeout, + BootstrapPreferIPv6: svc.bootstrapPreferIPv6, + UseDNS64: svc.useDNS64, + } + + return c +} diff --git a/internal/next/dnssvc/dnssvc_test.go b/internal/next/dnssvc/dnssvc_test.go new file mode 100644 index 00000000..48f49b8d --- /dev/null +++ b/internal/next/dnssvc/dnssvc_test.go @@ -0,0 +1,125 @@ +package dnssvc_test + +import ( + "context" + "net/netip" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" + "github.com/AdguardTeam/golibs/testutil" + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + testutil.DiscardLogOutput(m) +} + +// testTimeout is the common timeout for tests. +const testTimeout = 1 * time.Second + +func TestService(t *testing.T) { + const ( + listenAddr = "127.0.0.1:0" + bootstrapAddr = "127.0.0.1:0" + upstreamAddr = "upstream.example" + ) + + upstreamErrCh := make(chan error, 1) + upstreamStartedCh := make(chan struct{}) + upstreamSrv := &dns.Server{ + Addr: bootstrapAddr, + Net: "udp", + Handler: dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { + pt := testutil.PanicT{} + + resp := (&dns.Msg{}).SetReply(req) + resp.Answer = append(resp.Answer, &dns.A{ + Hdr: dns.RR_Header{}, + A: netip.MustParseAddrPort(bootstrapAddr).Addr().AsSlice(), + }) + + writeErr := w.WriteMsg(resp) + require.NoError(pt, writeErr) + }), + NotifyStartedFunc: func() { close(upstreamStartedCh) }, + } + + go func() { + listenErr := upstreamSrv.ListenAndServe() + if listenErr != nil { + // Log these immediately to see what happens. + t.Logf("upstream listen error: %s", listenErr) + } + + upstreamErrCh <- listenErr + }() + + _, _ = testutil.RequireReceive(t, upstreamStartedCh, testTimeout) + + c := &dnssvc.Config{ + Addresses: []netip.AddrPort{netip.MustParseAddrPort(listenAddr)}, + BootstrapServers: []string{upstreamSrv.PacketConn.LocalAddr().String()}, + UpstreamServers: []string{upstreamAddr}, + DNS64Prefixes: nil, + UpstreamTimeout: testTimeout, + BootstrapPreferIPv6: false, + UseDNS64: false, + } + + svc, err := dnssvc.New(c) + require.NoError(t, err) + + err = svc.Start() + require.NoError(t, err) + + gotConf := svc.Config() + require.NotNil(t, gotConf) + require.Len(t, gotConf.Addresses, 1) + + addr := gotConf.Addresses[0] + + t.Run("dns", func(t *testing.T) { + req := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{ + Name: "example.com.", + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + }}, + } + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + cli := &dns.Client{} + + var resp *dns.Msg + require.Eventually(t, func() (ok bool) { + var excErr error + resp, _, excErr = cli.ExchangeContext(ctx, req, addr.String()) + + return excErr == nil + }, testTimeout, testTimeout/10) + + assert.NotNil(t, resp) + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + err = svc.Shutdown(ctx) + require.NoError(t, err) + + err = upstreamSrv.Shutdown() + require.NoError(t, err) + + err, ok := testutil.RequireReceive(t, upstreamErrCh, testTimeout) + require.True(t, ok) + require.NoError(t, err) +} diff --git a/internal/next/websvc/config.go b/internal/next/websvc/config.go new file mode 100644 index 00000000..36a145c5 --- /dev/null +++ b/internal/next/websvc/config.go @@ -0,0 +1,79 @@ +package websvc + +import ( + "crypto/tls" + "io/fs" + "net/netip" + "time" +) + +// Config is the AdGuard Home web service configuration structure. +type Config struct { + // Pprof is the configuration for the pprof debug API. It must not be nil. + Pprof *PprofConfig + + // ConfigManager is used to show information about services as well as + // dynamically reconfigure them. + ConfigManager ConfigManager + + // Frontend is the filesystem with the frontend and other statically + // compiled files. + Frontend fs.FS + + // TLS is the optional TLS configuration. If TLS is not nil, + // SecureAddresses must not be empty. + TLS *tls.Config + + // Start is the time of start of AdGuard Home. + Start time.Time + + // OverrideAddress is the initial or override address for the HTTP API. If + // set, it is used instead of [Addresses] and [SecureAddresses]. + OverrideAddress netip.AddrPort + + // Addresses are the addresses on which to serve the plain HTTP API. + Addresses []netip.AddrPort + + // SecureAddresses are the addresses on which to serve the HTTPS API. If + // SecureAddresses is not empty, TLS must not be nil. + SecureAddresses []netip.AddrPort + + // Timeout is the timeout for all server operations. + Timeout time.Duration + + // ForceHTTPS tells if all requests to Addresses should be redirected to a + // secure address instead. + // + // TODO(a.garipov): Use; define rules, which address to redirect to. + ForceHTTPS bool +} + +// PprofConfig is the configuration for the pprof debug API. +type PprofConfig struct { + Port uint16 `yaml:"port"` + Enabled bool `yaml:"enabled"` +} + +// Config returns the current configuration of the web service. Config must not +// be called simultaneously with Start. If svc was initialized with ":0" +// addresses, addrs will not return the actual bound ports until Start is +// finished. +func (svc *Service) Config() (c *Config) { + c = &Config{ + Pprof: &PprofConfig{ + Port: svc.pprofPort, + Enabled: svc.pprof != nil, + }, + ConfigManager: svc.confMgr, + TLS: svc.tls, + // Leave Addresses and SecureAddresses empty and get the actual + // addresses that include the :0 ones later. + Start: svc.start, + Timeout: svc.timeout, + ForceHTTPS: svc.forceHTTPS, + } + + c.Addresses, c.SecureAddresses = svc.addrs() + + return c +} diff --git a/internal/next/websvc/dns.go b/internal/next/websvc/dns.go new file mode 100644 index 00000000..39f05d22 --- /dev/null +++ b/internal/next/websvc/dns.go @@ -0,0 +1,97 @@ +package websvc + +import ( + "encoding/json" + "fmt" + "net/http" + "net/netip" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" +) + +// DNS Settings Handlers + +// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns +// HTTP API. +type ReqPatchSettingsDNS struct { + // TODO(a.garipov): Add more as we go. + + Addresses []netip.AddrPort `json:"addresses"` + BootstrapServers []string `json:"bootstrap_servers"` + UpstreamServers []string `json:"upstream_servers"` + DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"` + UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"` + BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"` + UseDNS64 bool `json:"use_dns64"` +} + +// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the +// DnsSettings object in the OpenAPI specification. +type HTTPAPIDNSSettings struct { + // TODO(a.garipov): Add more as we go. + + Addresses []netip.AddrPort `json:"addresses"` + BootstrapServers []string `json:"bootstrap_servers"` + UpstreamServers []string `json:"upstream_servers"` + DNS64Prefixes []netip.Prefix `json:"dns64_prefixes"` + UpstreamTimeout aghhttp.JSONDuration `json:"upstream_timeout"` + BootstrapPreferIPv6 bool `json:"bootstrap_prefer_ipv6"` + UseDNS64 bool `json:"use_dns64"` +} + +// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP +// API. +func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) { + req := &ReqPatchSettingsDNS{ + Addresses: []netip.AddrPort{}, + BootstrapServers: []string{}, + UpstreamServers: []string{}, + } + + // TODO(a.garipov): Validate nulls and proper JSON patch. + + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err)) + + return + } + + newConf := &dnssvc.Config{ + Addresses: req.Addresses, + BootstrapServers: req.BootstrapServers, + UpstreamServers: req.UpstreamServers, + DNS64Prefixes: req.DNS64Prefixes, + UpstreamTimeout: time.Duration(req.UpstreamTimeout), + BootstrapPreferIPv6: req.BootstrapPreferIPv6, + UseDNS64: req.UseDNS64, + } + + ctx := r.Context() + err = svc.confMgr.UpdateDNS(ctx, newConf) + if err != nil { + aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("updating: %w", err)) + + return + } + + newSvc := svc.confMgr.DNS() + err = newSvc.Start() + if err != nil { + aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("starting new service: %w", err)) + + return + } + + aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIDNSSettings{ + Addresses: newConf.Addresses, + BootstrapServers: newConf.BootstrapServers, + UpstreamServers: newConf.UpstreamServers, + DNS64Prefixes: newConf.DNS64Prefixes, + UpstreamTimeout: aghhttp.JSONDuration(newConf.UpstreamTimeout), + BootstrapPreferIPv6: newConf.BootstrapPreferIPv6, + UseDNS64: newConf.UseDNS64, + }) +} diff --git a/internal/next/websvc/dns_test.go b/internal/next/websvc/dns_test.go new file mode 100644 index 00000000..5f6e3d44 --- /dev/null +++ b/internal/next/websvc/dns_test.go @@ -0,0 +1,75 @@ +package websvc_test + +import ( + "context" + "encoding/json" + "net/http" + "net/netip" + "net/url" + "sync/atomic" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" + "github.com/AdguardTeam/AdGuardHome/internal/next/websvc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestService_HandlePatchSettingsDNS(t *testing.T) { + wantDNS := &websvc.HTTPAPIDNSSettings{ + Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")}, + BootstrapServers: []string{"1.0.0.1"}, + UpstreamServers: []string{"1.1.1.1"}, + DNS64Prefixes: []netip.Prefix{netip.MustParsePrefix("1234::/64")}, + UpstreamTimeout: aghhttp.JSONDuration(2 * time.Second), + BootstrapPreferIPv6: true, + UseDNS64: true, + } + + var started atomic.Bool + confMgr := newConfigManager() + confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) { + return &aghtest.ServiceWithConfig[*dnssvc.Config]{ + OnStart: func() (err error) { + started.Store(true) + + return nil + }, + OnShutdown: func(_ context.Context) (err error) { panic("not implemented") }, + OnConfig: func() (c *dnssvc.Config) { panic("not implemented") }, + } + } + confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) { + return nil + } + + _, addr := newTestServer(t, confMgr) + u := &url.URL{ + Scheme: "http", + Host: addr.String(), + Path: websvc.PathV1SettingsDNS, + } + + req := jobj{ + "addresses": wantDNS.Addresses, + "bootstrap_servers": wantDNS.BootstrapServers, + "upstream_servers": wantDNS.UpstreamServers, + "dns64_prefixes": wantDNS.DNS64Prefixes, + "upstream_timeout": wantDNS.UpstreamTimeout, + "bootstrap_prefer_ipv6": wantDNS.BootstrapPreferIPv6, + "use_dns64": wantDNS.UseDNS64, + } + + respBody := httpPatch(t, u, req, http.StatusOK) + resp := &websvc.HTTPAPIDNSSettings{} + err := json.Unmarshal(respBody, resp) + require.NoError(t, err) + + assert.True(t, started.Load()) + assert.Equal(t, wantDNS, resp) + assert.Equal(t, wantDNS, resp) +} diff --git a/internal/next/websvc/http.go b/internal/next/websvc/http.go new file mode 100644 index 00000000..db32372d --- /dev/null +++ b/internal/next/websvc/http.go @@ -0,0 +1,123 @@ +package websvc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/netip" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/golibs/log" +) + +// HTTP Settings Handlers + +// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http +// HTTP API. +type ReqPatchSettingsHTTP struct { + // TODO(a.garipov): Add more as we go. + // + // TODO(a.garipov): Add wait time. + + Addresses []netip.AddrPort `json:"addresses"` + SecureAddresses []netip.AddrPort `json:"secure_addresses"` + Timeout aghhttp.JSONDuration `json:"timeout"` +} + +// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the +// HttpSettings object in the OpenAPI specification. +type HTTPAPIHTTPSettings struct { + // TODO(a.garipov): Add more as we go. + + Addresses []netip.AddrPort `json:"addresses"` + SecureAddresses []netip.AddrPort `json:"secure_addresses"` + Timeout aghhttp.JSONDuration `json:"timeout"` + ForceHTTPS bool `json:"force_https"` +} + +// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http +// HTTP API. +func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) { + req := &ReqPatchSettingsHTTP{} + + // TODO(a.garipov): Validate nulls and proper JSON patch. + + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("decoding: %w", err)) + + return + } + + newConf := &Config{ + Pprof: &PprofConfig{ + Port: svc.pprofPort, + Enabled: svc.pprof != nil, + }, + ConfigManager: svc.confMgr, + Frontend: svc.frontend, + TLS: svc.tls, + Addresses: req.Addresses, + SecureAddresses: req.SecureAddresses, + Timeout: time.Duration(req.Timeout), + ForceHTTPS: svc.forceHTTPS, + } + + aghhttp.WriteJSONResponseOK(w, r, &HTTPAPIHTTPSettings{ + Addresses: newConf.Addresses, + SecureAddresses: newConf.SecureAddresses, + Timeout: aghhttp.JSONDuration(newConf.Timeout), + ForceHTTPS: newConf.ForceHTTPS, + }) + + cancelUpd := func() {} + updCtx := context.Background() + + ctx := r.Context() + if deadline, ok := ctx.Deadline(); ok { + updCtx, cancelUpd = context.WithDeadline(updCtx, deadline) + } + + // Launch the new HTTP service in a separate goroutine to let this handler + // finish and thus, this server to shutdown. + go svc.relaunch(updCtx, cancelUpd, newConf) +} + +// relaunch updates the web service in the configuration manager and starts it. +// It is intended to be used as a goroutine. +func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, newConf *Config) { + defer log.OnPanic("websvc: relaunching") + + defer cancel() + + err := svc.confMgr.UpdateWeb(ctx, newConf) + if err != nil { + log.Error("websvc: updating web: %s", err) + + return + } + + // TODO(a.garipov): Consider better ways to do this. + const maxUpdDur = 5 * time.Second + updStart := time.Now() + var newSvc agh.ServiceWithConfig[*Config] + for newSvc = svc.confMgr.Web(); newSvc == svc; { + if time.Since(updStart) >= maxUpdDur { + log.Error("websvc: failed to update svc after %s", maxUpdDur) + + return + } + + log.Debug("websvc: waiting for new websvc to be configured") + + time.Sleep(100 * time.Millisecond) + } + + err = newSvc.Start() + if err != nil { + log.Error("websvc: new svc failed to start with error: %s", err) + } +} diff --git a/internal/next/websvc/http_test.go b/internal/next/websvc/http_test.go new file mode 100644 index 00000000..3168ec03 --- /dev/null +++ b/internal/next/websvc/http_test.go @@ -0,0 +1,66 @@ +package websvc_test + +import ( + "context" + "crypto/tls" + "encoding/json" + "net/http" + "net/netip" + "net/url" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/websvc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestService_HandlePatchSettingsHTTP(t *testing.T) { + wantWeb := &websvc.HTTPAPIHTTPSettings{ + Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")}, + SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")}, + Timeout: aghhttp.JSONDuration(10 * time.Second), + ForceHTTPS: false, + } + + svc, err := websvc.New(&websvc.Config{ + Pprof: &websvc.PprofConfig{ + Enabled: false, + }, + TLS: &tls.Config{ + Certificates: []tls.Certificate{{}}, + }, + Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")}, + SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")}, + Timeout: 5 * time.Second, + ForceHTTPS: true, + }) + require.NoError(t, err) + + confMgr := newConfigManager() + confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) { return svc } + confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) { return nil } + + _, addr := newTestServer(t, confMgr) + u := &url.URL{ + Scheme: "http", + Host: addr.String(), + Path: websvc.PathV1SettingsHTTP, + } + + req := jobj{ + "addresses": wantWeb.Addresses, + "secure_addresses": wantWeb.SecureAddresses, + "timeout": wantWeb.Timeout, + "force_https": wantWeb.ForceHTTPS, + } + + respBody := httpPatch(t, u, req, http.StatusOK) + resp := &websvc.HTTPAPIHTTPSettings{} + err = json.Unmarshal(respBody, resp) + require.NoError(t, err) + + assert.Equal(t, wantWeb, resp) +} diff --git a/internal/next/websvc/middleware.go b/internal/next/websvc/middleware.go new file mode 100644 index 00000000..8dc66b34 --- /dev/null +++ b/internal/next/websvc/middleware.go @@ -0,0 +1,38 @@ +package websvc + +import ( + "net/http" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/golibs/httphdr" + "github.com/AdguardTeam/golibs/log" +) + +// Middlewares + +// jsonMw sets the content type of the response to application/json. +func jsonMw(h http.Handler) (wrapped http.HandlerFunc) { + f := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON) + + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(f) +} + +// logMw logs the queries with level debug. +func logMw(h http.Handler) (wrapped http.HandlerFunc) { + f := func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + m, u := r.Method, r.RequestURI + + log.Debug("websvc: %s %s started", m, u) + defer func() { log.Debug("websvc: %s %s finished in %s", m, u, time.Since(start)) }() + + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(f) +} diff --git a/internal/next/websvc/path.go b/internal/next/websvc/path.go new file mode 100644 index 00000000..95be8204 --- /dev/null +++ b/internal/next/websvc/path.go @@ -0,0 +1,14 @@ +package websvc + +// Path constants +const ( + PathRoot = "/" + PathFrontend = "/*filepath" + + PathHealthCheck = "/health-check" + + PathV1SettingsAll = "/api/v1/settings/all" + PathV1SettingsDNS = "/api/v1/settings/dns" + PathV1SettingsHTTP = "/api/v1/settings/http" + PathV1SystemInfo = "/api/v1/system/info" +) diff --git a/internal/next/websvc/settings.go b/internal/next/websvc/settings.go new file mode 100644 index 00000000..44364ca3 --- /dev/null +++ b/internal/next/websvc/settings.go @@ -0,0 +1,47 @@ +package websvc + +import ( + "net/http" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" +) + +// All Settings Handlers + +// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all +// HTTP API. +type RespGetV1SettingsAll struct { + // TODO(a.garipov): Add more as we go. + + DNS *HTTPAPIDNSSettings `json:"dns"` + HTTP *HTTPAPIHTTPSettings `json:"http"` +} + +// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP +// API. +func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) { + dnsSvc := svc.confMgr.DNS() + dnsConf := dnsSvc.Config() + + webSvc := svc.confMgr.Web() + httpConf := webSvc.Config() + + // TODO(a.garipov): Add all currently supported parameters. + aghhttp.WriteJSONResponseOK(w, r, &RespGetV1SettingsAll{ + DNS: &HTTPAPIDNSSettings{ + Addresses: dnsConf.Addresses, + BootstrapServers: dnsConf.BootstrapServers, + UpstreamServers: dnsConf.UpstreamServers, + DNS64Prefixes: dnsConf.DNS64Prefixes, + UpstreamTimeout: aghhttp.JSONDuration(dnsConf.UpstreamTimeout), + BootstrapPreferIPv6: dnsConf.BootstrapPreferIPv6, + UseDNS64: dnsConf.UseDNS64, + }, + HTTP: &HTTPAPIHTTPSettings{ + Addresses: httpConf.Addresses, + SecureAddresses: httpConf.SecureAddresses, + Timeout: aghhttp.JSONDuration(httpConf.Timeout), + ForceHTTPS: httpConf.ForceHTTPS, + }, + }) +} diff --git a/internal/next/websvc/settings_test.go b/internal/next/websvc/settings_test.go new file mode 100644 index 00000000..f07d8b3e --- /dev/null +++ b/internal/next/websvc/settings_test.go @@ -0,0 +1,84 @@ +package websvc_test + +import ( + "crypto/tls" + "encoding/json" + "net/http" + "net/netip" + "net/url" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" + "github.com/AdguardTeam/AdGuardHome/internal/next/websvc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestService_HandleGetSettingsAll(t *testing.T) { + // TODO(a.garipov): Add all currently supported parameters. + + wantDNS := &websvc.HTTPAPIDNSSettings{ + Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")}, + BootstrapServers: []string{"94.140.14.140", "94.140.14.141"}, + UpstreamServers: []string{"94.140.14.14", "1.1.1.1"}, + UpstreamTimeout: aghhttp.JSONDuration(1 * time.Second), + BootstrapPreferIPv6: true, + } + + wantWeb := &websvc.HTTPAPIHTTPSettings{ + Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")}, + SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")}, + Timeout: aghhttp.JSONDuration(5 * time.Second), + ForceHTTPS: true, + } + + confMgr := newConfigManager() + confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) { + c, err := dnssvc.New(&dnssvc.Config{ + Addresses: wantDNS.Addresses, + UpstreamServers: wantDNS.UpstreamServers, + BootstrapServers: wantDNS.BootstrapServers, + UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout), + BootstrapPreferIPv6: true, + }) + require.NoError(t, err) + + return c + } + + svc, err := websvc.New(&websvc.Config{ + Pprof: &websvc.PprofConfig{ + Enabled: false, + }, + TLS: &tls.Config{ + Certificates: []tls.Certificate{{}}, + }, + Addresses: wantWeb.Addresses, + SecureAddresses: wantWeb.SecureAddresses, + Timeout: time.Duration(wantWeb.Timeout), + ForceHTTPS: true, + }) + require.NoError(t, err) + + confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) { + return svc + } + + _, addr := newTestServer(t, confMgr) + u := &url.URL{ + Scheme: "http", + Host: addr.String(), + Path: websvc.PathV1SettingsAll, + } + + body := httpGet(t, u, http.StatusOK) + resp := &websvc.RespGetV1SettingsAll{} + err = json.Unmarshal(body, resp) + require.NoError(t, err) + + assert.Equal(t, wantDNS, resp.DNS) + assert.Equal(t, wantWeb, resp.HTTP) +} diff --git a/internal/next/websvc/system.go b/internal/next/websvc/system.go new file mode 100644 index 00000000..d4ca34ad --- /dev/null +++ b/internal/next/websvc/system.go @@ -0,0 +1,36 @@ +package websvc + +import ( + "net/http" + "runtime" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/AdGuardHome/internal/version" +) + +// System Handlers + +// RespGetV1SystemInfo describes the response of the GET /api/v1/system/info +// HTTP API. +type RespGetV1SystemInfo struct { + Arch string `json:"arch"` + Channel string `json:"channel"` + OS string `json:"os"` + NewVersion string `json:"new_version,omitempty"` + Start aghhttp.JSONTime `json:"start"` + Version string `json:"version"` +} + +// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP +// API. +func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) { + aghhttp.WriteJSONResponseOK(w, r, &RespGetV1SystemInfo{ + Arch: runtime.GOARCH, + Channel: version.Channel(), + OS: runtime.GOOS, + // TODO(a.garipov): Fill this when we have an updater. + NewVersion: "", + Start: aghhttp.JSONTime(svc.start), + Version: version.Version(), + }) +} diff --git a/internal/next/websvc/system_test.go b/internal/next/websvc/system_test.go new file mode 100644 index 00000000..acbdcba2 --- /dev/null +++ b/internal/next/websvc/system_test.go @@ -0,0 +1,37 @@ +package websvc_test + +import ( + "encoding/json" + "net/http" + "net/url" + "runtime" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/next/websvc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestService_handleGetV1SystemInfo(t *testing.T) { + confMgr := newConfigManager() + _, addr := newTestServer(t, confMgr) + u := &url.URL{ + Scheme: "http", + Host: addr.String(), + Path: websvc.PathV1SystemInfo, + } + + body := httpGet(t, u, http.StatusOK) + resp := &websvc.RespGetV1SystemInfo{} + err := json.Unmarshal(body, resp) + require.NoError(t, err) + + // TODO(a.garipov): Consider making version.Channel and version.Version + // testable and test these better. + assert.NotEmpty(t, resp.Channel) + + assert.Equal(t, resp.Arch, runtime.GOARCH) + assert.Equal(t, resp.OS, runtime.GOOS) + assert.Equal(t, testStart, time.Time(resp.Start)) +} diff --git a/internal/next/websvc/waitlistener.go b/internal/next/websvc/waitlistener.go new file mode 100644 index 00000000..8ab56269 --- /dev/null +++ b/internal/next/websvc/waitlistener.go @@ -0,0 +1,31 @@ +package websvc + +import ( + "net" + "sync" +) + +// Wait Listener + +// waitListener is a wrapper around a listener that also calls wg.Done() on the +// first call to Accept. It is useful in situations where it is important to +// catch the precise moment of the first call to Accept, for example when +// starting an HTTP server. +// +// TODO(a.garipov): Move to aghnet? +type waitListener struct { + net.Listener + + firstAcceptWG *sync.WaitGroup + firstAcceptOnce sync.Once +} + +// type check +var _ net.Listener = (*waitListener)(nil) + +// Accept implements the [net.Listener] interface for *waitListener. +func (l *waitListener) Accept() (conn net.Conn, err error) { + l.firstAcceptOnce.Do(l.firstAcceptWG.Done) + + return l.Listener.Accept() +} diff --git a/internal/next/websvc/waitlistener_internal_test.go b/internal/next/websvc/waitlistener_internal_test.go new file mode 100644 index 00000000..089c0531 --- /dev/null +++ b/internal/next/websvc/waitlistener_internal_test.go @@ -0,0 +1,40 @@ +package websvc + +import ( + "net" + "sync" + "sync/atomic" + "testing" + + "github.com/AdguardTeam/golibs/testutil/fakenet" + "github.com/stretchr/testify/assert" +) + +func TestWaitListener_Accept(t *testing.T) { + var accepted atomic.Bool + var l net.Listener = &fakenet.Listener{ + OnAccept: func() (conn net.Conn, err error) { + accepted.Store(true) + + return nil, nil + }, + OnAddr: func() (addr net.Addr) { panic("not implemented") }, + OnClose: func() (err error) { panic("not implemented") }, + } + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + var wrapper net.Listener = &waitListener{ + Listener: l, + firstAcceptWG: wg, + } + + _, _ = wrapper.Accept() + }() + + wg.Wait() + + assert.Eventually(t, accepted.Load, testTimeout, testTimeout/10) +} diff --git a/internal/next/websvc/websvc.go b/internal/next/websvc/websvc.go new file mode 100644 index 00000000..2dc309d4 --- /dev/null +++ b/internal/next/websvc/websvc.go @@ -0,0 +1,329 @@ +// Package websvc contains the AdGuard Home HTTP API service. +// +// NOTE: Packages other than cmd must not import this package, as it imports +// most other packages. +// +// TODO(a.garipov): Add tests. +package websvc + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/netip" + "runtime" + "sync" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/golibs/mathutil" + "github.com/AdguardTeam/golibs/pprofutil" + httptreemux "github.com/dimfeld/httptreemux/v5" +) + +// ConfigManager is the configuration manager interface. +type ConfigManager interface { + DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) + Web() (svc agh.ServiceWithConfig[*Config]) + + UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) + UpdateWeb(ctx context.Context, c *Config) (err error) +} + +// Service is the AdGuard Home web service. A nil *Service is a valid +// [agh.Service] that does nothing. +type Service struct { + confMgr ConfigManager + frontend fs.FS + tls *tls.Config + pprof *http.Server + start time.Time + overrideAddr netip.AddrPort + servers []*http.Server + timeout time.Duration + pprofPort uint16 + forceHTTPS bool +} + +// New returns a new properly initialized *Service. If c is nil, svc is a nil +// *Service that does nothing. The fields of c must not be modified after +// calling New. +// +// TODO(a.garipov): Get rid of this special handling of nil or explain it +// better. +func New(c *Config) (svc *Service, err error) { + if c == nil { + return nil, nil + } + + svc = &Service{ + confMgr: c.ConfigManager, + frontend: c.Frontend, + tls: c.TLS, + start: c.Start, + overrideAddr: c.OverrideAddress, + timeout: c.Timeout, + forceHTTPS: c.ForceHTTPS, + } + + mux := newMux(svc) + + if svc.overrideAddr != (netip.AddrPort{}) { + svc.servers = []*http.Server{newSrv(svc.overrideAddr, nil, mux, c.Timeout)} + } else { + for _, a := range c.Addresses { + svc.servers = append(svc.servers, newSrv(a, nil, mux, c.Timeout)) + } + + for _, a := range c.SecureAddresses { + svc.servers = append(svc.servers, newSrv(a, c.TLS, mux, c.Timeout)) + } + } + + svc.setupPprof(c.Pprof) + + return svc, nil +} + +// setupPprof sets the pprof properties of svc. +func (svc *Service) setupPprof(c *PprofConfig) { + if !c.Enabled { + // Set to zero explicitly in case pprof used to be enabled before a + // reconfiguration took place. + runtime.SetBlockProfileRate(0) + runtime.SetMutexProfileFraction(0) + + return + } + + runtime.SetBlockProfileRate(1) + runtime.SetMutexProfileFraction(1) + + pprofMux := http.NewServeMux() + pprofutil.RoutePprof(pprofMux) + + svc.pprofPort = c.Port + addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port) + + // TODO(a.garipov): Consider making pprof timeout configurable. + svc.pprof = newSrv(addr, nil, pprofMux, 10*time.Minute) +} + +// newSrv returns a new *http.Server with the given parameters. +func newSrv( + addr netip.AddrPort, + tlsConf *tls.Config, + h http.Handler, + timeout time.Duration, +) (srv *http.Server) { + addrStr := addr.String() + srv = &http.Server{ + Addr: addrStr, + Handler: h, + TLSConfig: tlsConf, + ReadTimeout: timeout, + WriteTimeout: timeout, + IdleTimeout: timeout, + ReadHeaderTimeout: timeout, + } + + if tlsConf == nil { + srv.ErrorLog = log.StdLog("websvc: plain http: "+addrStr, log.ERROR) + } else { + srv.ErrorLog = log.StdLog("websvc: https: "+addrStr, log.ERROR) + } + + return srv +} + +// newMux returns a new HTTP request multiplexer for the AdGuard Home web +// service. +func newMux(svc *Service) (mux *httptreemux.ContextMux) { + mux = httptreemux.NewContextMux() + + routes := []struct { + handler http.HandlerFunc + method string + pattern string + isJSON bool + }{{ + handler: svc.handleGetHealthCheck, + method: http.MethodGet, + pattern: PathHealthCheck, + isJSON: false, + }, { + handler: http.FileServer(http.FS(svc.frontend)).ServeHTTP, + method: http.MethodGet, + pattern: PathFrontend, + isJSON: false, + }, { + handler: http.FileServer(http.FS(svc.frontend)).ServeHTTP, + method: http.MethodGet, + pattern: PathRoot, + isJSON: false, + }, { + handler: svc.handleGetSettingsAll, + method: http.MethodGet, + pattern: PathV1SettingsAll, + isJSON: true, + }, { + handler: svc.handlePatchSettingsDNS, + method: http.MethodPatch, + pattern: PathV1SettingsDNS, + isJSON: true, + }, { + handler: svc.handlePatchSettingsHTTP, + method: http.MethodPatch, + pattern: PathV1SettingsHTTP, + isJSON: true, + }, { + handler: svc.handleGetV1SystemInfo, + method: http.MethodGet, + pattern: PathV1SystemInfo, + isJSON: true, + }} + + for _, r := range routes { + var hdlr http.Handler + if r.isJSON { + hdlr = jsonMw(r.handler) + } else { + hdlr = r.handler + } + + mux.Handle(r.method, r.pattern, logMw(hdlr)) + } + + return mux +} + +// addrs returns all addresses on which this server serves the HTTP API. addrs +// must not be called simultaneously with Start. If svc was initialized with +// ":0" addresses, addrs will not return the actual bound ports until Start is +// finished. +func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) { + if svc.overrideAddr != (netip.AddrPort{}) { + return []netip.AddrPort{svc.overrideAddr}, nil + } + + for _, srv := range svc.servers { + // Use MustParseAddrPort, since no errors should technically happen + // here, because all servers must have a valid address. + addrPort := netip.MustParseAddrPort(srv.Addr) + + // [srv.Serve] will set TLSConfig to an almost empty value, so, instead + // of relying only on the nilness of TLSConfig, check the length of the + // certificates field as well. + if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 { + addrs = append(addrs, addrPort) + } else { + secureAddrs = append(secureAddrs, addrPort) + } + } + + return addrs, secureAddrs +} + +// handleGetHealthCheck is the handler for the GET /health-check HTTP API. +func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request) { + _, _ = io.WriteString(w, "OK") +} + +// type check +var _ agh.Service = (*Service)(nil) + +// Start implements the [agh.Service] interface for *Service. svc may be nil. +// After Start exits, all HTTP servers have tried to start, possibly failing and +// writing error messages to the log. +func (svc *Service) Start() (err error) { + if svc == nil { + return nil + } + + pprofEnabled := svc.pprof != nil + srvNum := len(svc.servers) + mathutil.BoolToNumber[int](pprofEnabled) + + wg := &sync.WaitGroup{} + wg.Add(srvNum) + for _, srv := range svc.servers { + go serve(srv, wg) + } + + if pprofEnabled { + go serve(svc.pprof, wg) + } + + wg.Wait() + + return nil +} + +// serve starts and runs srv and writes all errors into its log. +func serve(srv *http.Server, wg *sync.WaitGroup) { + addr := srv.Addr + defer log.OnPanic(addr) + + var proto string + var l net.Listener + var err error + if srv.TLSConfig == nil { + proto = "http" + l, err = net.Listen("tcp", addr) + } else { + proto = "https" + l, err = tls.Listen("tcp", addr, srv.TLSConfig) + } + if err != nil { + srv.ErrorLog.Printf("starting srv %s: binding: %s", addr, err) + } + + // Update the server's address in case the address had the port zero, which + // would mean that a random available port was automatically chosen. + srv.Addr = l.Addr().String() + + log.Info("websvc: starting srv %s://%s", proto, srv.Addr) + + l = &waitListener{ + Listener: l, + firstAcceptWG: wg, + } + + err = srv.Serve(l) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + srv.ErrorLog.Printf("starting srv %s: %s", addr, err) + } +} + +// Shutdown implements the [agh.Service] interface for *Service. svc may be +// nil. +func (svc *Service) Shutdown(ctx context.Context) (err error) { + if svc == nil { + return nil + } + + defer func() { err = errors.Annotate(err, "shutting down: %w") }() + + var errs []error + for _, srv := range svc.servers { + shutdownErr := srv.Shutdown(ctx) + if shutdownErr != nil { + errs = append(errs, fmt.Errorf("srv %s: %w", srv.Addr, shutdownErr)) + } + } + + if svc.pprof != nil { + shutdownErr := svc.pprof.Shutdown(ctx) + if shutdownErr != nil { + errs = append(errs, fmt.Errorf("pprof srv %s: %w", svc.pprof.Addr, shutdownErr)) + } + } + + return errors.Join(errs...) +} diff --git a/internal/next/websvc/websvc_internal_test.go b/internal/next/websvc/websvc_internal_test.go new file mode 100644 index 00000000..3509b193 --- /dev/null +++ b/internal/next/websvc/websvc_internal_test.go @@ -0,0 +1,6 @@ +package websvc + +import "time" + +// testTimeout is the common timeout for tests. +const testTimeout = 1 * time.Second diff --git a/internal/next/websvc/websvc_test.go b/internal/next/websvc/websvc_test.go new file mode 100644 index 00000000..6a2505d5 --- /dev/null +++ b/internal/next/websvc/websvc_test.go @@ -0,0 +1,196 @@ +package websvc_test + +import ( + "bytes" + "context" + "encoding/json" + "io" + "io/fs" + "net/http" + "net/netip" + "net/url" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/next/agh" + "github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc" + "github.com/AdguardTeam/AdGuardHome/internal/next/websvc" + "github.com/AdguardTeam/golibs/testutil" + "github.com/AdguardTeam/golibs/testutil/fakefs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + testutil.DiscardLogOutput(m) +} + +// testTimeout is the common timeout for tests. +const testTimeout = 1 * time.Second + +// testStart is the server start value for tests. +var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) + +// type check +var _ websvc.ConfigManager = (*configManager)(nil) + +// configManager is a [websvc.ConfigManager] for tests. +type configManager struct { + onDNS func() (svc agh.ServiceWithConfig[*dnssvc.Config]) + onWeb func() (svc agh.ServiceWithConfig[*websvc.Config]) + + onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error) + onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error) +} + +// DNS implements the [websvc.ConfigManager] interface for *configManager. +func (m *configManager) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) { + return m.onDNS() +} + +// Web implements the [websvc.ConfigManager] interface for *configManager. +func (m *configManager) Web() (svc agh.ServiceWithConfig[*websvc.Config]) { + return m.onWeb() +} + +// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager. +func (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) { + return m.onUpdateDNS(ctx, c) +} + +// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager. +func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) { + return m.onUpdateWeb(ctx, c) +} + +// newConfigManager returns a *configManager all methods of which panic. +func newConfigManager() (m *configManager) { + return &configManager{ + onDNS: func() (svc agh.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") }, + onWeb: func() (svc agh.ServiceWithConfig[*websvc.Config]) { panic("not implemented") }, + onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) { + panic("not implemented") + }, + onUpdateWeb: func(_ context.Context, _ *websvc.Config) (err error) { + panic("not implemented") + }, + } +} + +// newTestServer creates and starts a new web service instance as well as its +// sole address. It also registers a cleanup procedure, which shuts the +// instance down. +// +// TODO(a.garipov): Use svc or remove it. +func newTestServer( + t testing.TB, + confMgr websvc.ConfigManager, +) (svc *websvc.Service, addr netip.AddrPort) { + t.Helper() + + c := &websvc.Config{ + Pprof: &websvc.PprofConfig{ + Enabled: false, + }, + ConfigManager: confMgr, + Frontend: &fakefs.FS{ + OnOpen: func(_ string) (_ fs.File, _ error) { return nil, fs.ErrNotExist }, + }, + TLS: nil, + Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")}, + SecureAddresses: nil, + Timeout: testTimeout, + Start: testStart, + ForceHTTPS: false, + } + + svc, err := websvc.New(c) + require.NoError(t, err) + + err = svc.Start() + require.NoError(t, err) + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + t.Cleanup(cancel) + + err = svc.Shutdown(ctx) + require.NoError(t, err) + }) + + c = svc.Config() + require.NotNil(t, c) + require.Len(t, c.Addresses, 1) + + return svc, c.Addresses[0] +} + +// jobj is a utility alias for JSON objects. +type jobj map[string]any + +// httpGet is a helper that performs an HTTP GET request and returns the body of +// the response as well as checks that the status code is correct. +// +// TODO(a.garipov): Add helpers for other methods. +func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) { + t.Helper() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + require.NoErrorf(t, err, "creating req") + + httpCli := &http.Client{ + Timeout: testTimeout, + } + resp, err := httpCli.Do(req) + require.NoErrorf(t, err, "performing req") + require.Equal(t, wantCode, resp.StatusCode) + + testutil.CleanupAndRequireSuccess(t, resp.Body.Close) + + body, err = io.ReadAll(resp.Body) + require.NoErrorf(t, err, "reading body") + + return body +} + +// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded +// reqBody as the request body and returns the body of the response as well as +// checks that the status code is correct. +// +// TODO(a.garipov): Add helpers for other methods. +func httpPatch(t testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) { + t.Helper() + + b, err := json.Marshal(reqBody) + require.NoErrorf(t, err, "marshaling reqBody") + + req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b)) + require.NoErrorf(t, err, "creating req") + + httpCli := &http.Client{ + Timeout: testTimeout, + } + resp, err := httpCli.Do(req) + require.NoErrorf(t, err, "performing req") + require.Equal(t, wantCode, resp.StatusCode) + + testutil.CleanupAndRequireSuccess(t, resp.Body.Close) + + body, err = io.ReadAll(resp.Body) + require.NoErrorf(t, err, "reading body") + + return body +} + +func TestService_Start_getHealthCheck(t *testing.T) { + confMgr := newConfigManager() + _, addr := newTestServer(t, confMgr) + u := &url.URL{ + Scheme: "http", + Host: addr.String(), + Path: websvc.PathHealthCheck, + } + + body := httpGet(t, u, http.StatusOK) + + assert.Equal(t, []byte("OK"), body) +} diff --git a/internal/schedule/schedule.go b/internal/schedule/schedule.go index fe5aeb82..8335a79e 100644 --- a/internal/schedule/schedule.go +++ b/internal/schedule/schedule.go @@ -339,9 +339,6 @@ func (r dayRange) toDayConfigJSON() (j *dayConfigJSON) { // weeklyConfigJSON is the JSON configuration structure of Weekly. type weeklyConfigJSON struct { - // TimeZone is the local time zone. - TimeZone string `json:"time_zone"` - // Days of the week. Sunday *dayConfigJSON `json:"sun,omitempty"` @@ -351,6 +348,9 @@ type weeklyConfigJSON struct { Thursday *dayConfigJSON `json:"thu,omitempty"` Friday *dayConfigJSON `json:"fri,omitempty"` Saturday *dayConfigJSON `json:"sat,omitempty"` + + // TimeZone is the local time zone. + TimeZone string `json:"time_zone"` } // dayConfigJSON is the JSON configuration structure of dayRange. diff --git a/internal/schedule/schedule_internal_test.go b/internal/schedule/schedule_internal_test.go index 0d9aa705..3e95676c 100644 --- a/internal/schedule/schedule_internal_test.go +++ b/internal/schedule/schedule_internal_test.go @@ -163,10 +163,10 @@ yaml: "bad" } testCases := []struct { + want *Weekly name string wantErrMsg string data []byte - want *Weekly }{{ name: "empty", wantErrMsg: "", @@ -228,9 +228,9 @@ func TestWeekly_MarshalYAML(t *testing.T) { } testCases := []struct { + want *Weekly name string data []byte - want *Weekly }{{ name: "empty", data: []byte(""), @@ -263,8 +263,8 @@ func TestWeekly_MarshalYAML(t *testing.T) { func TestWeekly_Validate(t *testing.T) { testCases := []struct { name string - in dayRange wantErrMsg string + in dayRange }{{ name: "empty", wantErrMsg: "", @@ -298,8 +298,8 @@ func TestWeekly_Validate(t *testing.T) { func TestDayRange_Validate(t *testing.T) { testCases := []struct { name string - in dayRange wantErrMsg string + in dayRange }{{ name: "empty", wantErrMsg: "", @@ -413,10 +413,10 @@ func TestWeekly_UnmarshalJSON(t *testing.T) { } testCases := []struct { + want *Weekly name string wantErrMsg string data []byte - want *Weekly }{{ name: "empty", wantErrMsg: "unexpected end of JSON input", @@ -478,9 +478,9 @@ func TestWeekly_MarshalJSON(t *testing.T) { } testCases := []struct { + want *Weekly name string data []byte - want *Weekly }{{ name: "empty", data: []byte(""), diff --git a/internal/tools/go.mod b/internal/tools/go.mod index a2e3bc9a..36e339fa 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -10,7 +10,7 @@ require ( github.com/kyoh86/looppointer v0.2.1 github.com/securego/gosec/v2 v2.18.2 github.com/uudashr/gocognit v1.1.2 - golang.org/x/tools v0.15.0 + golang.org/x/tools v0.16.0 golang.org/x/vuln v1.0.1 honnef.co/go/tools v0.4.6 mvdan.cc/gofumpt v0.5.0 @@ -26,9 +26,9 @@ require ( github.com/kyoh86/nolint v0.0.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect - golang.org/x/exp/typeparams v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/exp/typeparams v0.0.0-20231127185646-65229373498e // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/tools/go.sum b/internal/tools/go.sum index e9284b47..82c69078 100644 --- a/internal/tools/go.sum +++ b/internal/tools/go.sum @@ -49,8 +49,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/exp/typeparams v0.0.0-20231110203233-9a3e6036ecaa h1:wJBD77KpXKOckDJT0rqU5EwZDmxcmTh6aXVpU6s6GBg= -golang.org/x/exp/typeparams v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20231127185646-65229373498e h1:Iel2aGgaO80fSb1N54L7SE6XMeVvYy6caKt8u/5LvR8= +golang.org/x/exp/typeparams v0.0.0-20231127185646-65229373498e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= @@ -63,7 +63,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -79,8 +79,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -93,8 +93,8 @@ golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4X golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU= golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main_next.go b/main_next.go new file mode 100644 index 00000000..d67cd50f --- /dev/null +++ b/main_next.go @@ -0,0 +1,20 @@ +//go:build next + +package main + +import ( + "embed" + + "github.com/AdguardTeam/AdGuardHome/internal/next/cmd" +) + +// Embed the prebuilt client here since we strive to keep .go files inside the +// internal directory and the embed package is unable to embed files located +// outside of the same or underlying directory. + +//go:embed build +var frontend embed.FS + +func main() { + cmd.Main(frontend) +} diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index cb28b43f..16e7ab0d 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -4,6 +4,37 @@ ## v0.108.0: API changes +## v0.107.42: API changes + +### The new fields `"upstreams_cache_enabled"` and `"upstreams_cache_size"` in `Client` object + +* The new field `"upstreams_cache_enabled"` in `GET /control/clients`, + `GET /control/clients/find`, `POST /control/clients/add`, and + `POST /control/clients/update` methods shows if client's DNS cache is enabled + for the client. If not set AdGuard Home will use default value (false). + +* The new field `"upstreams_cache_size"` in `GET /control/clients`, + `GET /control/clients/find`, `POST /control/clients/add`, and + `POST /control/clients/update` methods is the size of client's DNS cache in + bytes. + +### The new field `"ratelimit_subnet_len_ipv4"` in `DNSConfig` object + +* The new field `"ratelimit_subnet_len_ipv4"` in `GET /control/dns_info` and + `POST /control/dns_config` is the length of the subnet mask for IPv4 + addresses. + +### The new field `"ratelimit_subnet_len_ipv6"` in `DNSConfig` object + +* The new field `"ratelimit_subnet_len_ipv6"` in `GET /control/dns_info` and + `POST /control/dns_config` is the length of the subnet mask for IPv6 + addresses. + +### The new field `"ratelimit_whitelist"` in `DNSConfig` object + +* The new field `"blocked_response_ttl"` in `GET /control/dns_info` and `POST + /control/dns_config` is the list of IP addresses excluded from rate limiting. + ## v0.107.39: API changes ### New HTTP API 'POST /control/dhcp/update_static_lease' diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 2085bfd8..c105ee1d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1468,6 +1468,23 @@ 'type': 'boolean' 'ratelimit': 'type': 'integer' + 'ratelimit_subnet_subnet_len_ipv4': + 'description': 'Length of the subnet mask for IPv4 addresses.' + 'type': 'integer' + 'default': 24 + 'minimum': 0 + 'maximum': 32 + 'ratelimit_subnet_subnet_len_ipv6': + 'description': 'Length of the subnet mask for IPv6 addresses.' + 'type': 'integer' + 'default': 56 + 'minimum': 0 + 'maximum': 128 + 'ratelimit_whitelist': + 'type': 'array' + 'description': 'List of IP addresses excluded from rate limiting.' + 'items': + 'type': 'string' 'blocking_mode': 'type': 'string' 'enum': @@ -2667,6 +2684,25 @@ If `ignore_statistics` is not set in HTTP API `GET /clients/update` request then the existing value will not be changed. + This behaviour can be changed in the future versions. + 'type': 'boolean' + 'upstreams_cache_enabled': + 'description': | + NOTE: If `upstreams_cache_enabled` is not set in HTTP API + `GET /clients/add` request then default value (false) will be used. + + If `upstreams_cache_enabled` is not set in HTTP API + `GET /clients/update` request then the existing value will not be + changed. + + This behaviour can be changed in the future versions. + 'type': 'boolean' + 'upstreams_cache_size': + 'description': | + NOTE: If `upstreams_cache_enabled` is not set in HTTP API + `GET /clients/update` request then the existing value will not be + changed. + This behaviour can be changed in the future versions. 'type': 'boolean' 'ClientAuto': diff --git a/scripts/make/go-lint.sh b/scripts/make/go-lint.sh index 1ba1e9d9..c04499a1 100644 --- a/scripts/make/go-lint.sh +++ b/scripts/make/go-lint.sh @@ -35,7 +35,7 @@ set -f -u go_version="$( "${GO:-go}" version )" readonly go_version -go_min_version='go1.20.11' +go_min_version='go1.20.12' go_version_msg=" warning: your go version (${go_version}) is different from the recommended minimal one (${go_min_version}). if you have the version installed, please set the GO environment variable. @@ -185,7 +185,6 @@ run_linter gocognit --over='18'\ run_linter gocognit --over='15'\ ./internal/aghos/\ - ./internal/dnsforward/\ ./internal/filtering/\ ; @@ -198,10 +197,13 @@ run_linter gocognit --over='13'\ ; run_linter gocognit --over='12'\ - ./internal/updater/\ ./internal/filtering/rewrite/\ ; +run_linter gocognit --over='11'\ + ./internal/updater/\ + ; + run_linter gocognit --over='10'\ ./internal/aghalg/\ ./internal/aghchan/\ @@ -212,6 +214,7 @@ run_linter gocognit --over='10'\ ./internal/client/\ ./internal/confmigrate/\ ./internal/dhcpsvc\ + ./internal/dnsforward/\ ./internal/filtering/hashprefix/\ ./internal/filtering/rulelist/\ ./internal/filtering/safesearch/\