Compare commits

..

2 Commits

Author SHA1 Message Date
Eugene Burkov
de837e4eec all: use netip for web 2022-09-29 14:56:37 +03:00
Eugene Burkov
e528d2f23b add basic lla 2022-09-28 20:30:40 +03:00
67 changed files with 785 additions and 1123 deletions

View File

@@ -12,104 +12,11 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
<!-- <!--
## [v0.108.0] - TBA (APPROX.) ## [v0.108.0] - 2022-12-01 (APPROX.)
--> -->
<!--
## [v0.107.16] - 2022-11-02 (APPROX.)
See also the [v0.107.16 GitHub milestone][ms-v0.107.15].
[ms-v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/milestone/52?closed=1
-->
## [v0.107.15] - 2022-10-03
See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
### Security ### Security
- As an additional CSRF protection measure, AdGuard Home now ensures that
requests that change its state but have no body (such as `POST
/control/stats_reset` requests) do not have a `Content-Type` header set on
them ([#4970]).
### Added
#### Experimental HTTP/3 Support
See [#3955] and the related issues for more details. These features are still
experimental and may break or change in the future.
- DNS-over-HTTP/3 DNS and web UI client request support. This feature must be
explicitly enabled by setting the new property `dns.serve_http3` in the
configuration file to `true`.
- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration
file property `use_http3_upstreams` is set to `true`.
- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions
using the `h3://` scheme.
### Fixed
- User-specific blocked services not applying correctly ([#4945], [#4982],
[#4983]).
- `only application/json is allowed` errors in various APIs ([#4970]).
[#3955]: https://github.com/AdguardTeam/AdGuardHome/issues/3955
[#4945]: https://github.com/AdguardTeam/AdGuardHome/issues/4945
[#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970
[#4982]: https://github.com/AdguardTeam/AdGuardHome/issues/4982
[#4983]: https://github.com/AdguardTeam/AdGuardHome/issues/4983
[ms-v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/milestone/51?closed=1
## [v0.107.14] - 2022-09-29
See also the [v0.107.14 GitHub milestone][ms-v0.107.14].
### Security
A Cross-Site Request Forgery (CSRF) vulnerability has been discovered. The CVE
number is to be assigned. We thank Daniel Elkabes from Mend.io for reporting
this vulnerability to us.
#### `SameSite` Policy
The `SameSite` policy on the AdGuard Home session cookies is now set to `Lax`.
Which means that the only cross-site HTTP request for which the browser is
allowed to send the session cookie is navigating to the AdGuard Home domain.
**Users are strongly advised to log out, clear browser cache, and log in again
after updating.**
#### Removal Of Plain-Text APIs (BREAKING API CHANGE)
We have implemented several measures to prevent such vulnerabilities in the
future, but some of these measures break backwards compatibility for the sake of
better protection.
The following APIs, which previously accepted or returned `text/plain` data,
now accept or return data as JSON. All new formats for the request and response
bodies are documented in `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
- `GET /control/i18n/current_language`;
- `POST /control/dhcp/find_active_dhcp`;
- `POST /control/filtering/set_rules`;
- `POST /control/i18n/change_language`.
#### Stricter Content-Type Checks (BREAKING API CHANGE)
All JSON APIs that expect a body now check if the request actually has
`Content-Type` set to `application/json`.
#### Other Security Changes
- Weaker cipher suites that use the CBC (cipher block chaining) mode of - Weaker cipher suites that use the CBC (cipher block chaining) mode of
operation have been disabled ([#2993]). operation have been disabled ([#2993]).
@@ -126,7 +33,16 @@ All JSON APIs that expect a body now check if the request actually has
[#4927]: https://github.com/AdguardTeam/AdGuardHome/issues/4927 [#4927]: https://github.com/AdguardTeam/AdGuardHome/issues/4927
[#4930]: https://github.com/AdguardTeam/AdGuardHome/issues/4930 [#4930]: https://github.com/AdguardTeam/AdGuardHome/issues/4930
<!--
## [v0.107.14] - 2022-10-05 (APPROX.)
See also the [v0.107.14 GitHub milestone][ms-v0.107.14].
[ms-v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/milestone/50?closed=1 [ms-v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/milestone/50?closed=1
-->
@@ -1325,13 +1241,11 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...HEAD
[v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...v0.107.15 [v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
--> -->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...HEAD
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13 [v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
[v0.107.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12 [v0.107.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12
[v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11 [v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11

View File

@@ -1,18 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please send your vulnerability reports to <security@adguard.com>. To make sure
that your report reaches us, please:
1. Include the words “AdGuard Home” and “vulnerability” to the subject line as
well as a short description of the vulnerability. For example:
> AdGuard Home API vulnerability: possible XSS attack
2. Make sure that the message body contains a clear description of the
vulnerability.
If you have not received a reply to your email within 7 days, please make sure
to follow up with us again at <security@adguard.com>. Once again, make sure
that the word “vulnerability” is in the subject line.

View File

@@ -635,6 +635,5 @@
"parental_control": "Бацькоўскі кантроль", "parental_control": "Бацькоўскі кантроль",
"safe_browsing": "Бяспечны інтэрнэт", "safe_browsing": "Бяспечны інтэрнэт",
"served_from_cache": "{{value}} <i>(атрымана з кэша)</i>", "served_from_cache": "{{value}} <i>(атрымана з кэша)</i>",
"form_error_password_length": "Пароль павінен быць не менш за {{value}} сімвалаў", "form_error_password_length": "Пароль павінен быць не менш за {{value}} сімвалаў"
"anonymizer_notification": "<0>Заўвага:</0> Ананімізацыя IP уключана. Вы можаце адключыць яго ў <1>Агульных наладах</1> ."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Rodičovská ochrana", "parental_control": "Rodičovská ochrana",
"safe_browsing": "Bezpečné prohlížení", "safe_browsing": "Bezpečné prohlížení",
"served_from_cache": "{{value}} <i>(převzato z mezipaměti)</i>", "served_from_cache": "{{value}} <i>(převzato z mezipaměti)</i>",
"form_error_password_length": "Heslo musí být alespoň {{value}} znaků dlouhé", "form_error_password_length": "Heslo musí být alespoň {{value}} znaků dlouhé"
"anonymizer_notification": "<0>Poznámka:</0> Anonymizace IP je zapnuta. Můžete ji vypnout v <1>Obecných nastaveních</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Forældrekontrol", "parental_control": "Forældrekontrol",
"safe_browsing": "Sikker Browsing", "safe_browsing": "Sikker Browsing",
"served_from_cache": "{{value}} <i>(leveret fra cache)</i>", "served_from_cache": "{{value}} <i>(leveret fra cache)</i>",
"form_error_password_length": "Adgangskoden skal udgøre mindst {{value}} tegn.", "form_error_password_length": "Adgangskoden skal udgøre mindst {{value}} tegn."
"anonymizer_notification": "<0>Bemærk:</0> IP-anonymisering er aktiveret. Det kan deaktiveres via <1>Generelle indstillinger</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Kindersicherung", "parental_control": "Kindersicherung",
"safe_browsing": "Internetsicherheit", "safe_browsing": "Internetsicherheit",
"served_from_cache": "{{value}} <i>(aus dem Cache abgerufen)</i>", "served_from_cache": "{{value}} <i>(aus dem Cache abgerufen)</i>",
"form_error_password_length": "Das Passwort muss mindestens {{value}} Zeichen enthalten", "form_error_password_length": "Das Passwort muss mindestens {{value}} Zeichen enthalten"
"anonymizer_notification": "<0>Hinweis:</0> Die IP-Anonymisierung ist aktiviert. Sie können sie in den <1>Allgemeinen Einstellungen</1> deaktivieren."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Control parental", "parental_control": "Control parental",
"safe_browsing": "Navegación segura", "safe_browsing": "Navegación segura",
"served_from_cache": "{{value}} <i>(servido desde la caché)</i>", "served_from_cache": "{{value}} <i>(servido desde la caché)</i>",
"form_error_password_length": "La contraseña debe tener al menos {{value}} caracteres", "form_error_password_length": "La contraseña debe tener al menos {{value}} caracteres"
"anonymizer_notification": "<0>Nota:</0> La anonimización de IP está habilitada. Puedes deshabilitarla en <1>Configuración general</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Lapsilukko", "parental_control": "Lapsilukko",
"safe_browsing": "Turvallinen selaus", "safe_browsing": "Turvallinen selaus",
"served_from_cache": "{{value}} <i>(jaettu välimuistista)</i>", "served_from_cache": "{{value}} <i>(jaettu välimuistista)</i>",
"form_error_password_length": "Salasanan on oltava ainakin {{value}} merkkiä", "form_error_password_length": "Salasanan on oltava ainakin {{value}} merkkiä"
"anonymizer_notification": "<0>Huomioi:</0> IP-osoitteen anonymisointi on käytössä. Voit poistaa sen käytöstä <1>Yleisistä asetuksista</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Contrôle parental", "parental_control": "Contrôle parental",
"safe_browsing": "Navigation sécurisée", "safe_browsing": "Navigation sécurisée",
"served_from_cache": "{{value}} <i>(depuis le cache)</i>", "served_from_cache": "{{value}} <i>(depuis le cache)</i>",
"form_error_password_length": "Le mot de passe doit comporter au moins {{value}} caractères", "form_error_password_length": "Le mot de passe doit comporter au moins {{value}} caractères"
"anonymizer_notification": "<0>Note :</0> L'anonymisation IP est activée. Vous pouvez la désactiver dans les <1>paramètres généraux</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Roditeljska zaštita", "parental_control": "Roditeljska zaštita",
"safe_browsing": "Sigurno surfanje", "safe_browsing": "Sigurno surfanje",
"served_from_cache": "{{value}} <i>(dohvaćeno iz predmemorije)</i>", "served_from_cache": "{{value}} <i>(dohvaćeno iz predmemorije)</i>",
"form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova", "form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova"
"anonymizer_notification": "<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Szülői felügyelet", "parental_control": "Szülői felügyelet",
"safe_browsing": "Biztonságos böngészés", "safe_browsing": "Biztonságos böngészés",
"served_from_cache": "{{value}} <i>(gyorsítótárból kiszolgálva)</i>", "served_from_cache": "{{value}} <i>(gyorsítótárból kiszolgálva)</i>",
"form_error_password_length": "A jelszó legalább {{value}} karakter hosszú kell, hogy legyen", "form_error_password_length": "A jelszó legalább {{value}} karakter hosszú kell, hogy legyen"
"anonymizer_notification": "<0>Megjegyzés:</0> Az IP anonimizálás engedélyezve van. Az <1>Általános beállításoknál letilthatja</1> ."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Kontrol Orang Tua", "parental_control": "Kontrol Orang Tua",
"safe_browsing": "Penjelajahan Aman", "safe_browsing": "Penjelajahan Aman",
"served_from_cache": "{{value}} <i>(disajikan dari cache)</i>", "served_from_cache": "{{value}} <i>(disajikan dari cache)</i>",
"form_error_password_length": "Kata sandi harus minimal {{value}} karakter", "form_error_password_length": "Kata sandi harus minimal {{value}} karakter"
"anonymizer_notification": "<0>Catatan:</0> Anonimisasi IP diaktifkan. Anda dapat menonaktifkannya di <1>Pengaturan umum</1> ."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Controllo Parentale", "parental_control": "Controllo Parentale",
"safe_browsing": "Navigazione Sicura", "safe_browsing": "Navigazione Sicura",
"served_from_cache": "{{value}} <i>(fornito dalla cache)</i>", "served_from_cache": "{{value}} <i>(fornito dalla cache)</i>",
"form_error_password_length": "La password deve contenere almeno {{value}} caratteri", "form_error_password_length": "La password deve contenere almeno {{value}} caratteri"
"anonymizer_notification": "<0>Attenzione:</0> L'anonimizzazione dell'IP è abilitata. Puoi disabilitarla in <1>Impostazioni generali</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "ペアレンタルコントロール", "parental_control": "ペアレンタルコントロール",
"safe_browsing": "セーフブラウジング", "safe_browsing": "セーフブラウジング",
"served_from_cache": "{{value}} <i>(キャッシュから応答)</i>", "served_from_cache": "{{value}} <i>(キャッシュから応答)</i>",
"form_error_password_length": "パスワードは{{value}}文字以上にしてください", "form_error_password_length": "パスワードは{{value}}文字以上にしてください"
"anonymizer_notification": "【<0>注意</0>】IPの匿名化が有効になっています。 <1>一般設定</1>で無効にできます。"
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "자녀 보호", "parental_control": "자녀 보호",
"safe_browsing": "세이프 브라우징", "safe_browsing": "세이프 브라우징",
"served_from_cache": "{{value}} <i>(캐시에서 제공)</i>", "served_from_cache": "{{value}} <i>(캐시에서 제공)</i>",
"form_error_password_length": "비밀번호는 {{value}}자 이상이어야 합니다", "form_error_password_length": "비밀번호는 {{value}}자 이상이어야 합니다"
"anonymizer_notification": "<0>참고:</0> IP 익명화가 활성화되었습니다. <1>일반 설정</1>에서 비활성화할 수 있습니다."
} }

View File

@@ -557,7 +557,7 @@
"fastest_addr_desc": "Alle DNS-servers bevragen en het snelste IP adres terugkoppelen. Dit zal de DNS verzoeken vertragen omdat AdGuard Home moet wachten op de antwoorden van alles DNS-servers, maar verbetert wel de connectiviteit.", "fastest_addr_desc": "Alle DNS-servers bevragen en het snelste IP adres terugkoppelen. Dit zal de DNS verzoeken vertragen omdat AdGuard Home moet wachten op de antwoorden van alles DNS-servers, maar verbetert wel de connectiviteit.",
"autofix_warning_text": "Als je op \"Repareren\" klikt, configureert AdGuard Home jouw systeem om de AdGuard Home DNS-server te gebruiken.", "autofix_warning_text": "Als je op \"Repareren\" klikt, configureert AdGuard Home jouw systeem om de AdGuard Home DNS-server te gebruiken.",
"autofix_warning_list": "De volgende taken worden uitgevoerd: <0> Deactiveren van Systeem DNSStubListener</0> <0> DNS-serveradres instellen op 127.0.0.1 </0> <0> Symbolisch koppelingsdoel van /etc/resolv.conf vervangen door /run/systemd/resolve/resolv.conf </0> <0> Stop DNSStubListener (herlaad systemd-resolved service) </0>", "autofix_warning_list": "De volgende taken worden uitgevoerd: <0> Deactiveren van Systeem DNSStubListener</0> <0> DNS-serveradres instellen op 127.0.0.1 </0> <0> Symbolisch koppelingsdoel van /etc/resolv.conf vervangen door /run/systemd/resolve/resolv.conf </0> <0> Stop DNSStubListener (herlaad systemd-resolved service) </0>",
"autofix_warning_result": "Als gevolg hiervan worden alle DNS-aanvragen van je systeem standaard door AdGuard Home verwerkt.", "autofix_warning_result": "Als gevolg hiervan worden alle DNS-verzoeken van je systeem standaard door AdGuard Home verwerkt.",
"tags_title": "Labels", "tags_title": "Labels",
"tags_desc": "Je kunt labels selecteren die overeenkomen met de client. Labels kunnen worden opgenomen in de filterregels om ze \n nauwkeuriger toe te passen. <0>Meer informatie</0>.", "tags_desc": "Je kunt labels selecteren die overeenkomen met de client. Labels kunnen worden opgenomen in de filterregels om ze \n nauwkeuriger toe te passen. <0>Meer informatie</0>.",
"form_select_tags": "Client tags selecteren", "form_select_tags": "Client tags selecteren",
@@ -628,13 +628,12 @@
"original_response": "Oorspronkelijke reactie", "original_response": "Oorspronkelijke reactie",
"click_to_view_queries": "Klik om queries te bekijken", "click_to_view_queries": "Klik om queries te bekijken",
"port_53_faq_link": "Poort 53 wordt vaak gebruikt door services als DNSStubListener- of de systeem DNS-resolver. Lees a.u.b. <0>deze instructie</0> hoe dit is op te lossen.", "port_53_faq_link": "Poort 53 wordt vaak gebruikt door services als DNSStubListener- of de systeem DNS-resolver. Lees a.u.b. <0>deze instructie</0> hoe dit is op te lossen.",
"adg_will_drop_dns_queries": "AdGuard Home zal alle DNS-aanvragen van deze cliënt laten vervallen.", "adg_will_drop_dns_queries": "AdGuard Home zal alle DNS-verzoeken van deze cliënt laten vervallen.",
"filter_allowlist": "WAARSCHUWING: Deze actie zal ook de regel \"{{disallowed_rule}}\" uitsluiten van de lijst met toegestane clients.", "filter_allowlist": "WAARSCHUWING: Deze actie zal ook de regel \"{{disallowed_rule}}\" uitsluiten van de lijst met toegestane clients.",
"last_rule_in_allowlist": "Kan deze client niet weigeren omdat het uitsluiten van de regel \"{{disallowed_rule}}\" de lijst \"Toegestane clients\" zal UITSCHAKELEN.", "last_rule_in_allowlist": "Kan deze client niet weigeren omdat het uitsluiten van de regel \"{{disallowed_rule}}\" de lijst \"Toegestane clients\" zal UITSCHAKELEN.",
"use_saved_key": "De eerder opgeslagen sleutel gebruiken", "use_saved_key": "De eerder opgeslagen sleutel gebruiken",
"parental_control": "Ouderlijk toezicht", "parental_control": "Ouderlijk toezicht",
"safe_browsing": "Veilig browsen", "safe_browsing": "Veilig browsen",
"served_from_cache": "{{value}} <i>(geleverd vanuit cache)</i>", "served_from_cache": "{{value}} <i>(geleverd vanuit cache)</i>",
"form_error_password_length": "Wachtwoord moet minimaal {{value}} tekens lang zijn", "form_error_password_length": "Wachtwoord moet minimaal {{value}} tekens lang zijn"
"anonymizer_notification": "<0>Opmerking:</0> IP-anonimisering is ingeschakeld. Je kunt het uitschakelen in <1>Algemene instellingen</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Kontrola rodzicielska", "parental_control": "Kontrola rodzicielska",
"safe_browsing": "Bezpieczne przeglądanie", "safe_browsing": "Bezpieczne przeglądanie",
"served_from_cache": "{{value}} <i>(podawane z pamięci podręcznej)</i>", "served_from_cache": "{{value}} <i>(podawane z pamięci podręcznej)</i>",
"form_error_password_length": "Hasło musi mieć co najmniej {{value}} znaków", "form_error_password_length": "Hasło musi mieć co najmniej {{value}} znaków"
"anonymizer_notification": "<0>Uwaga:</0> Anonimizacja IP jest włączona. Możesz ją wyłączyć w <1>Ustawieniach ogólnych</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Controle parental", "parental_control": "Controle parental",
"safe_browsing": "Navegação segura", "safe_browsing": "Navegação segura",
"served_from_cache": "{{value}} <i>(servido do cache)</i>", "served_from_cache": "{{value}} <i>(servido do cache)</i>",
"form_error_password_length": "A senha deve ter pelo menos {{value}} caracteres", "form_error_password_length": "A senha deve ter pelo menos {{value}} caracteres"
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-lo em <1>Configurações gerais</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Controlo parental", "parental_control": "Controlo parental",
"safe_browsing": "Navegação segura", "safe_browsing": "Navegação segura",
"served_from_cache": "{{value}} <i>(servido do cache)</i>", "served_from_cache": "{{value}} <i>(servido do cache)</i>",
"form_error_password_length": "A palavra-passe deve ter pelo menos {{value}} caracteres", "form_error_password_length": "A palavra-passe deve ter pelo menos {{value}} caracteres"
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-la em <1>Definições gerais</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Control Parental", "parental_control": "Control Parental",
"safe_browsing": "Navigare în siguranță", "safe_browsing": "Navigare în siguranță",
"served_from_cache": "{{value}} <i>(furnizat din cache)</i>", "served_from_cache": "{{value}} <i>(furnizat din cache)</i>",
"form_error_password_length": "Parola trebuie să aibă cel puțin {{value}} caractere", "form_error_password_length": "Parola trebuie să aibă cel puțin {{value}} caractere"
"anonymizer_notification": "<0>Nota:</0> Anonimizarea IP este activată. Puteți să o dezactivați în <1>Setări generale</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Родительский контроль", "parental_control": "Родительский контроль",
"safe_browsing": "Безопасный интернет", "safe_browsing": "Безопасный интернет",
"served_from_cache": "{{value}} <i>(получено из кеша)</i>", "served_from_cache": "{{value}} <i>(получено из кеша)</i>",
"form_error_password_length": "Пароль должен быть длиной не меньше {{value}} символов", "form_error_password_length": "Пароль должен быть длиной не меньше {{value}} символов"
"anonymizer_notification": "<0>Внимание:</0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Rodičovská kontrola", "parental_control": "Rodičovská kontrola",
"safe_browsing": "Bezpečné prehliadanie", "safe_browsing": "Bezpečné prehliadanie",
"served_from_cache": "{{value}} <i>(prevzatá z cache pamäte)</i>", "served_from_cache": "{{value}} <i>(prevzatá z cache pamäte)</i>",
"form_error_password_length": "Heslo musí mať dĺžku aspoň {{value}} znakov", "form_error_password_length": "Heslo musí mať dĺžku aspoň {{value}} znakov"
"anonymizer_notification": "<0>Poznámka:</0> Anonymizácia IP je zapnutá. Môžete ju vypnúť vo <1>Všeobecných nastaveniach</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Starševski nadzor", "parental_control": "Starševski nadzor",
"safe_browsing": "Varno brskanje", "safe_browsing": "Varno brskanje",
"served_from_cache": "{{value}} <i>(postreženo iz predpomnilnika)</i>", "served_from_cache": "{{value}} <i>(postreženo iz predpomnilnika)</i>",
"form_error_password_length": "Geslo mora vsebovati najmanj {{value}} znakov", "form_error_password_length": "Geslo mora vsebovati najmanj {{value}} znakov"
"anonymizer_notification": "<0>Opomba:</0> Anonimizacija IP je omogočena. Onemogočite ga lahko v <1>Splošnih nastavitvah</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Roditeljska kontrola", "parental_control": "Roditeljska kontrola",
"safe_browsing": "Sigurno pregledanje", "safe_browsing": "Sigurno pregledanje",
"served_from_cache": "{{value}} <i>(posluženo iz predmemorije)</i>", "served_from_cache": "{{value}} <i>(posluženo iz predmemorije)</i>",
"form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova", "form_error_password_length": "Lozinka mora imati najmanje {{value}} znakova"
"anonymizer_notification": "<0>Nota:</0> IP prepoznavanje je omogućeno. Možete ga onemogućiti u opštim <1>postavkama</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Föräldrakontroll", "parental_control": "Föräldrakontroll",
"safe_browsing": "Säker surfning", "safe_browsing": "Säker surfning",
"served_from_cache": "{{value}} <i>(levereras från cache)</i>", "served_from_cache": "{{value}} <i>(levereras från cache)</i>",
"form_error_password_length": "Lösenordet måste vara minst {{value}} tecken långt", "form_error_password_length": "Lösenordet måste vara minst {{value}} tecken långt"
"anonymizer_notification": "<0>Observera:</0> IP-anonymisering är aktiverad. Du kan inaktivera den i <1>Allmänna inställningar</1>."
} }

View File

@@ -368,7 +368,7 @@
"encryption_server_enter": "Alan adınızı girin", "encryption_server_enter": "Alan adınızı girin",
"encryption_server_desc": "Ayarlanırsa, AdGuard Home ClientID'leri algılar, DDR sorgularına yanıt verir ve ek bağlantı doğrulamaları gerçekleştirir. Ayarlanmazsa, bu özellikler devre dışı bırakılır. Sertifikadaki DNS Adlarından biriyle eşleşmelidir.", "encryption_server_desc": "Ayarlanırsa, AdGuard Home ClientID'leri algılar, DDR sorgularına yanıt verir ve ek bağlantı doğrulamaları gerçekleştirir. Ayarlanmazsa, bu özellikler devre dışı bırakılır. Sertifikadaki DNS Adlarından biriyle eşleşmelidir.",
"encryption_redirect": "Otomatik olarak HTTPS'e yönlendir", "encryption_redirect": "Otomatik olarak HTTPS'e yönlendir",
"encryption_redirect_desc": "İşaretlenirse, AdGuard Home sizi otomatik olarak HTTP adresinden HTTPS adreslerine yönlendirecektir.", "encryption_redirect_desc": "Etkinleştirirseniz, AdGuard Home sizi HTTP adresi yerine HTTPS adresine yönlendirir.",
"encryption_https": "HTTPS bağlantı noktası", "encryption_https": "HTTPS bağlantı noktası",
"encryption_https_desc": "HTTPS bağlantı noktası yapılandırılırsa, AdGuard Home yönetici arayüzüne HTTPS aracılığıyla erişilebilir olacak ve ayrıca '/dns-query' üzerinden DNS-over-HTTPS bağlantısı sağlayacaktır.", "encryption_https_desc": "HTTPS bağlantı noktası yapılandırılırsa, AdGuard Home yönetici arayüzüne HTTPS aracılığıyla erişilebilir olacak ve ayrıca '/dns-query' üzerinden DNS-over-HTTPS bağlantısı sağlayacaktır.",
"encryption_dot": "DNS-over-TLS bağlantı noktası", "encryption_dot": "DNS-over-TLS bağlantı noktası",
@@ -408,7 +408,7 @@
"fix": "Düzelt", "fix": "Düzelt",
"dns_providers": "Aralarından seçim yapabileceğiniz, bilinen <0>DNS sağlayıcıların listesi</0>.", "dns_providers": "Aralarından seçim yapabileceğiniz, bilinen <0>DNS sağlayıcıların listesi</0>.",
"update_now": "Şimdi güncelle", "update_now": "Şimdi güncelle",
"update_failed": "Otomatik güncelleme başarısız oldu. Elle güncellemek için lütfen <a>bu adımları izleyin</a>.", "update_failed": "Otomatik güncelleme başarısız oldu. Elle güncellemek için lütfen <a>bu adımları uygulayın</a>.",
"manual_update": "Elle güncellemek için lütfen <a>bu adımları uygulayın</a>.", "manual_update": "Elle güncellemek için lütfen <a>bu adımları uygulayın</a>.",
"processing_update": "Lütfen bekleyin, AdGuard Home güncelleniyor", "processing_update": "Lütfen bekleyin, AdGuard Home güncelleniyor",
"clients_title": "Kalıcı istemciler", "clients_title": "Kalıcı istemciler",
@@ -635,6 +635,5 @@
"parental_control": "Ebeveyn Denetimi", "parental_control": "Ebeveyn Denetimi",
"safe_browsing": "Güvenli Gezinti", "safe_browsing": "Güvenli Gezinti",
"served_from_cache": "{{value}} <i>(önbellekten kullanıldı)</i>", "served_from_cache": "{{value}} <i>(önbellekten kullanıldı)</i>",
"form_error_password_length": "Parola en az {{value}} karakter uzunluğunda olmalıdır", "form_error_password_length": "Parola en az {{value}} karakter uzunluğunda olmalıdır"
"anonymizer_notification": "<0>Not:</0> IP anonimleştirme etkinleştirildi. Bunu <1>Genel ayarlardan</1> devre dışı bırakabilirsiniz."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Батьківський контроль", "parental_control": "Батьківський контроль",
"safe_browsing": "Безпечний перегляд", "safe_browsing": "Безпечний перегляд",
"served_from_cache": "{{value}} <i>(отримано з кешу)</i>", "served_from_cache": "{{value}} <i>(отримано з кешу)</i>",
"form_error_password_length": "Пароль мусить мати принаймні {{value}} символів", "form_error_password_length": "Пароль мусить мати принаймні {{value}} символів"
"anonymizer_notification": "<0>Примітка:</0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування</1> ."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "Quản lý của phụ huynh", "parental_control": "Quản lý của phụ huynh",
"safe_browsing": "Duyệt web an toàn", "safe_browsing": "Duyệt web an toàn",
"served_from_cache": "{{value}} <i>(được phục vụ từ bộ nhớ cache)</i>", "served_from_cache": "{{value}} <i>(được phục vụ từ bộ nhớ cache)</i>",
"form_error_password_length": "Mật khẩu phải có ít nhất {{value}} ký tự", "form_error_password_length": "Mật khẩu phải có ít nhất {{value}} ký tự"
"anonymizer_notification": "<0> Lưu ý:</0> Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung</1>."
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "家长控制", "parental_control": "家长控制",
"safe_browsing": "安全浏览", "safe_browsing": "安全浏览",
"served_from_cache": "{{value}}<i>(由缓存提供)</i>", "served_from_cache": "{{value}}<i>(由缓存提供)</i>",
"form_error_password_length": "密码必须至少有 {{value}} 个字符", "form_error_password_length": "密码必须至少有 {{value}} 个字符"
"anonymizer_notification": "<0>注意:</0> IP 匿名化已启用。您可以在<1>常规设置</1>中禁用它。"
} }

View File

@@ -635,6 +635,5 @@
"parental_control": "家長控制", "parental_control": "家長控制",
"safe_browsing": "安全瀏覽", "safe_browsing": "安全瀏覽",
"served_from_cache": "{{value}} <i>(由快取提供)</i>", "served_from_cache": "{{value}} <i>(由快取提供)</i>",
"form_error_password_length": "密碼必須為至少長 {{value}} 個字元", "form_error_password_length": "密碼必須為至少長 {{value}} 個字元"
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。"
} }

View File

@@ -31,9 +31,7 @@ export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
export const setRules = (rules) => async (dispatch) => { export const setRules = (rules) => async (dispatch) => {
dispatch(setRulesRequest()); dispatch(setRulesRequest());
try { try {
const normalizedRules = { const normalizedRules = normalizeRulesTextarea(rules);
rules: normalizeRulesTextarea(rules)?.split('\n'),
};
await apiClient.setRules(normalizedRules); await apiClient.setRules(normalizedRules);
dispatch(addSuccessToast('updated_custom_filtering_toast')); dispatch(addSuccessToast('updated_custom_filtering_toast'));
dispatch(setRulesSuccess()); dispatch(setRulesSuccess());

View File

@@ -355,7 +355,7 @@ export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');
export const changeLanguage = (lang) => async (dispatch) => { export const changeLanguage = (lang) => async (dispatch) => {
dispatch(changeLanguageRequest()); dispatch(changeLanguageRequest());
try { try {
await apiClient.changeLanguage({ language: lang }); await apiClient.changeLanguage(lang);
dispatch(changeLanguageSuccess()); dispatch(changeLanguageSuccess());
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
@@ -370,8 +370,8 @@ export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS');
export const getLanguage = () => async (dispatch) => { export const getLanguage = () => async (dispatch) => {
dispatch(getLanguageRequest()); dispatch(getLanguageRequest());
try { try {
const langSettings = await apiClient.getCurrentLanguage(); const language = await apiClient.getCurrentLanguage();
dispatch(getLanguageSuccess(langSettings.language)); dispatch(getLanguageSuccess(language));
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(getLanguageFailure()); dispatch(getLanguageFailure());
@@ -421,10 +421,7 @@ export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
export const findActiveDhcp = (name) => async (dispatch, getState) => { export const findActiveDhcp = (name) => async (dispatch, getState) => {
dispatch(findActiveDhcpRequest()); dispatch(findActiveDhcpRequest());
try { try {
const req = { const activeDhcp = await apiClient.findActiveDhcp(name);
interface: name,
};
const activeDhcp = await apiClient.findActiveDhcp(req);
dispatch(findActiveDhcpSuccess(activeDhcp)); dispatch(findActiveDhcpSuccess(activeDhcp));
const { check, interface_name, interfaces } = getState().dhcp; const { check, interface_name, interfaces } = getState().dhcp;
const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name; const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name;

View File

@@ -10,17 +10,11 @@ class Api {
async makeRequest(path, method = 'POST', config) { async makeRequest(path, method = 'POST', config) {
const url = `${this.baseUrl}/${path}`; const url = `${this.baseUrl}/${path}`;
const axiosConfig = config || {};
if (method !== 'GET' && axiosConfig.data) {
axiosConfig.headers = axiosConfig.headers || {};
axiosConfig.headers['Content-Type'] = axiosConfig.headers['Content-Type'] || 'application/json';
}
try { try {
const response = await axios({ const response = await axios({
url, url,
method, method,
...axiosConfig, ...config,
}); });
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -61,6 +55,7 @@ class Api {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS; const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = { const config = {
data: servers, data: servers,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
@@ -69,6 +64,7 @@ class Api {
const { path, method } = this.GLOBAL_VERSION; const { path, method } = this.GLOBAL_VERSION;
const config = { const config = {
data, data,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
@@ -104,6 +100,7 @@ class Api {
const { path, method } = this.FILTERING_REFRESH; const { path, method } = this.FILTERING_REFRESH;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
@@ -113,6 +110,7 @@ class Api {
const { path, method } = this.FILTERING_ADD_FILTER; const { path, method } = this.FILTERING_ADD_FILTER;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
@@ -122,6 +120,7 @@ class Api {
const { path, method } = this.FILTERING_REMOVE_FILTER; const { path, method } = this.FILTERING_REMOVE_FILTER;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
@@ -131,6 +130,7 @@ class Api {
const { path, method } = this.FILTERING_SET_RULES; const { path, method } = this.FILTERING_SET_RULES;
const parameters = { const parameters = {
data: rules, data: rules,
headers: { 'Content-Type': 'text/plain' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -139,6 +139,7 @@ class Api {
const { path, method } = this.FILTERING_CONFIG; const { path, method } = this.FILTERING_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -147,6 +148,7 @@ class Api {
const { path, method } = this.FILTERING_SET_URL; const { path, method } = this.FILTERING_SET_URL;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -171,7 +173,12 @@ class Api {
enableParentalControl() { enableParentalControl() {
const { path, method } = this.PARENTAL_ENABLE; const { path, method } = this.PARENTAL_ENABLE;
return this.makeRequest(path, method); const parameter = 'sensitivity=TEEN'; // this parameter TEEN is hardcoded
const config = {
data: parameter,
headers: { 'Content-Type': 'text/plain' },
};
return this.makeRequest(path, method, config);
} }
disableParentalControl() { disableParentalControl() {
@@ -233,10 +240,11 @@ class Api {
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
changeLanguage(config) { changeLanguage(lang) {
const { path, method } = this.CHANGE_LANGUAGE; const { path, method } = this.CHANGE_LANGUAGE;
const parameters = { const parameters = {
data: config, data: lang,
headers: { 'Content-Type': 'text/plain' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -272,14 +280,16 @@ class Api {
const { path, method } = this.DHCP_SET_CONFIG; const { path, method } = this.DHCP_SET_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
findActiveDhcp(req) { findActiveDhcp(name) {
const { path, method } = this.DHCP_FIND_ACTIVE; const { path, method } = this.DHCP_FIND_ACTIVE;
const parameters = { const parameters = {
data: req, data: name,
headers: { 'Content-Type': 'text/plain' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -288,6 +298,7 @@ class Api {
const { path, method } = this.DHCP_ADD_STATIC_LEASE; const { path, method } = this.DHCP_ADD_STATIC_LEASE;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -296,6 +307,7 @@ class Api {
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE; const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -326,6 +338,7 @@ class Api {
const { path, method } = this.INSTALL_CONFIGURE; const { path, method } = this.INSTALL_CONFIGURE;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -334,6 +347,7 @@ class Api {
const { path, method } = this.INSTALL_CHECK_CONFIG; const { path, method } = this.INSTALL_CHECK_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -354,6 +368,7 @@ class Api {
const { path, method } = this.TLS_CONFIG; const { path, method } = this.TLS_CONFIG;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -362,6 +377,7 @@ class Api {
const { path, method } = this.TLS_VALIDATE; const { path, method } = this.TLS_VALIDATE;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -386,6 +402,7 @@ class Api {
const { path, method } = this.ADD_CLIENT; const { path, method } = this.ADD_CLIENT;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -394,6 +411,7 @@ class Api {
const { path, method } = this.DELETE_CLIENT; const { path, method } = this.DELETE_CLIENT;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -402,6 +420,7 @@ class Api {
const { path, method } = this.UPDATE_CLIENT; const { path, method } = this.UPDATE_CLIENT;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -426,6 +445,7 @@ class Api {
const { path, method } = this.ACCESS_SET; const { path, method } = this.ACCESS_SET;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -446,6 +466,7 @@ class Api {
const { path, method } = this.REWRITE_ADD; const { path, method } = this.REWRITE_ADD;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -454,6 +475,7 @@ class Api {
const { path, method } = this.REWRITE_DELETE; const { path, method } = this.REWRITE_DELETE;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -479,6 +501,7 @@ class Api {
const { path, method } = this.BLOCKED_SERVICES_SET; const { path, method } = this.BLOCKED_SERVICES_SET;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
@@ -506,6 +529,7 @@ class Api {
const { path, method } = this.STATS_CONFIG; const { path, method } = this.STATS_CONFIG;
const config = { const config = {
data, data,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
@@ -541,6 +565,7 @@ class Api {
const { path, method } = this.QUERY_LOG_CONFIG; const { path, method } = this.QUERY_LOG_CONFIG;
const config = { const config = {
data, data,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
@@ -557,6 +582,7 @@ class Api {
const { path, method } = this.LOGIN; const { path, method } = this.LOGIN;
const config = { const config = {
data, data,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }
@@ -583,6 +609,7 @@ class Api {
const { path, method } = this.SET_DNS_CONFIG; const { path, method } = this.SET_DNS_CONFIG;
const config = { const config = {
data, data,
headers: { 'Content-Type': 'application/json' },
}; };
return this.makeRequest(path, method, config); return this.makeRequest(path, method, config);
} }

16
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.18 go 1.18
require ( require (
github.com/AdguardTeam/dnsproxy v0.45.2 github.com/AdguardTeam/dnsproxy v0.44.0
github.com/AdguardTeam/golibs v0.10.9 github.com/AdguardTeam/golibs v0.10.9
github.com/AdguardTeam/urlfilter v0.16.0 github.com/AdguardTeam/urlfilter v0.16.0
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
@@ -18,7 +18,7 @@ require (
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84 github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
github.com/kardianos/service v1.2.1 github.com/kardianos/service v1.2.1
github.com/lucas-clemente/quic-go v0.29.1 github.com/lucas-clemente/quic-go v0.29.0
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
github.com/mdlayher/netlink v1.6.0 github.com/mdlayher/netlink v1.6.0
// TODO(a.garipov): This package is deprecated; find a new one or use // TODO(a.garipov): This package is deprecated; find a new one or use
@@ -28,10 +28,10 @@ require (
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/ti-mo/netfilter v0.4.0 github.com/ti-mo/netfilter v0.4.0
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91
golang.org/x/net v0.0.0-20220927171203-f486391704dc golang.org/x/net v0.0.0-20220906165146-f3363e06e74c
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0 howett.net/plist v1.0.0
@@ -43,12 +43,10 @@ require (
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/josharian/native v1.0.0 // indirect github.com/josharian/native v1.0.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
github.com/mdlayher/packet v1.0.0 // indirect github.com/mdlayher/packet v1.0.0 // indirect
@@ -59,7 +57,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e // indirect golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 // indirect
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.1.12 // indirect

35
go.sum
View File

@@ -1,5 +1,5 @@
github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko= github.com/AdguardTeam/dnsproxy v0.44.0 h1:JzIxEXF4OyJq4wZVHeZkM1af4VfuwcgrUzjgdBGljsE=
github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE= github.com/AdguardTeam/dnsproxy v0.44.0/go.mod h1:HsxYYW/bC8uo+4eX9pRW21hFD6gWZdrvcfBb1R6/AzU=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw= github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0= github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
@@ -23,8 +23,6 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -90,10 +88,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0= github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE=
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
@@ -129,7 +125,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@@ -173,16 +168,16 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 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.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e h1:WhB000cGjOfbJiedMGvJkMTclI18VD69w27k+sceql8= golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0=
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -194,7 +189,6 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -206,8 +200,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -230,7 +224,6 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -254,8 +247,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 h1:C1tElbkWrsSkn3IRl1GCW/gETw1TywWIPgwZtXTZbYg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -2,12 +2,10 @@
package aghhttp package aghhttp
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@@ -33,43 +31,6 @@ func OK(w http.ResponseWriter) {
// Error writes formatted message to w and also logs it. // Error writes formatted message to w and also logs it.
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) { func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
text := fmt.Sprintf(format, args...) text := fmt.Sprintf(format, args...)
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text) log.Error("%s %s: %s", r.Method, r.URL, text)
http.Error(w, text, code) http.Error(w, text, code)
} }
// UserAgent returns the ID of the service as a User-Agent string. It can also
// be used as the value of the Server HTTP header.
func UserAgent() (ua string) {
return fmt.Sprintf("AdGuardHome/%s", version.Version())
}
// textPlainDeprMsg is the message returned to API users when they try to use
// an API that used to accept "text/plain" but doesn't anymore.
const textPlainDeprMsg = `using this api with the text/plain content-type is deprecated; ` +
`use application/json`
// WriteTextPlainDeprecated responds to the request with a message about
// deprecation and removal of a plain-text API if the request is made with the
// "text/plain" content-type.
func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainText bool) {
if r.Header.Get(HdrNameContentType) != HdrValTextPlain {
return false
}
Error(r, w, http.StatusUnsupportedMediaType, textPlainDeprMsg)
return true
}
// WriteJSONResponse sets the content-type header in w.Header() to
// "application/json", encodes resp to w, calls Error on any returned error, and
// returns it as well.
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
w.Header().Set(HdrNameContentType, HdrValApplicationJSON)
err = json.NewEncoder(w).Encode(resp)
if err != nil {
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
}
return err
}

View File

@@ -1,22 +0,0 @@
package aghhttp
// HTTP Headers
// HTTP header name constants.
//
// TODO(a.garipov): Remove unused.
const (
HdrNameAcceptEncoding = "Accept-Encoding"
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HdrNameContentEncoding = "Content-Encoding"
HdrNameContentType = "Content-Type"
HdrNameServer = "Server"
HdrNameTrailer = "Trailer"
HdrNameUserAgent = "User-Agent"
)
// HTTP header value constants.
const (
HdrValApplicationJSON = "application/json"
HdrValTextPlain = "text/plain"
)

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"syscall" "syscall"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
@@ -31,6 +32,12 @@ var (
// the IP being static is available. // the IP being static is available.
const ErrNoStaticIPInfo errors.Error = "no information about static ip" const ErrNoStaticIPInfo errors.Error = "no information about static ip"
// IPv4Localhost returns 127.0.0.1, which returns true for [netip.Addr.Is4].
func IPv4Localhost() (ip netip.Addr) { return netip.AddrFrom4([4]byte{0: 127, 3: 1}) }
// IPv6Localhost returns ::1, which returns true for [netip.Addr.Is6].
func IPv6Localhost() (ip netip.Addr) { return netip.AddrFrom16([16]byte{15: 1}) }
// IfaceHasStaticIP checks if interface is configured to have static IP address. // IfaceHasStaticIP checks if interface is configured to have static IP address.
// If it can't give a definitive answer, it returns false and an error for which // If it can't give a definitive answer, it returns false and an error for which
// errors.Is(err, ErrNoStaticIPInfo) is true. // errors.Is(err, ErrNoStaticIPInfo) is true.
@@ -47,26 +54,31 @@ func IfaceSetStaticIP(ifaceName string) (err error) {
// //
// TODO(e.burkov): Investigate if the gateway address may be fetched in another // TODO(e.burkov): Investigate if the gateway address may be fetched in another
// way since not every machine has the software installed. // way since not every machine has the software installed.
func GatewayIP(ifaceName string) (ip net.IP) { func GatewayIP(ifaceName string) (ip netip.Addr) {
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName) code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
if err != nil { if err != nil {
log.Debug("%s", err) log.Debug("%s", err)
return nil return ip
} else if code != 0 { } else if code != 0 {
log.Debug("fetching gateway ip: unexpected exit code: %d", code) log.Debug("fetching gateway ip: unexpected exit code: %d", code)
return nil return ip
} }
fields := bytes.Fields(out) fields := bytes.Fields(out)
// The meaningful "ip route" command output should contain the word // The meaningful "ip route" command output should contain the word
// "default" at first field and default gateway IP address at third field. // "default" at first field and default gateway IP address at third field.
if len(fields) < 3 || string(fields[0]) != "default" { if len(fields) < 3 || string(fields[0]) != "default" {
return nil return ip
} }
return net.ParseIP(string(fields[2])) ip, err = netip.ParseAddr(string(fields[2]))
if err != nil {
return netip.Addr{}
}
return ip
} }
// CanBindPrivilegedPorts checks if current process can bind to privileged // CanBindPrivilegedPorts checks if current process can bind to privileged
@@ -78,9 +90,9 @@ func CanBindPrivilegedPorts() (can bool, err error) {
// NetInterface represents an entry of network interfaces map. // NetInterface represents an entry of network interfaces map.
type NetInterface struct { type NetInterface struct {
// Addresses are the network interface addresses. // Addresses are the network interface addresses.
Addresses []net.IP `json:"ip_addresses,omitempty"` Addresses []netip.Addr `json:"ip_addresses,omitempty"`
// Subnets are the IP networks for this network interface. // Subnets are the IP networks for this network interface.
Subnets []*net.IPNet `json:"-"` Subnets []netip.Prefix `json:"-"`
Name string `json:"name"` Name string `json:"name"`
HardwareAddr net.HardwareAddr `json:"hardware_address"` HardwareAddr net.HardwareAddr `json:"hardware_address"`
Flags net.Flags `json:"flags"` Flags net.Flags `json:"flags"`
@@ -101,57 +113,78 @@ func (iface NetInterface) MarshalJSON() ([]byte, error) {
}) })
} }
func NetInterfaceFrom(iface *net.Interface) (niface *NetInterface, err error) {
niface = &NetInterface{
Name: iface.Name,
HardwareAddr: iface.HardwareAddr,
Flags: iface.Flags,
MTU: iface.MTU,
}
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
}
// Collect network interface addresses.
for _, addr := range addrs {
n, ok := addr.(*net.IPNet)
if !ok {
// Should be *net.IPNet, this is weird.
return nil, fmt.Errorf("expected %[2]s to be %[1]T, got %[2]T", n, addr)
} else if ip4 := n.IP.To4(); ip4 != nil {
n.IP = ip4
}
ip, ok := netip.AddrFromSlice(n.IP)
if !ok {
return nil, fmt.Errorf("bad address %s", n.IP)
}
if ip.IsLinkLocalUnicast() {
// Ignore link-local IPv4.
if ip.Is4() {
continue
}
ip = ip.WithZone(iface.Name)
}
ones, _ := n.Mask.Size()
p := netip.PrefixFrom(ip, ones)
niface.Addresses = append(niface.Addresses, ip)
niface.Subnets = append(niface.Subnets, p)
}
return niface, nil
}
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and // GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and
// WEB only we do not return link-local addresses here. // WEB only we do not return link-local addresses here.
// //
// TODO(e.burkov): Can't properly test the function since it's nontrivial to // TODO(e.burkov): Can't properly test the function since it's nontrivial to
// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used. // substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.
func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) { func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {
ifaces, err := net.Interfaces() ifaces, err := net.Interfaces()
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't get interfaces: %w", err) return nil, fmt.Errorf("getting interfaces: %w", err)
} else if len(ifaces) == 0 { } else if len(ifaces) == 0 {
return nil, errors.Error("couldn't find any legible interface") return nil, errors.Error("no legible interfaces")
} }
for _, iface := range ifaces { for i := range ifaces {
var addrs []net.Addr var niface *NetInterface
addrs, err = iface.Addrs() niface, err = NetInterfaceFrom(&ifaces[i])
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err) return nil, err
} } else if len(niface.Addresses) != 0 {
netIface := &NetInterface{
MTU: iface.MTU,
Name: iface.Name,
HardwareAddr: iface.HardwareAddr,
Flags: iface.Flags,
}
// Collect network interface addresses.
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
// Should be net.IPNet, this is weird.
return nil, fmt.Errorf("got %s that is not net.IPNet, it is %T", addr, addr)
}
// Ignore link-local.
if ipNet.IP.IsLinkLocalUnicast() {
continue
}
netIface.Addresses = append(netIface.Addresses, ipNet.IP)
netIface.Subnets = append(netIface.Subnets, ipNet)
}
// Discard interfaces with no addresses. // Discard interfaces with no addresses.
if len(netIface.Addresses) != 0 { nifaces = append(nifaces, niface)
netIfaces = append(netIfaces, netIface)
} }
} }
return netIfaces, nil return nifaces, nil
} }
// InterfaceByIP returns the name of the interface bound to ip. // InterfaceByIP returns the name of the interface bound to ip.
@@ -160,7 +193,7 @@ func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
// IP address can be shared by multiple interfaces in some configurations. // IP address can be shared by multiple interfaces in some configurations.
// //
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb. // TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func InterfaceByIP(ip net.IP) (ifaceName string) { func InterfaceByIP(ip netip.Addr) (ifaceName string) {
ifaces, err := GetValidNetInterfacesForWeb() ifaces, err := GetValidNetInterfacesForWeb()
if err != nil { if err != nil {
return "" return ""
@@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
for _, iface := range ifaces { for _, iface := range ifaces {
for _, addr := range iface.Addresses { for _, addr := range iface.Addresses {
if ip.Equal(addr) { if ip == addr {
return iface.Name return iface.Name
} }
} }
@@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
return "" return ""
} }
// GetSubnet returns pointer to net.IPNet for the specified interface or nil if // GetSubnet returns the subnet corresponding to the interface of zero prefix if
// the search fails. // the search fails.
// //
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb. // TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func GetSubnet(ifaceName string) *net.IPNet { func GetSubnet(ifaceName string) (p netip.Prefix) {
netIfaces, err := GetValidNetInterfacesForWeb() netIfaces, err := GetValidNetInterfacesForWeb()
if err != nil { if err != nil {
log.Error("Could not get network interfaces info: %v", err) log.Error("Could not get network interfaces info: %v", err)
return nil
return p
} }
for _, netIface := range netIfaces { for _, netIface := range netIfaces {
@@ -194,14 +228,14 @@ func GetSubnet(ifaceName string) *net.IPNet {
} }
} }
return nil return p
} }
// CheckPort checks if the port is available for binding. network is expected // CheckPort checks if the port is available for binding. network is expected
// to be one of "udp" and "tcp". // to be one of "udp" and "tcp".
func CheckPort(network string, ip net.IP, port int) (err error) { func CheckPort(network string, ipp netip.AddrPort) (err error) {
var c io.Closer var c io.Closer
addr := netutil.IPPort{IP: ip, Port: port}.String() addr := ipp.String()
switch network { switch network {
case "tcp": case "tcp":
c, err = net.Listen(network, addr) c, err = net.Listen(network, addr)

View File

@@ -6,7 +6,7 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"net" "net/netip"
"os" "os"
"strings" "strings"
@@ -151,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
// interface through dhcpcd.conf. // interface through dhcpcd.conf.
func ifaceSetStaticIP(ifaceName string) (err error) { func ifaceSetStaticIP(ifaceName string) (err error) {
ipNet := GetSubnet(ifaceName) ipNet := GetSubnet(ifaceName)
if ipNet.IP == nil { if !ipNet.Addr().IsValid() {
return errors.Error("can't get IP address") return errors.Error("can't get IP address")
} }
@@ -174,7 +174,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that // dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that
// configure the interface to have a static IP. // configure the interface to have a static IP.
func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf string) { func dhcpcdConfIface(ifaceName string, subnet netip.Prefix, gateway netip.Addr) (conf string) {
b := &strings.Builder{} b := &strings.Builder{}
stringutil.WriteToBuilder( stringutil.WriteToBuilder(
b, b,
@@ -183,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri
" added by AdGuard Home.\ninterface ", " added by AdGuard Home.\ninterface ",
ifaceName, ifaceName,
"\nstatic ip_address=", "\nstatic ip_address=",
ipNet.String(), subnet.String(),
"\n", "\n",
) )
if gwIP != nil { if gateway.IsValid() {
stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n") stringutil.WriteToBuilder(b, "static routers=", gateway.String(), "\n")
} }
stringutil.WriteToBuilder(b, "static domain_name_servers=", ipNet.IP.String(), "\n\n") stringutil.WriteToBuilder(b, "static domain_name_servers=", subnet.Addr().String(), "\n\n")
return b.String() return b.String()
} }

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"net" "net"
"net/netip"
"os" "os"
"strings" "strings"
"testing" "testing"
@@ -93,34 +94,34 @@ func TestGatewayIP(t *testing.T) {
const cmd = "ip route show dev " + ifaceName const cmd = "ip route show dev " + ifaceName
testCases := []struct { testCases := []struct {
name string
shell mapShell shell mapShell
want net.IP want netip.Addr
name string
}{{ }{{
name: "success_v4",
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil), shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
want: net.IP{1, 2, 3, 4}.To16(), want: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
name: "success_v4",
}, { }, {
name: "success_v6",
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil), shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
want: net.IP{ want: netip.AddrFrom16([16]byte{
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF,
}, }),
name: "success_v6",
}, { }, {
name: "bad_output",
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil), shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
want: nil, want: netip.Addr{},
name: "bad_output",
}, { }, {
name: "err_runcmd",
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")), shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
want: nil, want: netip.Addr{},
name: "err_runcmd",
}, { }, {
name: "bad_code",
shell: theOnlyCmd(cmd, 1, "", nil), shell: theOnlyCmd(cmd, 1, "", nil),
want: nil, want: netip.Addr{},
name: "bad_code",
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@@ -198,17 +199,21 @@ func TestBroadcastFromIPNet(t *testing.T) {
} }
func TestCheckPort(t *testing.T) { func TestCheckPort(t *testing.T) {
laddr := netip.AddrPortFrom(IPv4Localhost(), 0)
t.Run("tcp_bound", func(t *testing.T) { t.Run("tcp_bound", func(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:") l, err := net.Listen("tcp", laddr.String())
require.NoError(t, err) require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, l.Close) testutil.CleanupAndRequireSuccess(t, l.Close)
ipp := netutil.IPPortFromAddr(l.Addr()) addr := l.Addr()
require.NotNil(t, ipp) require.IsType(t, new(net.TCPAddr), addr)
require.NotNil(t, ipp.IP)
require.NotZero(t, ipp.Port)
err = CheckPort("tcp", ipp.IP, ipp.Port) ipp := addr.(*net.TCPAddr).AddrPort()
require.Equal(t, laddr.Addr(), ipp.Addr())
require.NotZero(t, ipp.Port())
err = CheckPort("tcp", ipp)
target := &net.OpError{} target := &net.OpError{}
require.ErrorAs(t, err, &target) require.ErrorAs(t, err, &target)
@@ -216,16 +221,18 @@ func TestCheckPort(t *testing.T) {
}) })
t.Run("udp_bound", func(t *testing.T) { t.Run("udp_bound", func(t *testing.T) {
conn, err := net.ListenPacket("udp", "127.0.0.1:") conn, err := net.ListenPacket("udp", laddr.String())
require.NoError(t, err) require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, conn.Close) testutil.CleanupAndRequireSuccess(t, conn.Close)
ipp := netutil.IPPortFromAddr(conn.LocalAddr()) addr := conn.LocalAddr()
require.NotNil(t, ipp) require.IsType(t, new(net.UDPAddr), addr)
require.NotNil(t, ipp.IP)
require.NotZero(t, ipp.Port)
err = CheckPort("udp", ipp.IP, ipp.Port) ipp := addr.(*net.UDPAddr).AddrPort()
require.Equal(t, laddr.Addr(), ipp.Addr())
require.NotZero(t, ipp.Port())
err = CheckPort("udp", ipp)
target := &net.OpError{} target := &net.OpError{}
require.ErrorAs(t, err, &target) require.ErrorAs(t, err, &target)
@@ -233,12 +240,12 @@ func TestCheckPort(t *testing.T) {
}) })
t.Run("bad_network", func(t *testing.T) { t.Run("bad_network", func(t *testing.T) {
err := CheckPort("bad_network", nil, 0) err := CheckPort("bad_network", netip.AddrPortFrom(netip.Addr{}, 0))
assert.NoError(t, err) assert.NoError(t, err)
}) })
t.Run("can_bind", func(t *testing.T) { t.Run("can_bind", func(t *testing.T) {
err := CheckPort("udp", net.IP{0, 0, 0, 0}, 0) err := CheckPort("udp", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
assert.NoError(t, err) assert.NoError(t, err)
}) })
} }
@@ -322,18 +329,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) {
`"mtu":1500` + `"mtu":1500` +
`}` + "\n" `}` + "\n"
ip4, ip6 := net.IP{1, 2, 3, 4}, net.IP{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen) require.True(t, ok)
ip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
require.True(t, ok)
net4 := netip.PrefixFrom(ip4, 24)
net6 := netip.PrefixFrom(ip6, 8)
iface := &NetInterface{ iface := &NetInterface{
Addresses: []net.IP{ip4, ip6}, Addresses: []netip.Addr{ip4, ip6},
Subnets: []*net.IPNet{{ Subnets: []netip.Prefix{net4, net6},
IP: ip4.Mask(mask4),
Mask: mask4,
}, {
IP: ip6.Mask(mask6),
Mask: mask6,
}},
Name: "iface0", Name: "iface0",
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
Flags: net.FlagUp | net.FlagMulticast, Flags: net.FlagUp | net.FlagMulticast,

View File

@@ -5,9 +5,11 @@ package dhcpd
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"os" "os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -347,6 +349,8 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
return return
} }
// ignore link-local // ignore link-local
//
// TODO(e.burkov): Try to listen DHCP on LLA as well.
if ipnet.IP.IsLinkLocalUnicast() { if ipnet.IP.IsLinkLocalUnicast() {
continue continue
} }
@@ -357,7 +361,7 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
} }
} }
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {
jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name) jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name).AsSlice()
response[iface.Name] = jsonIface response[iface.Name] = jsonIface
} }
} }
@@ -408,37 +412,31 @@ type dhcpSearchResult struct {
V6 dhcpSearchV6Result `json:"v6"` V6 dhcpSearchV6Result `json:"v6"`
} }
// findActiveServerReq is the JSON structure for the request to find active DHCP // Perform the following tasks:
// servers. // . Search for another DHCP server running
type findActiveServerReq struct { // . Check if a static IP is configured for the network interface
Interface string `json:"interface"` // Respond with results
}
// handleDHCPFindActiveServer performs the following tasks:
// 1. searches for another DHCP server in the network;
// 2. check if a static IP is configured for the network interface;
// 3. responds with the results.
func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) { func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
if aghhttp.WriteTextPlainDeprecated(w, r) { // This use of ReadAll is safe, because request's body is now limited.
return body, err := io.ReadAll(r.Body)
}
req := &findActiveServerReq{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err) msg := fmt.Sprintf("failed to read request body: %s", err)
log.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
return return
} }
ifaceName := req.Interface ifaceName := strings.TrimSpace(string(body))
if ifaceName == "" { if ifaceName == "" {
aghhttp.Error(r, w, http.StatusBadRequest, "empty interface name") msg := "empty interface name specified"
log.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
return return
} }
result := &dhcpSearchResult{ result := dhcpSearchResult{
V4: dhcpSearchV4Result{ V4: dhcpSearchV4Result{
OtherServer: dhcpSearchOtherResult{ OtherServer: dhcpSearchOtherResult{
Found: "no", Found: "no",
@@ -463,14 +461,6 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
result.V4.StaticIP.IP = aghnet.GetSubnet(ifaceName).String() result.V4.StaticIP.IP = aghnet.GetSubnet(ifaceName).String()
} }
setOtherDHCPResult(ifaceName, result)
_ = aghhttp.WriteJSONResponse(w, r, result)
}
// setOtherDHCPResult sets the results of the check for another DHCP server in
// result.
func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
found4, found6, err4, err6 := aghnet.CheckOtherDHCP(ifaceName) found4, found6, err4, err6 := aghnet.CheckOtherDHCP(ifaceName)
if err4 != nil { if err4 != nil {
result.V4.OtherServer.Found = "error" result.V4.OtherServer.Found = "error"
@@ -478,13 +468,24 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
} else if found4 { } else if found4 {
result.V4.OtherServer.Found = "yes" result.V4.OtherServer.Found = "yes"
} }
if err6 != nil { if err6 != nil {
result.V6.OtherServer.Found = "error" result.V6.OtherServer.Found = "error"
result.V6.OtherServer.Error = err6.Error() result.V6.OtherServer.Error = err6.Error()
} else if found6 { } else if found6 {
result.V6.OtherServer.Found = "yes" result.V6.OtherServer.Found = "yes"
} }
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Failed to marshal DHCP found json: %s",
err,
)
}
} }
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) { func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {

View File

@@ -183,7 +183,15 @@ func (s *Server) accessListJSON() (j accessListJSON) {
} }
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) { func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
_ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON()) j := s.accessListJSON()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(j)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
return
}
} }
// validateAccessSet checks the internal accessListJSON lists. To search for // validateAccessSet checks the internal accessListJSON lists. To search for

View File

@@ -201,10 +201,6 @@ type ServerConfig struct {
// Register an HTTP handler // Register an HTTP handler
HTTPRegister aghhttp.RegisterFunc HTTPRegister aghhttp.RegisterFunc
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
// resolving PTR queries for local addresses.
LocalPTRResolvers []string
// ResolveClients signals if the RDNS should resolve clients' addresses. // ResolveClients signals if the RDNS should resolve clients' addresses.
ResolveClients bool ResolveClients bool
@@ -212,12 +208,9 @@ type ServerConfig struct {
// locally-served networks should be resolved via private PTR resolvers. // locally-served networks should be resolved via private PTR resolvers.
UsePrivateRDNS bool UsePrivateRDNS bool
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests. // LocalPTRResolvers is a slice of addresses to be used as upstreams for
ServeHTTP3 bool // resolving PTR queries for local addresses.
LocalPTRResolvers []string
// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
// upstreams.
UseHTTP3Upstreams bool
} }
// if any of ServerConfig values are zero, then default values from below are used // if any of ServerConfig values are zero, then default values from below are used
@@ -233,7 +226,6 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
conf = proxy.Config{ conf = proxy.Config{
UDPListenAddr: srvConf.UDPListenAddrs, UDPListenAddr: srvConf.UDPListenAddrs,
TCPListenAddr: srvConf.TCPListenAddrs, TCPListenAddr: srvConf.TCPListenAddrs,
HTTP3: srvConf.ServeHTTP3,
Ratelimit: int(srvConf.Ratelimit), Ratelimit: int(srvConf.Ratelimit),
RatelimitWhitelist: srvConf.RatelimitWhitelist, RatelimitWhitelist: srvConf.RatelimitWhitelist,
RefuseAny: srvConf.RefuseAny, RefuseAny: srvConf.RefuseAny,
@@ -332,20 +324,6 @@ func (s *Server) initDefaultSettings() {
} }
} }
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
// depending on configuration.
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
if !http3 {
return upstream.DefaultHTTPVersions
}
return []upstream.HTTPVersion{
upstream.HTTPVersion3,
upstream.HTTPVersion2,
upstream.HTTPVersion11,
}
}
// prepareUpstreamSettings - prepares upstream DNS server settings // prepareUpstreamSettings - prepares upstream DNS server settings
func (s *Server) prepareUpstreamSettings() error { func (s *Server) prepareUpstreamSettings() error {
// We're setting a customized set of RootCAs // We're setting a customized set of RootCAs
@@ -375,14 +353,12 @@ func (s *Server) prepareUpstreamSettings() error {
upstreams = s.conf.UpstreamDNS upstreams = s.conf.UpstreamDNS
} }
httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams)
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty) upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
upstreamConfig, err := proxy.ParseUpstreamsConfig( upstreamConfig, err := proxy.ParseUpstreamsConfig(
upstreams, upstreams,
&upstream.Options{ &upstream.Options{
Bootstrap: s.conf.BootstrapDNS, Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout, Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
}, },
) )
if err != nil { if err != nil {
@@ -397,7 +373,6 @@ func (s *Server) prepareUpstreamSettings() error {
&upstream.Options{ &upstream.Options{
Bootstrap: s.conf.BootstrapDNS, Bootstrap: s.conf.BootstrapDNS,
Timeout: s.conf.UpstreamTimeout, Timeout: s.conf.UpstreamTimeout,
HTTPVersions: httpVersions,
}, },
) )
if err != nil { if err != nil {

View File

@@ -151,7 +151,7 @@ func (s *Server) checkHostRules(host string, rrtype uint16, setts *filtering.Set
} }
// filterDNSResponse checks each resource record of the response's answer // filterDNSResponse checks each resource record of the response's answer
// section from pctx and returns a non-nil res if at least one of canonical // section from pctx and returns a non-nil res if at least one of canonnical
// names or IP addresses in it matches the filtering rules. // names or IP addresses in it matches the filtering rules.
func (s *Server) filterDNSResponse( func (s *Server) filterDNSResponse(
pctx *proxy.DNSContext, pctx *proxy.DNSContext,

View File

@@ -112,7 +112,13 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
DefautLocalPTRUpstreams: defLocalPTRUps, DefautLocalPTRUpstreams: defLocalPTRUps,
} }
_ = aghhttp.WriteJSONResponse(w, r, resp) w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(resp); err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encoder: %s", err)
return
}
} }
func (req *jsonDNSConfig) checkBlockingMode() (err error) { func (req *jsonDNSConfig) checkBlockingMode() (err error) {
@@ -343,10 +349,7 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
conf, err = proxy.ParseUpstreamsConfig( conf, err = proxy.ParseUpstreamsConfig(
upstreams, upstreams,
&upstream.Options{ &upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
Bootstrap: []string{},
Timeout: DefaultTimeout,
},
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -409,15 +412,7 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
return nil return nil
} }
var protocols = []string{ var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"}
"h3://",
"https://",
"quic://",
"sdns://",
"tcp://",
"tls://",
"udp://",
}
// validateUpstream returns an error if u alongside with domains is not a valid // validateUpstream returns an error if u alongside with domains is not a valid
// upstream configuration. useDefault is true if the upstream is // upstream configuration. useDefault is true if the upstream is
@@ -664,7 +659,24 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
result[host] = "OK" result[host] = "OK"
} }
_ = aghhttp.WriteJSONResponse(w, r, result) jsonVal, err := json.Marshal(result)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to marshal status json: %s",
err,
)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonVal)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
}
} }
// handleDoH is the DNS-over-HTTPs handler. // handleDoH is the DNS-over-HTTPs handler.
@@ -680,13 +692,11 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil { if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
aghhttp.Error(r, w, http.StatusNotFound, "Not Found") aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
return return
} }
if !s.IsRunning() { if !s.IsRunning() {
aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running") aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
return return
} }

View File

@@ -12,7 +12,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
@@ -117,8 +116,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
s.conf = tc.conf() s.conf = tc.conf()
s.handleGetConfig(w, nil) s.handleGetConfig(w, nil)
cType := w.Header().Get(aghhttp.HdrNameContentType) assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
assert.JSONEq(t, string(caseWant), w.Body.String()) assert.JSONEq(t, string(caseWant), w.Body.String())
}) })
} }

View File

@@ -3,11 +3,13 @@ package filtering
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -247,25 +249,16 @@ func (d *DNSFilter) handleFilteringSetURL(w http.ResponseWriter, r *http.Request
} }
} }
// filteringRulesReq is the JSON structure for settings custom filtering rules.
type filteringRulesReq struct {
Rules []string `json:"rules"`
}
func (d *DNSFilter) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
if aghhttp.WriteTextPlainDeprecated(w, r) { // This use of ReadAll is safe, because request's body is now limited.
return body, err := io.ReadAll(r.Body)
}
req := &filteringRulesReq{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err) aghhttp.Error(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
return return
} }
d.UserRules = req.Rules d.UserRules = strings.Split(string(body), "\n")
d.ConfigModified() d.ConfigModified()
d.EnableFilters(true) d.EnableFilters(true)
} }

View File

@@ -8,14 +8,12 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"path"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
@@ -34,8 +32,7 @@ const sessionTokenSize = 16
type session struct { type session struct {
userName string userName string
// expire is the expiration time, in seconds. expire uint32 // expiration time (in seconds)
expire uint32
} }
func (s *session) serialize() []byte { func (s *session) serialize() []byte {
@@ -68,26 +65,26 @@ func (s *session) deserialize(data []byte) bool {
// Auth - global object // Auth - global object
type Auth struct { type Auth struct {
db *bbolt.DB db *bbolt.DB
raleLimiter *authRateLimiter blocker *authRateLimiter
sessions map[string]*session sessions map[string]*session
users []webUser users []User
lock sync.Mutex lock sync.Mutex
sessionTTL uint32 sessionTTL uint32
} }
// webUser represents a user of the Web UI. // User object
type webUser struct { type User struct {
Name string `yaml:"name"` Name string `yaml:"name"`
PasswordHash string `yaml:"password"` PasswordHash string `yaml:"password"` // bcrypt hash
} }
// InitAuth - create a global object // InitAuth - create a global object
func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter *authRateLimiter) *Auth { func InitAuth(dbFilename string, users []User, sessionTTL uint32, blocker *authRateLimiter) *Auth {
log.Info("Initializing auth module: %s", dbFilename) log.Info("Initializing auth module: %s", dbFilename)
a := &Auth{ a := &Auth{
sessionTTL: sessionTTL, sessionTTL: sessionTTL,
raleLimiter: rateLimiter, blocker: blocker,
sessions: make(map[string]*session), sessions: make(map[string]*session),
users: users, users: users,
} }
@@ -329,25 +326,35 @@ func newSessionToken() (data []byte, err error) {
return randData, nil return randData, nil
} }
// newCookie creates a new authentication cookie. // cookieTimeFormat is the format to be used in (time.Time).Format for cookie's
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) { // expiry field.
rateLimiter := a.raleLimiter const cookieTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
u, ok := a.findUser(req.Name, req.Password)
if !ok { // cookieExpiryFormat returns the formatted exp to be used in cookie string.
if rateLimiter != nil { // It's quite simple for now, but probably will be expanded in the future.
rateLimiter.inc(addr) func cookieExpiryFormat(exp time.Time) (formatted string) {
return exp.Format(cookieTimeFormat)
} }
return nil, errors.Error("invalid username or password") func (a *Auth) httpCookie(req loginJSON, addr string) (cookie string, err error) {
blocker := a.blocker
u := a.UserFind(req.Name, req.Password)
if len(u.Name) == 0 {
if blocker != nil {
blocker.inc(addr)
} }
if rateLimiter != nil { return "", err
rateLimiter.remove(addr)
} }
sess, err := newSessionToken() if blocker != nil {
blocker.remove(addr)
}
var sess []byte
sess, err = newSessionToken()
if err != nil { if err != nil {
return nil, fmt.Errorf("generating token: %w", err) return "", err
} }
now := time.Now().UTC() now := time.Now().UTC()
@@ -357,15 +364,11 @@ func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error)
expire: uint32(now.Unix()) + a.sessionTTL, expire: uint32(now.Unix()) + a.sessionTTL,
}) })
return &http.Cookie{ return fmt.Sprintf(
Name: sessionCookieName, "%s=%s; Path=/; HttpOnly; Expires=%s",
Value: hex.EncodeToString(sess), sessionCookieName, hex.EncodeToString(sess),
Path: "/", cookieExpiryFormat(now.Add(cookieTTL)),
Expires: now.Add(cookieTTL), ), nil
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
} }
// realIP extracts the real IP address of the client from an HTTP request using // realIP extracts the real IP address of the client from an HTTP request using
@@ -433,8 +436,8 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil { if blocker := Context.auth.blocker; blocker != nil {
if left := rateLimiter.check(remoteAddr); left > 0 { if left := blocker.check(remoteAddr); left > 0 {
w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds()))) w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds())))
aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left) aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left)
@@ -442,9 +445,10 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
} }
} }
cookie, err := Context.auth.newCookie(req, remoteAddr) var cookie string
cookie, err = Context.auth.httpCookie(req, remoteAddr)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusForbidden, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "crypto rand reader: %s", err)
return return
} }
@@ -458,11 +462,20 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
log.Error("auth: unknown ip") log.Error("auth: unknown ip")
} }
if len(cookie) == 0 {
log.Info("auth: failed to login user %q from ip %v", req.Name, ip)
time.Sleep(1 * time.Second)
http.Error(w, "invalid username or password", http.StatusBadRequest)
return
}
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip) log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
http.SetCookie(w, cookie)
h := w.Header() h := w.Header()
h.Set("Set-Cookie", cookie)
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate") h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
h.Set("Pragma", "no-cache") h.Set("Pragma", "no-cache")
h.Set("Expires", "0") h.Set("Expires", "0")
@@ -471,31 +484,17 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
} }
func handleLogout(w http.ResponseWriter, r *http.Request) { func handleLogout(w http.ResponseWriter, r *http.Request) {
respHdr := w.Header() cookie := r.Header.Get("Cookie")
c, err := r.Cookie(sessionCookieName) sess := parseCookie(cookie)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// The user is already logged out.
respHdr.Set("Location", "/login.html")
w.WriteHeader(http.StatusFound)
return Context.auth.RemoveSession(sess)
}
Context.auth.RemoveSession(c.Value) w.Header().Set("Location", "/login.html")
c = &http.Cookie{ s := fmt.Sprintf("%s=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT",
Name: sessionCookieName, sessionCookieName)
Value: "", w.Header().Set("Set-Cookie", s)
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
respHdr.Set("Location", "/login.html")
respHdr.Set("Set-Cookie", c.String())
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
} }
@@ -505,108 +504,101 @@ func RegisterAuthHandlers() {
httpRegister(http.MethodGet, "/control/logout", handleLogout) httpRegister(http.MethodGet, "/control/logout", handleLogout)
} }
// optionalAuthThird return true if user should authenticate first. func parseCookie(cookie string) string {
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) { pairs := strings.Split(cookie, ";")
if glProcessCookie(r) { for _, pair := range pairs {
log.Debug("auth: authentication is handled by GL-Inet submodule") pair = strings.TrimSpace(pair)
kv := strings.SplitN(pair, "=", 2)
return false if len(kv) != 2 {
continue
}
if kv[0] == sessionCookieName {
return kv[1]
}
}
return ""
} }
// optionalAuthThird return true if user should authenticate first.
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (authFirst bool) {
authFirst = false
// redirect to login page if not authenticated // redirect to login page if not authenticated
isAuthenticated := false ok := false
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie]. if glProcessCookie(r) {
// Check Basic authentication. log.Debug("auth: authentication was handled by GL-Inet submodule")
user, pass, hasBasic := r.BasicAuth() ok = true
if hasBasic { } else if err == nil {
_, isAuthenticated = Context.auth.findUser(user, pass) r := Context.auth.checkSession(cookie.Value)
if !isAuthenticated { if r == checkSessionOK {
ok = true
} else if r < 0 {
log.Debug("auth: invalid cookie value: %s", cookie)
}
} else {
// there's no Cookie, check Basic authentication
user, pass, ok2 := r.BasicAuth()
if ok2 {
u := Context.auth.UserFind(user, pass)
if len(u.Name) != 0 {
ok = true
} else {
log.Info("auth: invalid Basic Authorization value") log.Info("auth: invalid Basic Authorization value")
} }
} }
} else {
res := Context.auth.checkSession(cookie.Value)
isAuthenticated = res == checkSessionOK
if !isAuthenticated {
log.Debug("auth: invalid cookie value: %s", cookie)
} }
} if !ok {
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
if isAuthenticated {
return false
}
if p := r.URL.Path; p == "/" || p == "/index.html" {
if glProcessRedirect(w, r) { if glProcessRedirect(w, r) {
log.Debug("auth: redirected to login page by GL-Inet submodule") log.Debug("auth: redirected to login page by GL-Inet submodule")
} else { } else {
log.Debug("auth: redirected to login page")
w.Header().Set("Location", "/login.html") w.Header().Set("Location", "/login.html")
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
} }
} else { } else {
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("Forbidden")) _, _ = w.Write([]byte("Forbidden"))
} }
authFirst = true
return true
} }
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the return authFirst
// project. }
func optionalAuth(
h func(http.ResponseWriter, *http.Request), func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path if r.URL.Path == "/login.html" {
// redirect to dashboard if already authenticated
authRequired := Context.auth != nil && Context.auth.AuthRequired() authRequired := Context.auth != nil && Context.auth.AuthRequired()
if p == "/login.html" {
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil { if authRequired && err == nil {
// Redirect to the dashboard if already authenticated. r := Context.auth.checkSession(cookie.Value)
res := Context.auth.checkSession(cookie.Value) if r == checkSessionOK {
if res == checkSessionOK {
w.Header().Set("Location", "/") w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
return return
} } else if r == checkSessionNotFound {
log.Debug("auth: invalid cookie value: %s", cookie) log.Debug("auth: invalid cookie value: %s", cookie)
} }
} else if isPublicResource(p) { }
// Process as usual, no additional auth requirements.
} else if authRequired { } else if strings.HasPrefix(r.URL.Path, "/assets/") ||
strings.HasPrefix(r.URL.Path, "/login.") {
// process as usual
// no additional auth requirements
} else if Context.auth != nil && Context.auth.AuthRequired() {
if optionalAuthThird(w, r) { if optionalAuthThird(w, r) {
return return
} }
} }
h(w, r) handler(w, r)
} }
} }
// isPublicResource returns true if p is a path to a public resource.
func isPublicResource(p string) (ok bool) {
isAsset, err := path.Match("/assets/*", p)
if err != nil {
// The only error that is returned from path.Match is
// [path.ErrBadPattern]. This is a programmer error.
panic(fmt.Errorf("bad asset pattern: %w", err))
}
isLogin, err := path.Match("/login.*", p)
if err != nil {
// Same as above.
panic(fmt.Errorf("bad login pattern: %w", err))
}
return isAsset || isLogin
}
type authHandler struct { type authHandler struct {
handler http.Handler handler http.Handler
} }
@@ -620,7 +612,7 @@ func optionalAuthHandler(handler http.Handler) http.Handler {
} }
// UserAdd - add new user // UserAdd - add new user
func (a *Auth) UserAdd(u *webUser, password string) { func (a *Auth) UserAdd(u *User, password string) {
if len(password) == 0 { if len(password) == 0 {
return return
} }
@@ -639,35 +631,31 @@ func (a *Auth) UserAdd(u *webUser, password string) {
log.Debug("auth: added user: %s", u.Name) log.Debug("auth: added user: %s", u.Name)
} }
// findUser returns a user if there is one. // UserFind - find a user
func (a *Auth) findUser(login, password string) (u webUser, ok bool) { func (a *Auth) UserFind(login, password string) User {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
for _, u := range a.users {
for _, u = range a.users {
if u.Name == login && if u.Name == login &&
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil { bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
return u, true return u
} }
} }
return User{}
return webUser{}, false
} }
// getCurrentUser returns the current user. It returns an empty User if the // getCurrentUser returns the current user. It returns an empty User if the
// user is not found. // user is not found.
func (a *Auth) getCurrentUser(r *http.Request) (u webUser) { func (a *Auth) getCurrentUser(r *http.Request) User {
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if err != nil { if err != nil {
// There's no Cookie, check Basic authentication. // There's no Cookie, check Basic authentication.
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if ok { if ok {
u, _ = Context.auth.findUser(user, pass) return Context.auth.UserFind(user, pass)
return u
} }
return webUser{} return User{}
} }
a.lock.Lock() a.lock.Lock()
@@ -675,20 +663,20 @@ func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
s, ok := a.sessions[cookie.Value] s, ok := a.sessions[cookie.Value]
if !ok { if !ok {
return webUser{} return User{}
} }
for _, u = range a.users { for _, u := range a.users {
if u.Name == s.userName { if u.Name == s.userName {
return u return u
} }
} }
return webUser{} return User{}
} }
// GetUsers - get users // GetUsers - get users
func (a *Auth) GetUsers() []webUser { func (a *Auth) GetUsers() []User {
a.lock.Lock() a.lock.Lock()
users := a.users users := a.users
a.lock.Unlock() a.lock.Unlock()

View File

@@ -43,14 +43,14 @@ func TestAuth(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db") fn := filepath.Join(dir, "sessions.db")
users := []webUser{{ users := []User{{
Name: "name", Name: "name",
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}} }}
a := InitAuth(fn, nil, 60, nil) a := InitAuth(fn, nil, 60, nil)
s := session{} s := session{}
user := webUser{Name: "name"} user := User{Name: "name"}
a.UserAdd(&user, "password") a.UserAdd(&user, "password")
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound")) assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
@@ -84,8 +84,7 @@ func TestAuth(t *testing.T) {
a.storeSession(sess, &s) a.storeSession(sess, &s)
a.Close() a.Close()
u, ok := a.findUser("name", "password") u := a.UserFind("name", "password")
assert.True(t, ok)
assert.NotEmpty(t, u.Name) assert.NotEmpty(t, u.Name)
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
@@ -119,7 +118,7 @@ func TestAuthHTTP(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db") fn := filepath.Join(dir, "sessions.db")
users := []webUser{ users := []User{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
Context.auth = InitAuth(fn, users, 60, nil) Context.auth = InitAuth(fn, users, 60, nil)
@@ -151,19 +150,18 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
// perform login // perform login
cookie, err := Context.auth.newCookie(loginJSON{Name: "name", Password: "password"}, "") cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}, "")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, cookie) assert.NotEmpty(t, cookie)
// get / // get /
handler2 = optionalAuth(handler) handler2 = optionalAuth(handler)
w.hdr = make(http.Header) w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie.String()) r.Header.Set("Cookie", cookie)
r.URL = &url.URL{Path: "/"} r.URL = &url.URL{Path: "/"}
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del("Cookie") r.Header.Del("Cookie")
// get / with basic auth // get / with basic auth
@@ -179,7 +177,7 @@ func TestAuthHTTP(t *testing.T) {
// get login page with a valid cookie - we're redirected to / // get login page with a valid cookie - we're redirected to /
handler2 = optionalAuth(handler) handler2 = optionalAuth(handler)
w.hdr = make(http.Header) w.hdr = make(http.Header)
r.Header.Set("Cookie", cookie.String()) r.Header.Set("Cookie", cookie)
r.URL = &url.URL{Path: loginURL} r.URL = &url.URL{Path: loginURL}
handlerCalled = false handlerCalled = false
handler2(&w, &r) handler2(&w, &r)

View File

@@ -458,7 +458,6 @@ func (clients *clientsContainer) findUpstreams(
&upstream.Options{ &upstream.Options{
Bootstrap: config.DNS.BootstrapDNS, Bootstrap: config.DNS.BootstrapDNS,
Timeout: config.DNS.UpstreamTimeout.Duration, Timeout: config.DNS.UpstreamTimeout.Duration,
HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams),
}, },
) )
if err != nil { if err != nil {

View File

@@ -93,7 +93,13 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
data.Tags = clientTags data.Tags = clientTags
_ = aghhttp.WriteJSONResponse(w, r, data) w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data)
if e != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "failed to encode to json: %v", e)
return
}
} }
// Convert JSON object to Client object // Convert JSON object to Client object
@@ -243,7 +249,11 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
}) })
} }
_ = aghhttp.WriteJSONResponse(w, r, data) w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write response: %s", err)
}
} }
// findRuntime looks up the IP in runtime and temporary storages, like // findRuntime looks up the IP in runtime and temporary storages, like

View File

@@ -3,7 +3,7 @@ package home
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@@ -85,10 +85,10 @@ type configuration struct {
// It's reset after config is parsed // It's reset after config is parsed
fileData []byte fileData []byte
BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to BindHost netip.Addr `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
Users []webUser `yaml:"users"` // Users that can access HTTP server Users []User `yaml:"users"` // Users that can access HTTP server
// AuthAttempts is the maximum number of failed login attempts a user // AuthAttempts is the maximum number of failed login attempts a user
// can do before being blocked. // can do before being blocked.
AuthAttempts uint `yaml:"auth_attempts"` AuthAttempts uint `yaml:"auth_attempts"`
@@ -135,7 +135,7 @@ type configuration struct {
// field ordering is important -- yaml fields will mirror ordering from here // field ordering is important -- yaml fields will mirror ordering from here
type dnsConfig struct { type dnsConfig struct {
BindHosts []net.IP `yaml:"bind_hosts"` BindHosts []netip.Addr `yaml:"bind_hosts"`
Port int `yaml:"port"` Port int `yaml:"port"`
// time interval for statistics (in days) // time interval for statistics (in days)
@@ -166,19 +166,6 @@ type dnsConfig struct {
// LocalPTRResolvers is the slice of addresses to be used as upstreams // LocalPTRResolvers is the slice of addresses to be used as upstreams
// for PTR queries for locally-served networks. // for PTR queries for locally-served networks.
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"` LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
// ServeHTTP3 defines if HTTP/3 is be 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
// upstreams.
//
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
// experimental.
UseHTTP3Upstreams bool `yaml:"use_http3_upstreams"`
} }
type tlsConfigSettings struct { type tlsConfigSettings struct {
@@ -211,12 +198,12 @@ type tlsConfigSettings struct {
var config = &configuration{ var config = &configuration{
BindPort: 3000, BindPort: 3000,
BetaBindPort: 0, BetaBindPort: 0,
BindHost: net.IP{0, 0, 0, 0}, BindHost: netip.IPv4Unspecified(),
AuthAttempts: 5, AuthAttempts: 5,
AuthBlockMin: 15, AuthBlockMin: 15,
WebSessionTTLHours: 30 * 24, WebSessionTTLHours: 30 * 24,
DNS: dnsConfig{ DNS: dnsConfig{
BindHosts: []net.IP{{0, 0, 0, 0}}, BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS, Port: defaultPortDNS,
StatsInterval: 1, StatsInterval: 1,
QueryLogEnabled: true, QueryLogEnabled: true,

View File

@@ -1,13 +1,13 @@
package home package home
import ( import (
"encoding/json"
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
@@ -20,11 +20,11 @@ import (
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS // appendDNSAddrs is a convenient helper for appending a formatted form of DNS
// addresses to a slice of strings. // addresses to a slice of strings.
func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) { func appendDNSAddrs(dst []string, addrs ...netip.Addr) (res []string) {
for _, addr := range addrs { for _, addr := range addrs {
var hostport string var hostport string
if config.DNS.Port != defaultPortDNS { if config.DNS.Port != defaultPortDNS {
hostport = netutil.JoinHostPort(addr.String(), config.DNS.Port) hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String()
} else { } else {
hostport = addr.String() hostport = addr.String()
} }
@@ -38,7 +38,7 @@ func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to // appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to
// dst. It also adds the IP addresses of all network interfaces if src contains // dst. It also adds the IP addresses of all network interfaces if src contains
// an unspecified IP address. // an unspecified IP address.
func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err error) { func appendDNSAddrsWithIfaces(dst []string, src []netip.Addr) (res []string, err error) {
ifacesAdded := false ifacesAdded := false
for _, h := range src { for _, h := range src {
if !h.IsUnspecified() { if !h.IsUnspecified() {
@@ -71,7 +71,9 @@ func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err err
// on, including the addresses on all interfaces in cases of unspecified IPs. // on, including the addresses on all interfaces in cases of unspecified IPs.
func collectDNSAddresses() (addrs []string, err error) { func collectDNSAddresses() (addrs []string, err error) {
if hosts := config.DNS.BindHosts; len(hosts) == 0 { if hosts := config.DNS.BindHosts; len(hosts) == 0 {
addrs = appendDNSAddrs(addrs, net.IP{127, 0, 0, 1}) addr := aghnet.IPv4Localhost()
addrs = appendDNSAddrs(addrs, addr)
} else { } else {
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts) addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
if err != nil { if err != nil {
@@ -97,8 +99,6 @@ func collectDNSAddresses() (addrs []string, err error) {
// statusResponse is a response for /control/status endpoint. // statusResponse is a response for /control/status endpoint.
type statusResponse struct { type statusResponse struct {
Version string `json:"version"`
Language string `json:"language"`
DNSAddrs []string `json:"dns_addresses"` DNSAddrs []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"` DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"` HTTPPort int `json:"http_port"`
@@ -107,6 +107,8 @@ type statusResponse struct {
// openapi.yaml declares. // openapi.yaml declares.
IsDHCPAvailable bool `json:"dhcp_available"` IsDHCPAvailable bool `json:"dhcp_available"`
IsRunning bool `json:"running"` IsRunning bool `json:"running"`
Version string `json:"version"`
Language string `json:"language"`
} }
func handleStatus(w http.ResponseWriter, r *http.Request) { func handleStatus(w http.ResponseWriter, r *http.Request) {
@@ -125,12 +127,12 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
defer config.RUnlock() defer config.RUnlock()
resp = statusResponse{ resp = statusResponse{
Version: version.Version(),
DNSAddrs: dnsAddrs, DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port, DNSPort: config.DNS.Port,
HTTPPort: config.BindPort, HTTPPort: config.BindPort,
Language: config.Language,
IsRunning: isRunning(), IsRunning: isRunning(),
Version: version.Version(),
Language: config.Language,
} }
}() }()
@@ -146,7 +148,13 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
resp.IsDHCPAvailable = Context.dhcpServer != nil resp.IsDHCPAvailable = Context.dhcpServer != nil
} }
_ = aghhttp.WriteJSONResponse(w, r, resp) w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
return
}
} }
type profileJSON struct { type profileJSON struct {
@@ -154,12 +162,16 @@ type profileJSON struct {
} }
func handleGetProfile(w http.ResponseWriter, r *http.Request) { func handleGetProfile(w http.ResponseWriter, r *http.Request) {
pj := profileJSON{}
u := Context.auth.getCurrentUser(r) u := Context.auth.getCurrentUser(r)
resp := &profileJSON{ pj.Name = u.Name
Name: u.Name,
}
_ = aghhttp.WriteJSONResponse(w, r, resp) data, err := json.Marshal(pj)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
return
}
_, _ = w.Write(data)
} }
// ------------------------ // ------------------------
@@ -189,29 +201,19 @@ func httpRegister(method, url string, handler http.HandlerFunc) {
Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler))))) Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
} }
// ensure returns a wrapped handler that makes sure that the request has the // ----------------------------------
// correct method as well as additional method and header checks. // helper functions for HTTP handlers
func ensure( // ----------------------------------
method string, func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
handler func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
start := time.Now() log.Debug("%s %v", r.Method, r.URL)
m, u := r.Method, r.URL
log.Debug("started %s %s %s", m, r.Host, u)
defer func() { log.Debug("finished %s %s %s in %s", m, r.Host, u, time.Since(start)) }()
if m != method {
aghhttp.Error(r, w, http.StatusMethodNotAllowed, "only method %s is allowed", method)
if r.Method != method {
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
return return
} }
if modifiesData(m) { if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
if !ensureContentType(w, r) {
return
}
Context.controlLock.Lock() Context.controlLock.Lock()
defer Context.controlLock.Unlock() defer Context.controlLock.Unlock()
} }
@@ -220,42 +222,6 @@ func ensure(
} }
} }
// modifiesData returns true if m is an HTTP method that can modify data.
func modifiesData(m string) (ok bool) {
return m == http.MethodPost || m == http.MethodPut || m == http.MethodDelete
}
// ensureContentType makes sure that the content type of a data-modifying
// request is set correctly. If it is not, ensureContentType writes a response
// to w, and ok is false.
func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {
const statusUnsup = http.StatusUnsupportedMediaType
cType := r.Header.Get(aghhttp.HdrNameContentType)
if r.ContentLength == 0 {
if cType == "" {
return true
}
// Assume that browsers always send a content type when submitting HTML
// forms and require no content type for requests with no body to make
// sure that the request comes from JavaScript.
aghhttp.Error(r, w, statusUnsup, "empty body with content-type %q not allowed", cType)
return false
}
const wantCType = aghhttp.HdrValApplicationJSON
if cType == wantCType {
return true
}
aghhttp.Error(r, w, statusUnsup, "only content-type %s is allowed", wantCType)
return false
}
func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure(http.MethodPost, handler) return ensure(http.MethodPost, handler)
} }

View File

@@ -5,8 +5,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/netip"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -21,7 +21,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/lucas-clemente/quic-go/http3"
) )
// getAddrsResponse is the response for /install/get_addresses endpoint. // getAddrsResponse is the response for /install/get_addresses endpoint.
@@ -60,11 +59,23 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
data.Interfaces[iface.Name] = iface data.Interfaces[iface.Name] = iface
} }
_ = aghhttp.WriteJSONResponse(w, r, data) w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to marshal default addresses to json: %s",
err,
)
return
}
} }
type checkConfReqEnt struct { type checkConfReqEnt struct {
IP net.IP `json:"ip"` IP netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
Autofix bool `json:"autofix"` Autofix bool `json:"autofix"`
} }
@@ -117,7 +128,7 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
// unbound after install. // unbound after install.
} }
return aghnet.CheckPort("tcp", req.Web.IP, portInt) return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(portInt)))
} }
// validateDNS returns error if the DNS part of the initial configuration can't // validateDNS returns error if the DNS part of the initial configuration can't
@@ -142,13 +153,13 @@ func (req *checkConfReq) validateDNS(
return false, err return false, err
} }
err = aghnet.CheckPort("tcp", req.DNS.IP, port) err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
if err != nil { if err != nil {
return false, err return false, err
} }
} }
err = aghnet.CheckPort("udp", req.DNS.IP, port) err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
if !aghnet.IsAddrInUse(err) { if !aghnet.IsAddrInUse(err) {
return false, err return false, err
} }
@@ -160,7 +171,7 @@ func (req *checkConfReq) validateDNS(
log.Error("disabling DNSStubListener: %s", err) log.Error("disabling DNSStubListener: %s", err)
} }
err = aghnet.CheckPort("udp", req.DNS.IP, port) err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
canAutofix = false canAutofix = false
} }
@@ -190,13 +201,19 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP) resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
} }
_ = aghhttp.WriteJSONResponse(w, r, resp) w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding the response: %s", err)
return
}
} }
// handleStaticIP - handles static IP request // handleStaticIP - handles static IP request
// It either checks if we have a static IP // It either checks if we have a static IP
// Or if set=true, it tries to set it // Or if set=true, it tries to set it
func handleStaticIP(ip net.IP, set bool) staticIPJSON { func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
resp := staticIPJSON{} resp := staticIPJSON{}
interfaceName := aghnet.InterfaceByIP(ip) interfaceName := aghnet.InterfaceByIP(ip)
@@ -304,7 +321,7 @@ func disableDNSStubListener() error {
} }
type applyConfigReqEnt struct { type applyConfigReqEnt struct {
IP net.IP `json:"ip"` IP netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
} }
@@ -329,7 +346,6 @@ func copyInstallSettings(dst, src *configuration) {
// shutdownTimeout is the timeout for shutting HTTP server down operation. // shutdownTimeout is the timeout for shutting HTTP server down operation.
const shutdownTimeout = 5 * time.Second const shutdownTimeout = 5 * time.Second
// shutdownSrv shuts srv down and prints error messages to the log.
func shutdownSrv(ctx context.Context, srv *http.Server) { func shutdownSrv(ctx context.Context, srv *http.Server) {
defer log.OnPanic("") defer log.OnPanic("")
@@ -338,10 +354,7 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
} }
err := srv.Shutdown(ctx) err := srv.Shutdown(ctx)
if err == nil { if err != nil {
return
}
const msgFmt = "shutting down http server %q: %s" const msgFmt = "shutting down http server %q: %s"
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err) log.Debug(msgFmt, srv.Addr, err)
@@ -349,28 +362,6 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
log.Error(msgFmt, srv.Addr, err) log.Error(msgFmt, srv.Addr, err)
} }
} }
// shutdownSrv3 shuts srv down and prints error messages to the log.
//
// TODO(a.garipov): Think of a good way to merge with [shutdownSrv].
func shutdownSrv3(srv *http3.Server) {
defer log.OnPanic("")
if srv == nil {
return
}
err := srv.Close()
if err == nil {
return
}
const msgFmt = "shutting down http/3 server %q: %s"
if errors.Is(err, context.Canceled) {
log.Debug(msgFmt, srv.Addr, err)
} else {
log.Error(msgFmt, srv.Addr, err)
}
} }
// PasswordMinRunes is the minimum length of user's password in runes. // PasswordMinRunes is the minimum length of user's password in runes.
@@ -397,14 +388,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return return
} }
err = aghnet.CheckPort("udp", req.DNS.IP, req.DNS.Port) err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return return
} }
err = aghnet.CheckPort("tcp", req.DNS.IP, req.DNS.Port) err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -417,7 +408,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
Context.firstRun = false Context.firstRun = false
config.BindHost = req.Web.IP config.BindHost = req.Web.IP
config.BindPort = req.Web.Port config.BindPort = req.Web.Port
config.DNS.BindHosts = []net.IP{req.DNS.IP} config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
config.DNS.Port = req.DNS.Port config.DNS.Port = req.DNS.Port
// TODO(e.burkov): StartMods() should be put in a separate goroutine at the // TODO(e.burkov): StartMods() should be put in a separate goroutine at the
@@ -433,7 +424,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return return
} }
u := &webUser{ u := &User{
Name: req.Username, Name: req.Username,
} }
Context.auth.UserAdd(u, req.Password) Context.auth.UserAdd(u, req.Password)
@@ -490,9 +481,9 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
return nil, false, errors.Error("ports cannot be 0") return nil, false, errors.Error("ports cannot be 0")
} }
restartHTTP = !config.BindHost.Equal(req.Web.IP) || config.BindPort != req.Web.Port restartHTTP = config.BindHost != req.Web.IP || config.BindPort != req.Web.Port
if restartHTTP { if restartHTTP {
err = aghnet.CheckPort("tcp", req.Web.IP, req.Web.Port) err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port)))
if err != nil { if err != nil {
return nil, false, fmt.Errorf( return nil, false, fmt.Errorf(
"checking address %s:%d: %w", "checking address %s:%d: %w",
@@ -518,7 +509,7 @@ func (web *Web) registerInstallHandlers() {
// TODO(e.burkov): This should removed with the API v1 when the appropriate // TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default checkConfigReqEnt. // functionality will appear in default checkConfigReqEnt.
type checkConfigReqEntBeta struct { type checkConfigReqEntBeta struct {
IP []net.IP `json:"ip"` IP []netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
Autofix bool `json:"autofix"` Autofix bool `json:"autofix"`
} }
@@ -572,11 +563,16 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData) err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "encoding check_config: %s", err) aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Failed to encode 'check_config' JSON data: %s",
err,
)
return return
} }
body := nonBetaReqBody.String() body := nonBetaReqBody.String()
r.Body = io.NopCloser(strings.NewReader(body)) r.Body = io.NopCloser(strings.NewReader(body))
r.ContentLength = int64(len(body)) r.ContentLength = int64(len(body))
@@ -590,7 +586,7 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
// TODO(e.burkov): This should removed with the API v1 when the appropriate // TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default applyConfigReqEnt. // functionality will appear in default applyConfigReqEnt.
type applyConfigReqEntBeta struct { type applyConfigReqEntBeta struct {
IP []net.IP `json:"ip"` IP []netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
} }
@@ -644,7 +640,13 @@ func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Reques
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData) err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "encoding configure: %s", err) aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Failed to encode 'check_config' JSON data: %s",
err,
)
return return
} }
@@ -686,7 +688,19 @@ func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Req
data.Interfaces = ifaces data.Interfaces = ifaces
_ = aghhttp.WriteJSONResponse(w, r, data) w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Unable to marshal default addresses to json: %s",
err,
)
return
}
} }
// registerBetaInstallHandlers registers the install handlers for new client // registerBetaInstallHandlers registers the install handlers for new client

View File

@@ -28,6 +28,8 @@ type temporaryError interface {
// Get the latest available version from the Internet // Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := &versionResponse{} resp := &versionResponse{}
if Context.disableUpdate { if Context.disableUpdate {
resp.Disabled = true resp.Disabled = true
@@ -69,7 +71,10 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
return return
} }
_ = aghhttp.WriteJSONResponse(w, r, resp) err = json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "writing body: %s", err)
}
} }
// requestVersionInfo sets the VersionInfo field of resp if it can reach the // requestVersionInfo sets the VersionInfo field of resp if it can reach the

View File

@@ -3,6 +3,7 @@ package home
import ( import (
"fmt" "fmt"
"net" "net"
"net/netip"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@@ -164,33 +165,27 @@ func onDNSRequest(pctx *proxy.DNSContext) {
} }
} }
func ipsToTCPAddrs(ips []net.IP, port int) (tcpAddrs []*net.TCPAddr) { func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
if ips == nil { if ips == nil {
return nil return nil
} }
tcpAddrs = make([]*net.TCPAddr, len(ips)) tcpAddrs = make([]*net.TCPAddr, 0, len(ips))
for i, ip := range ips { for _, ip := range ips {
tcpAddrs[i] = &net.TCPAddr{ tcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
IP: ip,
Port: port,
}
} }
return tcpAddrs return tcpAddrs
} }
func ipsToUDPAddrs(ips []net.IP, port int) (udpAddrs []*net.UDPAddr) { func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
if ips == nil { if ips == nil {
return nil return nil
} }
udpAddrs = make([]*net.UDPAddr, len(ips)) udpAddrs = make([]*net.UDPAddr, 0, len(ips))
for i, ip := range ips { for _, ip := range ips {
udpAddrs[i] = &net.UDPAddr{ udpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
IP: ip,
Port: port,
}
} }
return udpAddrs return udpAddrs
@@ -200,7 +195,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
dnsConf := config.DNS dnsConf := config.DNS
hosts := dnsConf.BindHosts hosts := dnsConf.BindHosts
if len(hosts) == 0 { if len(hosts) == 0 {
hosts = []net.IP{{127, 0, 0, 1}} hosts = []netip.Addr{aghnet.IPv4Localhost()}
} }
newConf = dnsforward.ServerConfig{ newConf = dnsforward.ServerConfig{
@@ -246,18 +241,15 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
newConf.FilterHandler = applyAdditionalFiltering newConf.FilterHandler = applyAdditionalFiltering
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
newConf.ResolveClients = config.Clients.Sources.RDNS newConf.ResolveClients = config.Clients.Sources.RDNS
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
newConf.ServeHTTP3 = dnsConf.ServeHTTP3 newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
return newConf, nil return newConf, nil
} }
func newDNSCrypt(hosts []net.IP, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) { func newDNSCrypt(hosts []netip.Addr, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) {
if tlsConf.DNSCryptConfigFile == "" { if tlsConf.DNSCryptConfigFile == "" {
return dnscc, errors.Error("no dnscrypt_config_file") return dnscc, errors.Error("no dnscrypt_config_file")
} }
@@ -361,13 +353,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID) log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID)
if c.UseOwnBlockedServices { if c.UseOwnBlockedServices {
// TODO(e.burkov): Get rid of this crutch. Context.filters.ApplyBlockedServices(setts, c.BlockedServices)
svcs := c.BlockedServices
if svcs == nil {
svcs = []string{}
}
Context.filters.ApplyBlockedServices(setts, svcs)
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
} }
setts.ClientName = c.Name setts.ClientName = c.Name

View File

@@ -10,6 +10,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"net/netip"
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
@@ -277,7 +278,6 @@ func setupConfig(args options) (err error) {
config.DNS.DnsfilterConf.DataDir = Context.getDataDir() config.DNS.DnsfilterConf.DataDir = Context.getDataDir()
config.DNS.DnsfilterConf.Filters = slices.Clone(config.Filters) config.DNS.DnsfilterConf.Filters = slices.Clone(config.Filters)
config.DNS.DnsfilterConf.WhitelistFilters = slices.Clone(config.WhitelistFilters) config.DNS.DnsfilterConf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules)
config.DNS.DnsfilterConf.HTTPClient = Context.client config.DNS.DnsfilterConf.HTTPClient = Context.client
config.DHCP.WorkDir = Context.workDir config.DHCP.WorkDir = Context.workDir
@@ -340,7 +340,7 @@ func setupConfig(args options) (err error) {
} }
// override bind host/port from the console // override bind host/port from the console
if args.bindHost != nil { if args.bindHost.IsValid() {
config.BindHost = args.bindHost config.BindHost = args.bindHost
} }
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) { if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
@@ -381,11 +381,9 @@ func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
clientFS: clientFS, clientFS: clientFS,
clientBetaFS: clientBetaFS, clientBetaFS: clientBetaFS,
serveHTTP3: config.DNS.ServeHTTP3,
} }
web = newWeb(&webConf) web = CreateWeb(&webConf)
if web == nil { if web == nil {
return nil, fmt.Errorf("initializing web: %w", err) return nil, fmt.Errorf("initializing web: %w", err)
} }
@@ -411,7 +409,7 @@ func run(args options, clientBuildFS fs.FS) {
configureLogger(args) configureLogger(args)
// Print the first message after logger is configured. // Print the first message after logger is configured.
log.Info(version.Full()) log.Println(version.Full())
log.Debug("current working directory is %s", Context.workDir) log.Debug("current working directory is %s", Context.workDir)
if args.runningAsService { if args.runningAsService {
log.Info("AdGuard Home is running as a service") log.Info("AdGuard Home is running as a service")
@@ -457,9 +455,9 @@ func run(args options, clientBuildFS fs.FS) {
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db") sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
GLMode = args.glinetMode GLMode = args.glinetMode
var rateLimiter *authRateLimiter var arl *authRateLimiter
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 { if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
rateLimiter = newAuthRateLimiter( arl = newAuthRateLimiter(
time.Duration(config.AuthBlockMin)*time.Minute, time.Duration(config.AuthBlockMin)*time.Minute,
config.AuthAttempts, config.AuthAttempts,
) )
@@ -471,7 +469,7 @@ func run(args options, clientBuildFS fs.FS) {
sessFilename, sessFilename,
config.Users, config.Users,
config.WebSessionTTLHours*60*60, config.WebSessionTTLHours*60*60,
rateLimiter, arl,
) )
if Context.auth == nil { if Context.auth == nil {
log.Fatalf("Couldn't initialize Auth module") log.Fatalf("Couldn't initialize Auth module")
@@ -540,7 +538,7 @@ func checkPermissions() {
} }
// We should check if AdGuard Home is able to bind to port 53 // We should check if AdGuard Home is able to bind to port 53
err := aghnet.CheckPort("tcp", net.IP{127, 0, 0, 1}, defaultPortDNS) err := aghnet.CheckPort("tcp", netip.AddrPortFrom(aghnet.IPv4Localhost(), defaultPortDNS))
if err != nil { if err != nil {
if errors.Is(err, os.ErrPermission) { if errors.Is(err, os.ErrPermission) {
log.Fatal(`Permission check failed. log.Fatal(`Permission check failed.

View File

@@ -1,8 +1,10 @@
package home package home
import ( import (
"encoding/json" "fmt"
"io"
"net/http" "net/http"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
@@ -49,35 +51,43 @@ var allowedLanguages = stringutil.NewSet(
"zh-tw", "zh-tw",
) )
// languageJSON is the JSON structure for language requests and responses. func handleI18nCurrentLanguage(w http.ResponseWriter, _ *http.Request) {
type languageJSON struct { w.Header().Set("Content-Type", "text/plain")
Language string `json:"language"` log.Printf("config.Language is %s", config.Language)
_, err := fmt.Fprintf(w, "%s\n", config.Language)
if err != nil {
msg := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(msg)
http.Error(w, msg, http.StatusInternalServerError)
return
} }
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
log.Printf("home: language is %s", config.Language)
_ = aghhttp.WriteJSONResponse(w, r, &languageJSON{
Language: config.Language,
})
} }
func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) { func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
if aghhttp.WriteTextPlainDeprecated(w, r) { // This use of ReadAll is safe, because request's body is now limited.
return body, err := io.ReadAll(r.Body)
}
langReq := &languageJSON{}
err := json.NewDecoder(r.Body).Decode(langReq)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "reading req: %s", err) msg := fmt.Sprintf("failed to read request body: %s", err)
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return return
} }
lang := langReq.Language language := strings.TrimSpace(string(body))
if !allowedLanguages.Has(lang) { if language == "" {
aghhttp.Error(r, w, http.StatusBadRequest, "unknown language: %q", lang) msg := "empty language specified"
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
if !allowedLanguages.Has(language) {
msg := fmt.Sprintf("unknown language specified: %s", language)
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return return
} }
@@ -86,8 +96,7 @@ func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
config.Lock() config.Lock()
defer config.Unlock() defer config.Unlock()
config.Language = lang config.Language = language
log.Printf("home: language is set to %s", lang)
}() }()
onConfigModified() onConfigModified()

View File

@@ -3,12 +3,11 @@ package home
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip"
"testing" "testing"
"github.com/AdguardTeam/golibs/netutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"howett.net/plist" "howett.net/plist"
@@ -28,7 +27,7 @@ func setupDNSIPs(t testing.TB) {
config = &configuration{ config = &configuration{
DNS: dnsConfig{ DNS: dnsConfig{
BindHosts: []net.IP{netutil.IPv4Zero()}, BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS, Port: defaultPortDNS,
}, },
} }

View File

@@ -2,7 +2,7 @@ package home
import ( import (
"fmt" "fmt"
"net" "net/netip"
"os" "os"
"strconv" "strconv"
@@ -15,7 +15,7 @@ type options struct {
verbose bool // is verbose logging enabled verbose bool // is verbose logging enabled
configFilename string // path to the config file configFilename string // path to the config file
workDir string // path to the working directory where we will store the filters data and the querylog workDir string // path to the working directory where we will store the filters data and the querylog
bindHost net.IP // host address to bind HTTP server on bindHost netip.Addr // host address to bind HTTP server on
bindPort int // port to serve HTTP pages on bindPort int // port to serve HTTP pages on
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
pidFile string // File name to save PID to pidFile string // File name to save PID to
@@ -60,8 +60,8 @@ type arg struct {
// against its zero value and return nil if the parameter value is // against its zero value and return nil if the parameter value is
// zero otherwise they return a string slice of the parameter // zero otherwise they return a string slice of the parameter
func ipSliceOrNil(ip net.IP) []string { func ipSliceOrNil(ip netip.Addr) []string {
if ip == nil { if !ip.IsValid() {
return nil return nil
} }
@@ -113,7 +113,7 @@ var workDirArg = arg{
var hostArg = arg{ var hostArg = arg{
"Host address to bind HTTP server on.", "Host address to bind HTTP server on.",
"host", "h", "host", "h",
func(o options, v string) (options, error) { o.bindHost = net.ParseIP(v); return o, nil }, nil, nil, func(o options, v string) (options, error) { o.bindHost, _ = netip.ParseAddr(v); return o, nil }, nil, nil,
func(o options) []string { return ipSliceOrNil(o.bindHost) }, func(o options) []string { return ipSliceOrNil(o.bindHost) },
} }

View File

@@ -2,7 +2,7 @@ package home
import ( import (
"fmt" "fmt"
"net" "net/netip"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -56,11 +56,13 @@ func TestParseWorkDir(t *testing.T) {
} }
func TestParseBindHost(t *testing.T) { func TestParseBindHost(t *testing.T) {
assert.Nil(t, testParseOK(t).bindHost, "empty is not host") wantAddr := netip.AddrFrom4([4]byte{1, 2, 3, 4})
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
assert.Zero(t, testParseOK(t).bindHost, "empty is not host")
assert.Equal(t, wantAddr, testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
testParseParamMissing(t, "-h") testParseParamMissing(t, "-h")
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host") assert.Equal(t, wantAddr, testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host")
testParseParamMissing(t, "--host") testParseParamMissing(t, "--host")
} }
@@ -149,7 +151,7 @@ func TestSerialize(t *testing.T) {
ss: []string{"-w", "path"}, ss: []string{"-w", "path"},
}, { }, {
name: "bind_host", name: "bind_host",
opts: options{bindHost: net.IP{1, 2, 3, 4}}, opts: options{bindHost: netip.AddrFrom4([4]byte{1, 2, 3, 4})},
ss: []string{"-h", "1.2.3.4"}, ss: []string{"-h", "1.2.3.4"},
}, { }, {
name: "bind_port", name: "bind_port",

View File

@@ -176,8 +176,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
chooseSystem() chooseSystem()
action := opts.serviceControlAction action := opts.serviceControlAction
log.Info(version.Full()) log.Printf("service: control action: %s", action)
log.Info("service: control action: %s", action)
if action == "reload" { if action == "reload" {
sendSigReload() sendSigReload()

View File

@@ -266,7 +266,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
} }
} }
if !webCheckPortAvailable(setts.PortHTTPS) { if !WebCheckPortAvailable(setts.PortHTTPS) {
aghhttp.Error( aghhttp.Error(
r, r,
w, w,
@@ -356,7 +356,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
} }
// TODO(e.burkov): Investigate and perhaps check other ports. // TODO(e.burkov): Investigate and perhaps check other ports.
if !webCheckPortAvailable(data.PortHTTPS) { if !WebCheckPortAvailable(data.PortHTTPS) {
aghhttp.Error( aghhttp.Error(
r, r,
w, w,
@@ -680,6 +680,8 @@ func unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {
} }
func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) { func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
w.Header().Set("Content-Type", "application/json")
if data.CertificateChain != "" { if data.CertificateChain != "" {
encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain)) encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
data.CertificateChain = encoded data.CertificateChain = encoded
@@ -690,7 +692,16 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
data.PrivateKey = "" data.PrivateKey = ""
} }
_ = aghhttp.WriteJSONResponse(w, r, data) err := json.NewEncoder(w).Encode(data)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Failed to marshal json with TLS status: %s",
err,
)
}
} }
// registerWebHandlers registers HTTP handlers for TLS configuration // registerWebHandlers registers HTTP handlers for TLS configuration

View File

@@ -278,11 +278,11 @@ func upgradeSchema4to5(diskConf yobj) error {
log.Fatalf("Can't use password \"%s\": bcrypt.GenerateFromPassword: %s", passStr, err) log.Fatalf("Can't use password \"%s\": bcrypt.GenerateFromPassword: %s", passStr, err)
return nil return nil
} }
u := webUser{ u := User{
Name: nameStr, Name: nameStr,
PasswordHash: string(hash), PasswordHash: string(hash),
} }
users := []webUser{u} users := []User{u}
diskConf["users"] = users diskConf["users"] = users
return nil return nil
} }

View File

@@ -4,8 +4,8 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"io/fs" "io/fs"
"net"
"net/http" "net/http"
"net/netip"
"sync" "sync"
"time" "time"
@@ -16,8 +16,6 @@ import (
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/http3"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
) )
@@ -37,7 +35,7 @@ type webConfig struct {
clientFS fs.FS clientFS fs.FS
clientBetaFS fs.FS clientBetaFS fs.FS
BindHost net.IP BindHost netip.Addr
BindPort int BindPort int
BetaBindPort int BetaBindPort int
PortHTTPS int PortHTTPS int
@@ -55,56 +53,40 @@ type webConfig struct {
WriteTimeout time.Duration WriteTimeout time.Duration
firstRun bool firstRun bool
serveHTTP3 bool
} }
// httpsServer contains the data for the HTTPS server. // HTTPSServer - HTTPS Server
type httpsServer struct { type HTTPSServer struct {
// server is the pre-HTTP/3 HTTPS server.
server *http.Server server *http.Server
// server3 is the HTTP/3 HTTPS server. If it is not nil,
// [httpsServer.server] must also be non-nil.
server3 *http3.Server
// TODO(a.garipov): Why is there a *sync.Cond here? Remove.
cond *sync.Cond cond *sync.Cond
condLock sync.Mutex condLock sync.Mutex
cert tls.Certificate shutdown bool // if TRUE, don't restart the server
inShutdown bool
enabled bool enabled bool
cert tls.Certificate
} }
// Web is the web UI and API server. // Web - module object
type Web struct { type Web struct {
conf *webConfig conf *webConfig
forceHTTPS bool
// TODO(a.garipov): Refactor all these servers. httpServer *http.Server // HTTP module
httpServer *http.Server httpsServer HTTPSServer // HTTPS module
// httpServerBeta is a server for new client.
httpServerBeta *http.Server
// handlerBeta is the handler for new client. // handlerBeta is the handler for new client.
handlerBeta http.Handler handlerBeta http.Handler
// installerBeta is the pre-install handler for new client. // installerBeta is the pre-install handler for new client.
installerBeta http.Handler installerBeta http.Handler
// httpsServer is the server that handles HTTPS traffic. If it is not nil, // httpServerBeta is a server for new client.
// [Web.http3Server] must also not be nil. httpServerBeta *http.Server
httpsServer httpsServer
forceHTTPS bool
} }
// newWeb creates a new instance of the web UI and API server. // CreateWeb - create module
func newWeb(conf *webConfig) (w *Web) { func CreateWeb(conf *webConfig) *Web {
log.Info("web: initializing") log.Info("Initialize web module")
w = &Web{ w := Web{}
conf: conf, w.conf = conf
}
clientFS := http.FileServer(http.FS(conf.clientFS)) clientFS := http.FileServer(http.FS(conf.clientFS))
betaClientFS := http.FileServer(http.FS(conf.clientBetaFS)) betaClientFS := http.FileServer(http.FS(conf.clientBetaFS))
@@ -126,23 +108,23 @@ func newWeb(conf *webConfig) (w *Web) {
} }
w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock) w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock)
return &w
return w
} }
// webCheckPortAvailable checks if port, which is considered an HTTPS port, is // WebCheckPortAvailable - check if port is available
// available, unless the HTTPS server isn't active. // BUT: if we are already using this port, no need
// func WebCheckPortAvailable(port int) bool {
// TODO(a.garipov): Adapt for HTTP/3. if Context.web.httpsServer.server != nil {
func webCheckPortAvailable(port int) (ok bool) { return true
return Context.web.httpsServer.server != nil || }
aghnet.CheckPort("tcp", config.BindHost, port) == nil
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
} }
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server // TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
// if necessary. // if necessary.
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) { func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
log.Debug("web: applying new tls configuration") log.Debug("Web: applying new TLS configuration")
web.conf.PortHTTPS = tlsConf.PortHTTPS web.conf.PortHTTPS = tlsConf.PortHTTPS
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0) web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
@@ -164,8 +146,6 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
var cancel context.CancelFunc var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout) ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
shutdownSrv(ctx, web.httpsServer.server) shutdownSrv(ctx, web.httpsServer.server)
shutdownSrv3(web.httpsServer.server3)
cancel() cancel()
} }
@@ -183,7 +163,7 @@ func (web *Web) Start() {
go web.tlsServerLoop() go web.tlsServerLoop()
// this loop is used as an ability to change listening host and/or port // this loop is used as an ability to change listening host and/or port
for !web.httpsServer.inShutdown { for !web.httpsServer.shutdown {
printHTTPAddresses(aghhttp.SchemeHTTP) printHTTPAddresses(aghhttp.SchemeHTTP)
errs := make(chan error, 2) errs := make(chan error, 2)
@@ -254,7 +234,7 @@ func (web *Web) Close(ctx context.Context) {
log.Info("stopping http server...") log.Info("stopping http server...")
web.httpsServer.cond.L.Lock() web.httpsServer.cond.L.Lock()
web.httpsServer.inShutdown = true web.httpsServer.shutdown = true
web.httpsServer.cond.L.Unlock() web.httpsServer.cond.L.Unlock()
var cancel context.CancelFunc var cancel context.CancelFunc
@@ -262,7 +242,6 @@ func (web *Web) Close(ctx context.Context) {
defer cancel() defer cancel()
shutdownSrv(ctx, web.httpsServer.server) shutdownSrv(ctx, web.httpsServer.server)
shutdownSrv3(web.httpsServer.server3)
shutdownSrv(ctx, web.httpServer) shutdownSrv(ctx, web.httpServer)
shutdownSrv(ctx, web.httpServerBeta) shutdownSrv(ctx, web.httpServerBeta)
@@ -272,7 +251,7 @@ func (web *Web) Close(ctx context.Context) {
func (web *Web) tlsServerLoop() { func (web *Web) tlsServerLoop() {
for { for {
web.httpsServer.cond.L.Lock() web.httpsServer.cond.L.Lock()
if web.httpsServer.inShutdown { if web.httpsServer.shutdown {
web.httpsServer.cond.L.Unlock() web.httpsServer.cond.L.Unlock()
break break
} }
@@ -280,7 +259,7 @@ func (web *Web) tlsServerLoop() {
// this mechanism doesn't let us through until all conditions are met // this mechanism doesn't let us through until all conditions are met
for !web.httpsServer.enabled { // sleep until necessary data is supplied for !web.httpsServer.enabled { // sleep until necessary data is supplied
web.httpsServer.cond.Wait() web.httpsServer.cond.Wait()
if web.httpsServer.inShutdown { if web.httpsServer.shutdown {
web.httpsServer.cond.L.Unlock() web.httpsServer.cond.L.Unlock()
return return
} }
@@ -288,10 +267,11 @@ func (web *Web) tlsServerLoop() {
web.httpsServer.cond.L.Unlock() web.httpsServer.cond.L.Unlock()
addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS) // prepare HTTPS server
address := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
web.httpsServer.server = &http.Server{ web.httpsServer.server = &http.Server{
ErrorLog: log.StdLog("web: https", log.DEBUG), ErrorLog: log.StdLog("web: https", log.DEBUG),
Addr: addr, Addr: address,
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert}, Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: Context.tlsRoots, RootCAs: Context.tlsRoots,
@@ -305,40 +285,10 @@ func (web *Web) tlsServerLoop() {
} }
printHTTPAddresses(aghhttp.SchemeHTTPS) printHTTPAddresses(aghhttp.SchemeHTTPS)
if web.conf.serveHTTP3 {
go web.mustStartHTTP3(addr)
}
log.Debug("web: starting https server")
err := web.httpsServer.server.ListenAndServeTLS("", "") err := web.httpsServer.server.ListenAndServeTLS("", "")
if !errors.Is(err, http.ErrServerClosed) { if err != http.ErrServerClosed {
cleanupAlways() cleanupAlways()
log.Fatalf("web: https: %s", err) log.Fatal(err)
} }
} }
} }
func (web *Web) mustStartHTTP3(address string) {
defer log.OnPanic("web: http3")
web.httpsServer.server3 = &http3.Server{
// TODO(a.garipov): See if there is a way to use the error log as
// well as timeouts here.
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: Context.tlsRoots,
CipherSuites: aghtls.SaferCipherSuites(),
MinVersion: tls.VersionTLS12,
},
Handler: withMiddlewares(Context.mux, limitRequestBody),
}
log.Debug("web: starting http/3 server")
err := web.httpsServer.server3.ListenAndServe()
if !errors.Is(err, quic.ErrServerClosed) {
cleanupAlways()
log.Fatalf("web: http3: %s", err)
}
}

View File

@@ -4,89 +4,6 @@
## v0.108.0: API changes ## v0.108.0: API changes
## v0.107.15: `POST` Requests Without Bodies
As an additional CSRF protection measure, AdGuard Home now ensures that requests
that change its state but have no body do not have a `Content-Type` header set
on them.
This concerns the following APIs:
* `POST /control/dhcp/reset_leases`;
* `POST /control/dhcp/reset`;
* `POST /control/parental/disable`;
* `POST /control/parental/enable`;
* `POST /control/querylog_clear`;
* `POST /control/safebrowsing/disable`;
* `POST /control/safebrowsing/enable`;
* `POST /control/safesearch/disable`;
* `POST /control/safesearch/enable`;
* `POST /control/stats_reset`;
* `POST /control/update`.
## v0.107.14: BREAKING API CHANGES
A Cross-Site Request Forgery (CSRF) vulnerability has been discovered. We have
implemented several measures to prevent such vulnerabilities in the future, but
some of these measures break backwards compatibility for the sake of better
protection.
All JSON APIs that expect a body now check if the request actually has
`Content-Type` set to `application/json`.
All new formats for the request and response bodies are documented in
`openapi.yaml`.
### `POST /control/filtering/set_rules` And Other Plain-Text APIs
The following APIs, which previously accepted or returned `text/plain` data,
now accept or return data as JSON.
#### `POST /control/filtering/set_rules`
Previously, the API accepted a raw list of filters as a plain-text file. Now,
the filters must be presented in a JSON object with the following format:
```json
{
"rules":
[
"||example.com^",
"# comment",
"@@||www.example.com^"
]
}
```
#### `GET /control/i18n/current_language` And `POST /control/i18n/change_language`
Previously, these APIs accepted and returned the language code in plain text.
Now, they accept and return them in a JSON object with the following format:
```json
{
"language": "en"
}
```
#### `POST /control/dhcp/find_active_dhcp`
Previously, the API accepted the name of the network interface as a plain-text
string. Now, it must be contained within a JSON object with the following
format:
```json
{
"interface": "eth0"
}
```
## v0.107.12: API changes ## v0.107.12: API changes
### `GET /control/blocked_services/services` ### `GET /control/blocked_services/services`
@@ -94,8 +11,6 @@ format:
* The new `GET /control/blocked_services/services` HTTP API allows inspecting * The new `GET /control/blocked_services/services` HTTP API allows inspecting
all available services. all available services.
## v0.107.7: API changes ## v0.107.7: API changes
### The new optional field `"ecs"` in `QueryLogItem` ### The new optional field `"ecs"` in `QueryLogItem`
@@ -109,8 +24,6 @@ format:
`POST /install/configure` which means that the specified password does not `POST /install/configure` which means that the specified password does not
meet the strength requirements. meet the strength requirements.
## v0.107.3: API changes ## v0.107.3: API changes
### The new field `"version"` in `AddressesInfo` ### The new field `"version"` in `AddressesInfo`
@@ -118,8 +31,6 @@ format:
* The new field `"version"` in `GET /install/get_addresses` is the version of * The new field `"version"` in `GET /install/get_addresses` is the version of
the AdGuard Home instance. the AdGuard Home instance.
## v0.107.0: API changes ## v0.107.0: API changes
### The new field `"cached"` in `QueryLogItem` ### The new field `"cached"` in `QueryLogItem`

View File

@@ -413,11 +413,6 @@
- 'dhcp' - 'dhcp'
'operationId': 'checkActiveDhcp' 'operationId': 'checkActiveDhcp'
'summary': 'Searches for an active DHCP server on the network' 'summary': 'Searches for an active DHCP server on the network'
'requestBody':
'content':
'application/json':
'schema':
'$ref': '#/components/schemas/DhcpFindActiveReq'
'responses': 'responses':
'200': '200':
'description': 'OK.' 'description': 'OK.'
@@ -601,10 +596,11 @@
'summary': 'Set user-defined filter rules' 'summary': 'Set user-defined filter rules'
'requestBody': 'requestBody':
'content': 'content':
'application/json': 'text/plain':
'schema': 'schema':
'$ref': '#/components/schemas/SetRulesRequest' 'type': 'string'
'description': 'Custom filtering rules.' 'example': '@@||yandex.ru^|'
'description': 'All filtering rules, one line per rule'
'responses': 'responses':
'200': '200':
'description': 'OK.' 'description': 'OK.'
@@ -671,6 +667,24 @@
- 'parental' - 'parental'
'operationId': 'parentalEnable' 'operationId': 'parentalEnable'
'summary': 'Enable parental filtering' 'summary': 'Enable parental filtering'
'requestBody':
'content':
'text/plain':
'schema':
'type': 'string'
'enum':
- 'EARLY_CHILDHOOD'
- 'YOUNG'
- 'TEEN'
- 'MATURE'
'example': 'sensitivity=TEEN'
'description': |
Age sensitivity for parental filtering,
EARLY_CHILDHOOD is 3
YOUNG is 10
TEEN is 13
MATURE is 17
'required': true
'responses': 'responses':
'200': '200':
'description': 'OK.' 'description': 'OK.'
@@ -944,9 +958,10 @@
Change current language. Argument must be an ISO 639-1 two-letter code. Change current language. Argument must be an ISO 639-1 two-letter code.
'requestBody': 'requestBody':
'content': 'content':
'application/json': 'text/plain':
'schema': 'schema':
'$ref': '#/components/schemas/LanguageSettings' 'type': 'string'
'example': 'en'
'description': > 'description': >
New language. It must be known to the server and must be an ISO 639-1 New language. It must be known to the server and must be an ISO 639-1
two-letter code. two-letter code.
@@ -965,9 +980,10 @@
'200': '200':
'description': 'OK.' 'description': 'OK.'
'content': 'content':
'application/json': 'text/plain':
'schema': 'examples':
'$ref': '#/components/schemas/LanguageSettings' 'response':
'value': 'en'
'/install/get_addresses_beta': '/install/get_addresses_beta':
'get': 'get':
'tags': 'tags':
@@ -1537,19 +1553,6 @@
'properties': 'properties':
'updated': 'updated':
'type': 'integer' 'type': 'integer'
'SetRulesRequest':
'description': 'Custom filtering rules setting request.'
'example':
'rules':
- '||example.com^'
- '# comment'
- '@@||www.example.com^'
'properties':
'rules':
'items':
'type': 'string'
'type': 'array'
'type': 'object'
'GetVersionRequest': 'GetVersionRequest':
'type': 'object' 'type': 'object'
'description': '/version.json request data' 'description': '/version.json request data'
@@ -1774,16 +1777,6 @@
'additionalProperties': 'additionalProperties':
'$ref': '#/components/schemas/NetInterface' '$ref': '#/components/schemas/NetInterface'
'DhcpFindActiveReq':
'description': >
Request for checking for other DHCP servers in the network.
'properties':
'interface':
'description': 'The name of the network interface'
'example': 'eth0'
'type': 'string'
'type': 'object'
'DhcpSearchResult': 'DhcpSearchResult':
'type': 'object' 'type': 'object'
'description': > 'description': >
@@ -2699,15 +2692,6 @@
'description': 'The error message, an opaque string.' 'description': 'The error message, an opaque string.'
'type': 'string' 'type': 'string'
'type': 'object' 'type': 'object'
'LanguageSettings':
'description': 'Language settings object.'
'properties':
'language':
'description': 'The current language or the language to set.'
'type': 'string'
'required':
- 'language'
'type': 'object'
'securitySchemes': 'securitySchemes':
'basicAuth': 'basicAuth':
'type': 'http' 'type': 'http'