Compare commits

...

5 Commits

Author SHA1 Message Date
Eugene Burkov
937c16e50d all: log changes 2025-04-22 15:03:21 +03:00
Eugene Burkov
4658a4d836 all: fix date 2025-04-18 16:55:44 +03:00
Eugene Miroshkin
ce2ee143d2 all: sync with master 2025-04-18 16:41:33 +03:00
Ainar Garipov
39e22ada96 all: upd base docker 2025-04-10 20:35:55 +03:00
Eugene Burkov
5aee57e297 all: sync with master 2025-04-08 19:57:40 +03:00
53 changed files with 2136 additions and 2678 deletions

View File

@@ -1,8 +1,8 @@
'name': 'build'
'env':
'GO_VERSION': '1.24.1'
'NODE_VERSION': '18'
'GO_VERSION': '1.24.2'
'NODE_VERSION': '20'
'on':
'push':

View File

@@ -1,7 +1,7 @@
'name': 'lint'
'env':
'GO_VERSION': '1.24.1'
'GO_VERSION': '1.24.2'
'on':
'push':

View File

@@ -9,25 +9,72 @@ The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/
<!--
## [v0.108.0] TBA
## [v0.107.60] - 2025-04-01 (APPROX.)
## [v0.107.62] - 2025-04-30 (APPROX.)
See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
See also the [v0.107.62 GitHub milestone][ms-v0.107.62].
[ms-v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/milestone/95?closed=1
[ms-v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/milestone/97?closed=1
NOTE: Add new changes BELOW THIS COMMENT.
-->
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.61] - 2025-04-22
See also the [v0.107.61 GitHub milestone][ms-v0.107.61].
### Security
- Any simultaneous requests that are considered duplicates will now only result in a single request to upstreams, reducing the chance of a cache poisoning attack succeeding. This is controlled by the new configuration object `pending_requests`, which has a single `enabled` property, set to `true` by default.
**NOTE:** We thank [Xiang Li][mr-xiang-li] for reporting this security issue. It's strongly recommended to leave it enabled, otherwise AdGuard Home will be vulnerable to untrusted clients.
[mr-xiang-li]: https://lixiang521.com/
[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1
## [v0.107.60] - 2025-04-14
See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
### Security
- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.2][go-1.24.2].
### Changed
- Alpine Linux version in `Dockerfile` has been updated to 3.21 ([#7588]).
### Deprecated
- Node 20 support, Node 22 will be required in future releases.
**NOTE:** `npm` may be replaced with a different tool, such as `pnpm` or `yarn`, in a future release.
### Fixed
- Filtering for DHCP clients ([#7734]).
- Incorrect label on login page ([#7729]).
- Validation process for the HTTPS port on the *Encryption Settings* page.
### Removed
- Node 18 support.
[#7588]: https://github.com/AdguardTeam/AdGuardHome/issues/7588
[#7729]: https://github.com/AdguardTeam/AdGuardHome/issues/7729
[#7734]: https://github.com/AdguardTeam/AdGuardHome/issues/7734
[go-1.24.2]: https://groups.google.com/g/golang-announce/c/Y2uBTVKjBQk
[ms-v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/milestone/95?closed=1
## [v0.107.59] - 2025-03-21
See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
[ms-v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/milestone/94?closed=1
### Fixed
- Rules with the `client` modifier not working ([#7708]).
@@ -37,6 +84,8 @@ See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
[#7704]: https://github.com/AdguardTeam/AdGuardHome/issues/7704
[#7708]: https://github.com/AdguardTeam/AdGuardHome/issues/7708
[ms-v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/milestone/94?closed=1
## [v0.107.58] - 2025-03-19
See also the [v0.107.58 GitHub milestone][ms-v0.107.58].
@@ -3068,11 +3117,13 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
[ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1
<!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...HEAD
[v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.62...HEAD
[v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...v0.107.62
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...HEAD
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...HEAD
[v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61
[v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60
[v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59
[v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58
[v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...v0.107.57

View File

@@ -27,7 +27,7 @@ DIST_DIR = dist
GOAMD64 = v1
GOPROXY = https://proxy.golang.org|direct
GOTELEMETRY = off
GOTOOLCHAIN = go1.24.1
GOTOOLCHAIN = go1.24.2
GPG_KEY = devteam@adguard.com
GPG_KEY_PASSPHRASE = not-a-real-password
NPM = npm
@@ -38,7 +38,6 @@ REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
SIGN = 1
SIGNER_API_KEY = not-a-real-key
VERSION = v0.0.0
YARN = yarn
NEXTAPI = 0
@@ -139,5 +138,4 @@ txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh
openapi-lint: ; cd ./openapi/ && $(YARN) test
openapi-show: ; cd ./openapi/ && $(YARN) start
# TODO(a.garipov): Re-add openapi-lint.

View File

@@ -205,9 +205,9 @@ Run `make init` to prepare the development environment.
You will need this to build AdGuard Home:
- [Go](https://golang.org/dl/) v1.23 or later;
- [Node.js](https://nodejs.org/en/download/) v18.18 or later;
- [npm](https://www.npmjs.com/) v8 or later;
- [Go](https://golang.org/dl/) v1.24 or later;
- [Node.js](https://nodejs.org/en/download/) v20.19 or later;
- [npm](https://www.npmjs.com/) v10.8 or later;
### <a href="#building" id="building" name="building">Building</a>

View File

@@ -7,8 +7,8 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
'dockerGo': 'adguard/go-builder:1.24.1--1'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
'stages':
- 'Build frontend':
@@ -50,7 +50,7 @@
'docker':
'image': '${bamboo.dockerFrontend}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
'key': 'BF'
'other':
'clean-working-dir': true
@@ -157,6 +157,7 @@
# Print Docker info.
docker info
docker buildx version
# Prepare and push the build.
env \
@@ -277,8 +278,8 @@
# need to build a few of these.
'variables':
'channel': 'beta'
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
'dockerGo': 'adguard/go-builder:1.24.1--1'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
# release-vX.Y.Z branches are the branches from which the actual final
# release is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
@@ -293,5 +294,5 @@
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
'dockerGo': 'adguard/go-builder:1.24.1--1'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'

View File

@@ -5,8 +5,8 @@
'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests'
'variables':
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
'dockerGo': 'adguard/go-builder:1.24.1--1'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
'channel': 'development'
'stages':
@@ -39,7 +39,7 @@
'docker':
'image': '${bamboo.dockerFrontend}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
'key': 'JSTEST'
'other':
'clean-working-dir': true
@@ -103,7 +103,7 @@
'docker':
'image': '${bamboo.dockerFrontend}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
'key': 'BF'
'other':
'clean-working-dir': true
@@ -178,7 +178,7 @@
'docker':
'image': '${bamboo.dockerFrontend}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
'key': 'E2ETEST'
'other':
'clean-working-dir': true
@@ -233,6 +233,6 @@
# Set the default release channel on the release branch to beta, as we
# may need to build a few of these.
'variables':
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
'dockerGo': 'adguard/go-builder:1.24.1--1'
'dockerFrontend': 'adguard/home-js-builder:3.1'
'dockerGo': 'adguard/go-builder:1.24.2--1'
'channel': 'candidate'

1207
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

4
client/package.json vendored
View File

@@ -66,7 +66,7 @@
"@babel/preset-react": "^7.24.1",
"@playwright/test": "1.50.1",
"@types/lodash": "^4.17.4",
"@types/node": "^22.10.2",
"@types/node": "^22.13.10",
"@types/react": "^17.0.80",
"@types/react-dom": "^18.3.0",
"@types/react-redux": "^7.1.33",
@@ -99,7 +99,7 @@
"stylelint": "^16.5.0",
"ts-loader": "^9.5.1",
"url-loader": "^4.1.1",
"vitest": "^3.0.4",
"vitest": "^3.1.1",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",

View File

@@ -45,6 +45,7 @@
"filter": "Филтър",
"query_log": "История на заявките",
"compact": "Compact",
"nothing_found": "Нищо не е намерено",
"faq": "ЧЗВ",
"version": "версия",
"address": "Адрес",
@@ -65,14 +66,12 @@
"stats_malware_phishing": "вируси/атаки",
"stats_adult": "сайтове за възрастни",
"stats_query_domain": "Най-отваряни страници",
"for_last_24_hours": "за последните 24 часа",
"no_domains_found": "Няма намерени резултати",
"requests_count": "Сума на заявките",
"top_blocked_domains": "Най-блокирани страници",
"top_clients": "Най-активни IP адреси",
"no_clients_found": "Нямa намерени адреси",
"general_statistics": "Обща статисика",
"number_of_dns_query_24_hours": "Сума на DNS заявки за последните 24 часа",
"number_of_dns_query_blocked_24_hours": "Сума на блокирани DNS заявки от филтрите за реклама и местни",
"number_of_dns_query_blocked_24_hours_by_sec": "Сума на блокирани DNS заявки от AdGuard свързани със сигурността",
"number_of_dns_query_blocked_24_hours_adult": "Сума на блокирани сайтове за възрастни",
@@ -156,6 +155,7 @@
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
"default": "По подразбиране",
"custom_ip": "Персонализиран IP",
"dnscrypt": "DNSCrypt",
"dns_over_https": "DNS-пред-HTTPS",
"dns_over_quic": "DNS-over-QUIC",
"plain_dns": "Обикновен DNS",

View File

@@ -97,9 +97,9 @@
"settings": "Einstellungen",
"filters": "Filter",
"filter": "Filter",
"query_log": "Anfragenprotokoll",
"query_log": "Abfrageprotokoll",
"compact": "Kompakt",
"nothing_found": "Nichts gefunden\n",
"nothing_found": "Nichts gefunden",
"faq": "FAQ",
"version": "Version",
"address": "Adresse",
@@ -199,7 +199,7 @@
"cancel_btn": "Abbrechen",
"enter_name_hint": "Name eingeben",
"enter_url_or_path_hint": "URL oder absoluten Pfad der Liste eingeben",
"check_updates_btn": "Nach Aktualisierungen suchen",
"check_updates_btn": "Nach Updates suchen",
"new_blocklist": "Neue Sperrliste",
"new_allowlist": "Neue Positivliste",
"edit_blocklist": "Sperrliste bearbeiten",
@@ -566,7 +566,7 @@
"ignore_domains": "Ignorierte Domains (durch Zeilenumbruch getrennt)",
"ignore_domains_title": "Ignorierte Domains",
"ignore_domains_desc_stats": "Abfragen, die diesen Regeln entsprechen, werden nicht in die Statistik aufgenommen",
"ignore_domains_desc_query": "Abfragen, die diesen Regeln entsprechen, werden nicht in das Anfragenprotokoll aufgenommen",
"ignore_domains_desc_query": "Abfragen, die diesen Regeln entsprechen, werden nicht in das Abfrageprotokoll aufgenommen",
"interval_hours": "{{count}} Stunde",
"interval_hours_plural": "{{count}} Stunden",
"filters_configuration": "Filterkonfiguration",

View File

@@ -1,16 +1,16 @@
{
"client_settings": "Configuración de clientes",
"example_upstream_reserved": "un DNS de subida <0>para un dominio específico</0>.",
"example_multiple_upstreams_reserved": "múltiples upstreams <0>para dominios específicos</0>;",
"example_upstream_reserved": "un proveedor DNS <0>para un dominio específico</0>.",
"example_multiple_upstreams_reserved": "múltiples proveedores DNS <0>para dominios específicos</0>.",
"example_upstream_comment": "un comentario.",
"upstream_parallel": "Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los servidores DNS de subida.",
"upstream_parallel": "Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los proveedores DNS.",
"parallel_requests": "Consultas paralelas",
"load_balancing": "Balanceo de carga",
"load_balancing_desc": "Consulta un servidor Dns upstream a la vez.<br/>AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo medio de búsqueda.",
"load_balancing_desc": "Consulta un proveedor DNS a la vez.<br/>AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo promedio de búsqueda.",
"bootstrap_dns": "Servidores DNS de arranque",
"bootstrap_dns_desc": "Direcciones IP de servidores DNS utilizadas para resolver direcciones IP de los solucionadores DoH/DoT que especifiques como ascendentes. No se permiten comentarios.",
"bootstrap_dns_desc": "Direcciones IP de los servidores DNS utilizados para resolver las direcciones IP de los resolutores DoH/DoT que especifiques como proveedores DNS. No se permiten comentarios.",
"fallback_dns_title": "Servidores DNS alternativos",
"fallback_dns_desc": "Lista de servidores DNS alternativos utilizados cuando los servidores DNS de subida no responden. La sintaxis es la misma que en el campo de los principales DNS de subida anterior.",
"fallback_dns_desc": "Lista de servidores DNS alternativos utilizados cuando los proveedores DNS no responden. La sintaxis es la misma que en el campo de los principales proveedores DNS anterior.",
"fallback_dns_placeholder": "Ingresa un servidor DNS alternativo por línea",
"local_ptr_title": "Servidores DNS inversos y privados",
"local_ptr_desc": "Los servidores DNS que AdGuard Home utiliza para consultas PTR, SOA y NS privadas. La petición se considera privada si solicita un dominio ARPA que contiene una subred dentro de rangos IP privados, por ejemplo \"192.168.12.34\", y procede de un cliente con dirección privada. Si no se configura, AdGuard Home utiliza las direcciones de los resolvedores DNS predeterminados de tu sistema operativo, excepto las direcciones del propio AdGuard Home.",
@@ -18,9 +18,9 @@
"local_ptr_no_default_resolver": "AdGuard Home no pudo determinar los resolutores DNS inversos y privados adecuados para este sistema.",
"local_ptr_placeholder": "Ingresa una dirección IP por línea",
"resolve_clients_title": "Habilitar la resolución inversa de las direcciones IP de clientes",
"resolve_clients_desc": "Resolve de manera inversa las direcciones IP de los clientes a sus nombres de hosts enviando consultas PTR a los resolutores correspondientes (servidores DNS privados para clientes locales, servidores DNS de subida para clientes con direcciones IP públicas).",
"resolve_clients_desc": "Resuelve de manera inversa las direcciones IP de los clientes a sus nombres de hosts enviando consultas PTR a los resolutores correspondientes (servidores DNS privados para clientes locales, proveedores DNS para clientes con direcciones IP públicas).",
"use_private_ptr_resolvers_title": "Usar resolutores DNS inversos y privados",
"use_private_ptr_resolvers_desc": "Resolver las peticiones PTR, SOA y NS para dominios ARPA que contienen direcciones privadas utilizando servidores upstream privados, DHCP, /etc/hosts, etc. Si se desactiva, AdGuard Home responde a todas estas consultas con NXDOMAIN.",
"use_private_ptr_resolvers_desc": "Resuelve peticiones PTR, SOA y NS para dominios ARPA que contienen direcciones IP privadas a través de proveedores DNS privados, DHCP, /etc/hosts, etc. Si se deshabilita, AdGuard Home responde a todas estas peticiones con NXDOMAIN.",
"check_dhcp_servers": "Comprobar si hay servidores DHCP",
"save_config": "Guardar configuración",
"enabled_dhcp": "Servidor DHCP habilitado",
@@ -132,8 +132,8 @@
"top_clients": "Clientes más frecuentes",
"no_clients_found": "No se han encontrado clientes",
"general_statistics": "Estadísticas generales",
"top_upstreams": "DNS de subida más frecuentes",
"no_upstreams_data_found": "No se han encontrado datos de DNS de subida",
"top_upstreams": "Proveedores DNS más frecuentes",
"no_upstreams_data_found": "No se han encontrado datos de proveedores DNS",
"number_of_dns_query_days": "Número de consultas DNS procesadas durante el último {{count}} día",
"number_of_dns_query_days_plural": "Número de consultas DNS procesadas durante los últimos {{count}} días",
"number_of_dns_query_hours": "Número de consultas DNS procesadas durante la última {{count}} hora",
@@ -144,7 +144,7 @@
"enforced_save_search": "Búsquedas seguras forzadas",
"number_of_dns_query_to_safe_search": "Número de peticiones DNS a los motores de búsqueda para los que se aplicó la búsqueda segura forzada",
"average_processing_time": "Tiempo promedio de procesamiento",
"average_upstream_response_time": "Tiempo promedio de respuesta upstream",
"average_upstream_response_time": "Tiempo promedio de respuesta del proveedor DNS",
"response_time": "Tiempo de respuesta",
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una petición DNS",
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
@@ -157,7 +157,7 @@
"enforce_save_search_hint": "AdGuard Home reforzará la búsqueda segura en los siguientes motores de búsqueda: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex y Pixabay.",
"no_servers_specified": "No hay servidores especificados",
"general_settings": "Configuración general",
"dns_settings": "Configuración del DNS",
"dns_settings": "Configuración DNS",
"dns_blocklists": "Listas de bloqueo DNS",
"dns_allowlists": "Listas de permitido DNS",
"dns_blocklists_desc": "AdGuard Home bloqueará los dominios que coincidan con las listas de bloqueo.",
@@ -165,12 +165,12 @@
"custom_filtering_rules": "Reglas de filtrado personalizado",
"encryption_settings": "Configuración de cifrado",
"dhcp_settings": "Configuración DHCP",
"upstream_dns": "Servidores DNS de subida",
"upstream_dns_help": "Ingresa una dirección de servidor por línea. <a>Más información</a> sobre la configuración de los servidores DNS de subida.",
"upstream_dns": "Proveedores DNS",
"upstream_dns_help": "Ingresa una dirección de servidor por línea. <a>Más información</a> sobre la configuración de los proveedores DNS.",
"upstream_dns_configured_in_file": "Configurado en {{path}}",
"test_upstream_btn": "Probar DNS de subida",
"upstreams": "DNS de subida",
"upstream": "DNS de subida",
"test_upstream_btn": "Probar proveedores DNS",
"upstreams": "Proveedores DNS",
"upstream": "Proveedor DNS",
"apply_btn": "Aplicar",
"disabled_filtering_toast": "Filtrado deshabilitado",
"enabled_filtering_toast": "Filtrado habilitado",
@@ -233,11 +233,11 @@
"example_upstream_tcp_port": "DNS regular (mediante TCP, con puerto).",
"example_upstream_tcp_hostname": "DNS regular (mediante TCP, nombre del host).",
"all_lists_up_to_date_toast": "Todas las listas ya están actualizadas",
"updated_upstream_dns_toast": "Servidores DNS de subida guardados correctamente",
"updated_upstream_dns_toast": "Proveedores DNS guardados correctamente",
"dns_test_ok_toast": "Los servidores DNS especificados funcionan correctamente",
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no se puede utilizar, por favor revisa si lo has escrito correctamente",
"dns_test_parsing_error_toast": "No se pudo utilizar la sección {{section}}: línea {{line}}:, verifica si la escribiste correctamente",
"dns_test_warning_toast": "DNS de subida \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
"dns_test_warning_toast": "Proveedor DNS \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
"unblock": "Desbloquear",
"block": "Bloquear",
"disallow_this_client": "No permitir a este cliente",
@@ -294,9 +294,9 @@
"blocked_response_ttl": "Respuesta TTL bloqueada",
"blocked_response_ttl_desc": "Especifica durante cuántos segundos los clientes deben almacenar en cache una respuesta filtrada",
"form_enter_blocked_response_ttl": "Ingresa el TTL de respuesta bloqueada (segundos)",
"upstream_timeout": "Tiempo de espera del upstream",
"upstream_timeout_desc": "Especifica el número de segundos que se debe esperar para recibir una respuesta del servidor upstream",
"form_enter_upstream_timeout": "Ingresa la duración del tiempo de espera del servidor DNS upstream en segundos",
"upstream_timeout": "Tiempo de espera del proveedor DNS",
"upstream_timeout_desc": "Especifica el número de segundos que se debe esperar para recibir una respuesta del proveedor DNS",
"form_enter_upstream_timeout": "Ingresa la duración de tiempo de espera del proveedor DNS en segundos",
"dnscrypt": "DNSCrypt",
"dns_over_https": "DNS mediante HTTPS",
"dns_over_tls": "DNS mediante TLS",
@@ -311,7 +311,7 @@
"form_enter_rate_limit": "Ingresa el límite de cantidad",
"rate_limit": "Límite de cantidad",
"edns_enable": "Habilitar subred de cliente EDNS",
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del DNS de subida y registra los valores enviados por los clientes en el registro de consultas.",
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del proveedor DNS y registra los valores enviados por los clientes en el registro de consultas.",
"edns_use_custom_ip": "Usar IP personalizada para EDNS",
"edns_use_custom_ip_desc": "Permitir el uso de IP personalizadas para EDNS",
"rate_limit_desc": "Número de peticiones por segundo permitidas por cliente. Establecerlo en 0 significa que no hay límite.",
@@ -335,7 +335,7 @@
"theme_auto": "Auto",
"theme_light": "Claro",
"theme_dark": "Oscuro",
"upstream_dns_client_desc": "Si se mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración del DNS</0>.",
"upstream_dns_client_desc": "Si se mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración DNS</0>.",
"tracker_source": "Fuente del rastreador",
"source_label": "Fuente",
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
@@ -596,12 +596,12 @@
"example_rewrite_wildcard": "reescribe las respuestas para todos los subdominios de <0>ejemplo.org</0>.",
"rewrite_ip_address": "Dirección IP: utiliza esta IP en una respuesta A o AAAA",
"rewrite_domain_name": "Nombre de dominio: añade un registro CNAME",
"rewrite_A": "<0>A</0>: valor especial, mantiene registros <0>A</0> del DNS de subida",
"rewrite_AAAA": "<0>AAAA</0>: valor especial, mantiene registros <0>AAAA</0> del DNS de subida",
"rewrite_A": "<0>A</0>: valor especial, mantiene registros <0>A</0> del proveedor DNS",
"rewrite_AAAA": "<0>AAAA</0>: valor especial, mantiene registros <0>AAAA</0> del proveedor DNS",
"disable_ipv6": "Deshabilitar resolución de direcciones IPv6",
"disable_ipv6_desc": "Descarta todas las consultas de DNS para direcciones IPv6 (tipo AAAA) y elimina las sugerencias de IPv6 de las respuestas HTTPS.",
"fastest_addr": "Dirección IP más rápida",
"fastest_addr_desc": "Espera a que respondan <b>todos</b> los servidores DNS, mide la velocidad de conexión TCP de cada servidor y devuelve la Dirección IP del servidor con la velocidad de conexión más rápida.<br/>Este modo puede ralentizar significativamente las consultas DNS, si uno o más servidores DNS de upstream no están respondiendo. Asegúrate de que tus servidores DNS upstream sean estables y tu tiempo de espera de upstream sea bajo.",
"fastest_addr_desc": "Espera respuestas de <b>todos</b> los servidores DNS, mide la velocidad de conexión TCP de cada servidor y devuelve la dirección IP del servidor con la velocidad de conexión más rápida.<br/>Este modo puede ralentizar significativamente las consultas DNS, si uno o más proveedores DNS no responden. Asegúrate de que tus proveedores DNS sean estables y de que el tiempo de espera tu proveedor DNS sea bajo.",
"autofix_warning_text": "Si haces clic en \"Corregir\", AdGuard Home configurará tu sistema para utilizar el servidor DNS de AdGuard Home.",
"autofix_warning_list": "Realizará estas tareas: <0>Deshabilitar el sistema DNSStubListener</0> <0>Establecer la dirección del servidor DNS en 127.0.0.1</0> <0>Reemplazar el destino del enlace simbólico de /etc/resolv.conf por /run/systemd/resolve/resolv.conf</0> <0>Detener DNSStubListener (recargar el servicio systemd-resolved)</0>",
"autofix_warning_result": "Como resultado, todas las peticiones DNS de tu sistema serán procesadas por AdGuard Home de manera predeterminada.",
@@ -662,7 +662,7 @@
"enter_cache_size": "Ingresa el tamaño de la caché (bytes)",
"enter_cache_ttl_min_override": "Ingresa el TTL mínimo (en segundos)",
"enter_cache_ttl_max_override": "Ingresa el TTL máximo (en segundos)",
"cache_ttl_min_override_desc": "Amplía el corto tiempo de vida (segundos) de los valores recibidos del servidor DNS de subida al almacenar en caché las respuestas DNS.",
"cache_ttl_min_override_desc": "Amplía el corto tiempo de vida (segundos) de los valores recibidos del proveedor DNS al almacenar en caché las respuestas DNS.",
"cache_ttl_max_override_desc": "Establece un valor de tiempo de vida (segundos) máximo para las entradas en la caché DNS.",
"ttl_cache_validation": "La anulación TTL mínimo de la caché debe ser menor o igual al máximo",
"cache_optimistic": "Caché optimista",
@@ -748,7 +748,7 @@
"thursday_short": "Jue.",
"friday_short": "Vie.",
"saturday_short": "Sáb.",
"upstream_dns_cache_configuration": "Configuración de la caché DNS upstream",
"enable_upstream_dns_cache": "Habilitar el almacenamiento en caché de DNS para la configuración personalizada de este cliente",
"upstream_dns_cache_configuration": "Configuración de la caché del proveedor DNS",
"enable_upstream_dns_cache": "Habilitar el almacenamiento en caché del DNS para la configuración personalizada de este cliente",
"dns_cache_size": "Tamaño de la caché DNS, en bytes"
}

View File

@@ -110,9 +110,9 @@
"homepage": "Startpagina",
"report_an_issue": "Rapporteer een probleem",
"privacy_policy": "Privacybeleid",
"enable_protection": "Schakel bescherming in",
"enable_protection": "Bescherming inschakelen",
"enabled_protection": "Bescherming ingeschakeld",
"disable_protection": "Schakel bescherming uit",
"disable_protection": "Bescherming uitschakelen",
"disabled_protection": "Bescherming uitgeschakeld",
"refresh_statics": "Ververs statistieken",
"dns_query": "DNS-queries",
@@ -702,13 +702,13 @@
"disable_for_hours": "Voor {{count}} uur",
"disable_for_hours_plural": "Voor {{count}} uren",
"disable_until_tomorrow": "Tot morgen",
"disable_notify_for_seconds": "Beveiliging uitschakelen voor {{count}} seconde",
"disable_notify_for_seconds_plural": "Beveiliging uitschakelen voor {{count}} seconden",
"disable_notify_for_minutes": "Beveiliging uitschakelen voor {{count}} minuut",
"disable_notify_for_minutes_plural": "Beveiliging uitschakelen voor {{count}} minuten",
"disable_notify_for_hours": "Beveiliging uitschakelen voor {{count}} uur",
"disable_notify_for_hours_plural": "Beveiliging uitschakelen voor {{count}} uren",
"disable_notify_until_tomorrow": "Beveiliging uitschakelen tot morgen",
"disable_notify_for_seconds": "Bescherming uitschakelen voor {{count}} seconde",
"disable_notify_for_seconds_plural": "Bescherming uitschakelen voor {{count}} seconden",
"disable_notify_for_minutes": "Bescherming uitschakelen voor {{count}} minuut",
"disable_notify_for_minutes_plural": "Bescherming uitschakelen voor {{count}} minuten",
"disable_notify_for_hours": "Bescherming uitschakelen voor {{count}} uur",
"disable_notify_for_hours_plural": "Bescherming uitschakelen voor {{count}} uren",
"disable_notify_until_tomorrow": "Bescherming uitschakelen tot morgen",
"enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}",
"custom_retention_input": "Voer retentie in uren in",
"custom_rotation_input": "Voer rotatie in uren in",

View File

@@ -106,7 +106,6 @@
"stats_malware_phishing": "Blokkert skadevare/phishing",
"stats_adult": "Blokkerte voksennettsteder",
"stats_query_domain": "Mest forespurte domener",
"for_last_24_hours": "de siste 24 timene",
"for_last_days": "for den siste {{count}} dagen",
"for_last_days_plural": "de siste {{count}} dagene",
"stats_disabled": "Statistikkene har blitt skrudd av. Du kan skru den på fra <0>innstillingssiden</0>.",
@@ -121,7 +120,6 @@
"no_upstreams_data_found": "Ingen oppstrøms servere data funnet",
"number_of_dns_query_days": "Antall DNS-spørringer behandlet for de siste {{count}} dagene",
"number_of_dns_query_days_plural": "Antall DNS-forespørsler som ble behandlet de siste {{count}} dagene",
"number_of_dns_query_24_hours": "Antall DNS-forespørsler som ble behandlet de siste 24 timene",
"number_of_dns_query_blocked_24_hours": "Antall DNS-forespørsler som ble blokkert av adblock-filtre, hosts-lister, og domene-lister",
"number_of_dns_query_blocked_24_hours_by_sec": "Antall DNS-forespørsler som ble blokkert av AdGuard sin nettlesersikkerhetsmodul",
"number_of_dns_query_blocked_24_hours_adult": "Antall voksennettsteder som ble blokkert",
@@ -266,6 +264,7 @@
"custom_ip": "Tilpasset IP",
"blocking_ipv4": "IPv4-blokkering",
"blocking_ipv6": "IPv6-blokkering",
"blocked_response_ttl": "Blokkerte svars TTL",
"dnscrypt": "DNSCrypt",
"dns_over_https": "DNS-over-HTTPS",
"dns_over_tls": "DNS-over-TLS",
@@ -627,7 +626,6 @@
"use_saved_key": "Bruk den tidligere lagrede nøkkelen",
"parental_control": "Foreldrekontroll",
"safe_browsing": "Sikker surfing",
"served_from_cache": "{{value}} <i>(formidlet fra mellomlageret)</i>",
"theme_dark_desc": "Mørkt tema",
"theme_light_desc": "Lyst tema",
"disable_notify_until_tomorrow": "Deaktiver beskyttelsen til i morgen",

View File

@@ -1,6 +1,6 @@
{
"client_settings": "用戶端設定",
"example_upstream_reserved": "<0>特定網域</0>上游;",
"example_upstream_reserved": "<0>特定網域</0>上游;",
"example_multiple_upstreams_reserved": "<0>特定網域</0>的多個上游伺服器;",
"example_upstream_comment": "註解。",
"upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析。",
@@ -20,7 +20,7 @@
"resolve_clients_title": "啟用用戶端的 IP 位址之反向的解析",
"resolve_clients_desc": "透過傳送指標PTR查詢到對應的解析器私人 DNS 伺服器供區域的用戶端,上游的伺服器供有公共 IP 位址的用戶端),反向地解析用戶端的 IP 位址變為它們的主機名稱。",
"use_private_ptr_resolvers_title": "使用私人反向的 DNS 解析器",
"use_private_ptr_resolvers_desc": "使用私人上游伺服器、DHCP/etc/hosts 等方式解析包含私人 IP 位址的 ARPA 網域的 PTR、SOA NS 請求。如果停用AdGuard Home 將對所有此類請求以 NXDOMAIN 回應。",
"use_private_ptr_resolvers_desc": "透過私有上游伺服器、DHCP/etc/hosts 等管道,解析含有私有 IP 位址的 ARPA 網域的 PTR、SOA NS 請求。若停用此功能AdGuard Home 將以 NXDOMAIN 回應所有相關請求。",
"check_dhcp_servers": "檢查動態主機設定協定DHCP伺服器",
"save_config": "儲存配置",
"enabled_dhcp": "動態主機設定協定DHCP伺服器被啟用",
@@ -112,8 +112,8 @@
"privacy_policy": "隱私政策",
"enable_protection": "啟用防護",
"enabled_protection": "已啟用防護",
"disable_protection": "用防護",
"disabled_protection": "已用防護",
"disable_protection": "用防護",
"disabled_protection": "已用防護",
"refresh_statics": "重新整理統計資料",
"dns_query": "DNS 查詢",
"blocked_by": "<0>被過濾器封鎖</0>",
@@ -124,8 +124,8 @@
"for_last_hours_plural": "在過去的 {{count}} 小時內",
"for_last_days": "在最近的 {{count}} 日內",
"for_last_days_plural": "在最近的 {{count}} 日內",
"stats_disabled": "統計資料已被禁用。您可從<0>設定頁面</0>中打開它。",
"stats_disabled_short": "該統計資料已被禁用",
"stats_disabled": "統計功能目前停用中,請至<0>設定頁面</0>重新開啟。",
"stats_disabled_short": "該統計資料已用",
"no_domains_found": "無已發現之網域",
"requests_count": "請求總數",
"top_blocked_domains": "熱門已封鎖的網域",
@@ -172,13 +172,13 @@
"upstreams": "上游",
"upstream": "上游伺服器",
"apply_btn": "套用",
"disabled_filtering_toast": "已用過濾",
"disabled_filtering_toast": "已用過濾",
"enabled_filtering_toast": "已啟用過濾",
"disabled_safe_browsing_toast": "已用安全瀏覽",
"disabled_safe_browsing_toast": "已用安全瀏覽",
"enabled_safe_browsing_toast": "已啟用安全瀏覽",
"disabled_parental_toast": "已用家長控制",
"disabled_parental_toast": "已用家長控制",
"enabled_parental_toast": "已啟用家長控制",
"disabled_safe_search_toast": "已用安全搜尋",
"disabled_safe_search_toast": "已用安全搜尋",
"enabled_save_search_toast": "已啟用安全搜尋",
"updated_save_search_toast": "安全搜尋設定更新成功",
"enabled_table_header": "已啟用",
@@ -275,7 +275,7 @@
"query_log_retention": "查詢記錄保留時間",
"query_log_enable": "啟用記錄",
"query_log_configuration": "記錄配置",
"query_log_disabled": "查詢記錄被禁用並可在<0>設定</0>中被配置",
"query_log_disabled": "查詢記錄功能已停用,請至「<0>設定</0>」調整",
"query_log_strict_search": "使用雙引號於嚴謹的搜尋",
"query_log_retention_confirm": "您確定要更改記錄檔保存期限嗎?如果您縮短期限部分資料可能將會遺失",
"anonymize_client_ip": "將用戶端 IP 匿名",
@@ -401,7 +401,7 @@
"encryption_config_saved": "加密配置被儲存",
"encryption_server": "伺服器名稱",
"encryption_server_enter": "輸入您的域名",
"encryption_server_desc": "如果設定AdGuard Home 檢測用戶端 IDs回覆 DDR 查詢,並執行額外的連線驗證。如果未設定,這些功能被禁用。必須與在該憑證裡的 DNS 名稱其中之一相符。",
"encryption_server_desc": "如果設定AdGuard Home 會偵測 ClientID、回應 DDR 查詢,並執行其他連線驗證。如果未設定,則會停用這些功能。必須符合憑證中的一個 DNS 名稱。",
"encryption_redirect": "自動地重新導向到 HTTPS",
"encryption_redirect_desc": "如果被勾選AdGuard Home 將自動地重新導向您從 HTTP 到 HTTPS 位址。",
"encryption_https": "HTTPS 連接埠",
@@ -429,8 +429,8 @@
"encryption_reset": "您確定您想要重置加密設定嗎?",
"encryption_warning": "警告",
"encryption_plain_dns_enable": "啟用一般的 DNS",
"encryption_plain_dns_desc": "預設情況下啟用一般 DNS。使用者可以用它強制所有裝置使用一般的 DNS。為此,必須至少啟用一個一般的 DNS 協定",
"encryption_plain_dns_error": "要禁用一般 DNS至少啟用一個一般的 DNS 協定",
"encryption_plain_dns_desc": "預設啟用一般 DNS。可以用它強制所有裝置使用加密 DNS。若要這樣做,您必須啟用至少一個加密 DNS 通訊協定",
"encryption_plain_dns_error": "若要停用一般 DNS啟用至少一個加密 DNS 通訊協定",
"topline_expiring_certificate": "您的安全通訊端層SSL憑證即將到期。更新<0>加密設定</0>。",
"topline_expired_certificate": "您的安全通訊端層SSL憑證為已到期的。更新<0>加密設定</0>。",
"form_error_port_range": "輸入在 80-65535 之範圍內的連接埠號碼",
@@ -572,7 +572,7 @@
"filters_configuration": "過濾器配置",
"filters_enable": "啟用過濾器",
"filters_interval": "過濾器更新間隔",
"disabled": "已用",
"disabled": "已用",
"username_label": "使用者名稱",
"username_placeholder": "輸入使用者名稱",
"password_label": "密碼",
@@ -598,7 +598,7 @@
"rewrite_domain_name": "域名新增一筆正規名稱CNAME記錄",
"rewrite_A": "<0>A</0>:特殊的數值,阻止 <0>A</0> 記錄免於該上游",
"rewrite_AAAA": "<0>AAAA</0>:特殊的數值,阻止 <0>AAAA</0> 記錄免於該上游",
"disable_ipv6": "用 IPv6 位址解析",
"disable_ipv6": "用 IPv6 位址解析",
"disable_ipv6_desc": "停止所有對於 IPv6 位址(類型 AAAA的 DNS 查詢,並從 HTTPS 回應中移除 IPv6 的提示。",
"fastest_addr": "最快的 IP 位址",
"fastest_addr_desc": "等待<b>所有</b> DNS 伺服器的回應,測量每個伺服器的 TCP 連線速度,並返回連線速度最快的伺服器的 IP 位址。<br/>如果一個或多個上游伺服器沒有回應,此模式會顯著減慢 DNS 查詢速度。確保您的上游伺服器穩定且上游超時時間短。",
@@ -656,7 +656,7 @@
"blocklist": "封鎖清單",
"milliseconds_abbreviation": "ms",
"cache_size": "快取大小",
"cache_size_desc": "DNS 快取大小(以位元組)。要禁用快取,留空。",
"cache_size_desc": "DNS 快取大小 (位元組)。若要停用快取,留空。",
"cache_ttl_min_override": "覆寫最小的存活時間TTL",
"cache_ttl_max_override": "覆寫最大的存活時間TTL",
"enter_cache_size": "輸入快取大小(位元組)",
@@ -681,13 +681,13 @@
"port_53_faq_link": "連接埠 53 常被 \"DNSStubListener\" 或 \"systemd-resolved\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明</0>。",
"adg_will_drop_dns_queries": "AdGuard Home 將持續排除來自此用戶端之所有的 DNS 查詢。",
"filter_allowlist": "警告:此動作也將把 \"{{disallowed_rule}}\" 規則排除在被允許的用戶端的清單之外。",
"last_rule_in_allowlist": "因為排除 \"{{disallowed_rule}}\" 規則將禁用\"被允許的用戶端\"清單,無法不允許此用戶端。",
"last_rule_in_allowlist": "無法禁止此用戶端,因為排除規則 \"{{disallowed_rule}}\" 會停用「允許的用戶端」清單。",
"use_saved_key": "使用該先前已儲存的金鑰",
"parental_control": "家長控制",
"safe_browsing": "安全瀏覽",
"served_from_cache_label": "從快取中",
"form_error_password_length": "密碼長度必須為 {{min}} 到 {{max}} 個字符",
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。",
"anonymizer_notification": "<0>注意:</0>IP 匿名功能已開啟。您可在<1>一般設定</1>中關閉。",
"confirm_dns_cache_clear": "您確定您想要清除 DNS 快取嗎?",
"cache_cleared": "DNS 快取被成功地清除",
"clear_cache": "清除快取",
@@ -702,14 +702,14 @@
"disable_for_hours": "{{count}} 小時",
"disable_for_hours_plural": "{{count}} 小時",
"disable_until_tomorrow": "直到明天",
"disable_notify_for_seconds": "計 {{count}} 秒用防護",
"disable_notify_for_seconds_plural": "計 {{count}} 秒用防護",
"disable_notify_for_minutes": "計 {{count}} 分鐘用防護",
"disable_notify_for_minutes_plural": "計 {{count}} 分鐘用防護",
"disable_notify_for_hours": "計 {{count}} 小時用防護",
"disable_notify_for_hours_plural": "計 {{count}} 小時用防護",
"disable_notify_until_tomorrow": "用防護直到明天",
"enable_protection_timer": "防護將於 {{time}} 啟用",
"disable_notify_for_seconds": "計 {{count}} 秒用防護",
"disable_notify_for_seconds_plural": "計 {{count}} 秒用防護",
"disable_notify_for_minutes": "計 {{count}} 分鐘用防護",
"disable_notify_for_minutes_plural": "計 {{count}} 分鐘用防護",
"disable_notify_for_hours": "計 {{count}} 小時用防護",
"disable_notify_for_hours_plural": "計 {{count}} 小時用防護",
"disable_notify_until_tomorrow": "用防護直到明天",
"enable_protection_timer": "防護將於 {{time}} 啟用",
"custom_retention_input": "輸入保留時間(小時)",
"custom_rotation_input": "輸入旋轉時間(小時)",
"protection_section_label": "防護",

View File

@@ -78,6 +78,7 @@ class CustomRules extends Component<CustomRulesProps> {
<form onSubmit={this.handleSubmit}>
<div className="text-edit-container mb-4">
<textarea
data-testid="custom_rule_textarea"
className="form-control font-monospace text-input"
value={userRules}
onChange={this.handleChange}
@@ -91,6 +92,7 @@ class CustomRules extends Component<CustomRulesProps> {
<div className="card-actions">
<button
data-testid="apply_custom_rule"
className="btn btn-success btn-standard btn-large"
type="submit"
onClick={this.handleSubmit}>

View File

@@ -59,7 +59,7 @@ const Header = () => {
<div className="header__column">
<div className="header__right">
{!processingProfile && name && (
<a href="control/logout" className="btn btn-sm btn-outline-secondary">
<a href="control/logout" className="btn btn-sm btn-outline-secondary" data-testid="sign_out">
{t('sign_out')}
</a>
)}

View File

@@ -288,7 +288,7 @@ const Row = memo(
);
return (
<div style={style} className={className} onClick={onClick} role="row">
<div style={style} className={className} onClick={onClick} role="row" data-testid="querylog_cell">
<DateCell {...rowProps} />
<DomainCell {...rowProps} />

View File

@@ -84,6 +84,7 @@ export const Form = ({ className, setIsLoading }: Props) => {
}}>
<div className="field__search">
<SearchField
data-testid="querylog_search"
value={searchValue}
handleChange={(val) => setValue('search', val)}
onKeyDown={onEnterPress}

View File

@@ -27,12 +27,14 @@ const SETTINGS = {
enabled: false,
title: i18next.t('use_adguard_browsing_sec'),
subtitle: i18next.t('use_adguard_browsing_sec_hint'),
testId: 'safebrowsing',
[ORDER_KEY]: 0,
},
parental: {
enabled: false,
title: i18next.t('use_adguard_parental'),
subtitle: i18next.t('use_adguard_parental_hint'),
testId: 'parental',
[ORDER_KEY]: 1,
},
};
@@ -90,11 +92,12 @@ class Settings extends Component<SettingsProps> {
renderSettings = (settings: any) =>
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
const setting = settings[key];
const { enabled, title, subtitle } = setting;
const { enabled, title, subtitle, testId } = setting;
return (
<div key={key} className="form__group form__group--checkbox">
<Checkbox
data-testid={testId}
value={enabled}
title={title}
subtitle={subtitle}
@@ -118,6 +121,7 @@ class Settings extends Component<SettingsProps> {
<>
<div className="form__group form__group--checkbox">
<Checkbox
data-testid="safesearch"
value={enabled}
title={i18next.t('enforce_safe_search')}
subtitle={i18next.t('enforce_save_search_hint')}

View File

@@ -94,14 +94,17 @@ const Footer = () => {
auto: {
desc: t('theme_auto_desc'),
icon: '#auto',
testId: 'theme_auto',
},
dark: {
desc: t('theme_dark_desc'),
icon: '#dark',
testId: 'theme_dark',
},
light: {
desc: t('theme_light_desc'),
icon: '#light',
testId: 'theme_light',
},
};
@@ -113,7 +116,9 @@ const Footer = () => {
type="button"
className="btn btn-sm btn-secondary footer__theme-button"
onClick={() => onThemeChange(theme)}
title={content[theme].desc}>
title={content[theme].desc}
data-testid={content[theme].testId}
>
<svg className={cn('footer__theme-icon', { 'footer__theme-icon--active': currentValue === theme })}>
<use xlinkHref={content[theme].icon} />
</svg>

View File

@@ -62,7 +62,7 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
{...field}
data-testid="password"
type="password"
label={t('username_label')}
label={t('password_label')}
placeholder={t('password_placeholder')}
error={fieldState.error?.message}
autoComplete="current-password"

View File

@@ -0,0 +1,34 @@
import { test, expect } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('Control Panel', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('should sign out successfully', async ({ page }) => {
await page.getByTestId('sign_out').click();
await page.waitForURL((url) => url.href.endsWith('/login.html'));
await expect(page.getByTestId('sign_in')).toBeVisible();
});
test('should change theme to dark and then light', async ({ page }) => {
await page.getByTestId('theme_dark').click();
await expect(page.locator('body[data-theme="dark"]')).toBeVisible();
await page.getByTestId('theme_light').click();
await expect(page.locator('body:not([data-theme="dark"])')).toBeVisible();
});
});

View File

@@ -0,0 +1,52 @@
import { test, expect, type Page } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('DNS Settings', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
const runDNSSettingsTest = async (page: Page, address: string) => {
await page.goto('/#dns');
const currentDns = await page.getByTestId('upstream_dns').inputValue();
await page.getByTestId('upstream_dns').fill(address);
await page.getByTestId('dns_upstream_test').click();
await page.waitForTimeout(2000);
await expect(page.getByTestId('upstream_dns')).toHaveValue(address);
await page.getByTestId('upstream_dns').fill(currentDns);
await page.getByTestId('dns_upstream_save').click({ force: true });
};
test('test for Default DNS', async ({ page }) => {
await runDNSSettingsTest(page, 'https://dns10.quad9.net/dns-query');
});
test('test for Plain DNS', async ({ page }) => {
await runDNSSettingsTest(page, '94.140.14.140');
});
test('test for DNS-over-HTTPS', async ({ page }) => {
await runDNSSettingsTest(page, 'https://unfiltered.adguard-dns.com/dns-query');
});
test('test for DNS-over-TLS', async ({ page }) => {
await runDNSSettingsTest(page, 'tls://unfiltered.adguard-dns.com');
});
test('test for DNS-over-QUIC', async ({ page }) => {
await runDNSSettingsTest(page, 'quic://unfiltered.adguard-dns.com');
});
});

View File

@@ -0,0 +1,73 @@
import { test, expect, type Page } from '@playwright/test';
import { execSync } from 'child_process';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('Filtering', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
const runTerminalCommand = (command: string) => {
try {
console.info(`Executing command: ${command}`);
const output = execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
console.info('Command executed successfully.');
console.debug(`Command output:\n${output}`);
return output;
} catch (error: any) {
console.error(`Command execution failed with error:\n${error.message}`);
throw new Error(`Failed to execute command: ${command}\nError: ${error.message}`);
}
}
const runCustomRuleTest = async (page: Page, domain_to_block: string) => {
await page.goto('/#custom_rules');
await page.getByTestId('custom_rule_textarea').fill(domain_to_block);
await page.getByTestId('apply_custom_rule').click();
const nslookupBlockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
console.info(`nslookup blocked CNAME result: '${nslookupBlockedResult}'`);
const currentRules = await page.getByTestId('custom_rule_textarea').inputValue();
console.debug(`Current rules before removal:\n${currentRules}`);
if (currentRules.includes(domain_to_block)) {
const updatedRules = currentRules
.split('\n')
.filter((line) => line.trim() !== domain_to_block.trim())
.join('\n');
await page.getByTestId('custom_rule_textarea').fill(updatedRules);
console.info(`Rule '${domain_to_block}' removed successfully.`);
console.info('Applying the updated filtering rules after removal.');
await page.getByTestId('apply_custom_rule').click();
await page.waitForLoadState('domcontentloaded');
console.info(`Filtering rules successfully updated after removing '${domain_to_block}'.`);
} else {
console.warn(`Rule '${domain_to_block}' not found. No changes were made.`);
}
const nslookupUnblockedResult = await runTerminalCommand(`nslookup ${domain_to_block} 127.0.0.1`).toString();
console.info(`nslookup unblocked CNAME result: '${nslookupUnblockedResult}'`);
};
test('Test blocking rule for apple.com', async ({ page }) => {
await runCustomRuleTest(page, 'apple.com');
});
});

View File

@@ -0,0 +1,89 @@
import { test, expect } from '@playwright/test';
import { execSync } from 'child_process';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('General Settings', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('should toggle browsing security feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
const browsingSecurity = await page.getByTestId('safebrowsing');
const browsingSecurityLabel = await browsingSecurity.locator('xpath=following-sibling::*[1]');
const initialState = await browsingSecurity.isChecked();
if (!initialState) {
await browsingSecurityLabel.click();
await expect(browsingSecurity).toBeChecked();
}
const resultEnabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
await browsingSecurityLabel.click();
await expect(browsingSecurity).not.toBeChecked();
const resultDisabled = execSync('nslookup totalvirus.com 127.0.0.1').toString();
expect(resultEnabled).not.toEqual(resultDisabled);
if (initialState) {
await browsingSecurityLabel.click();
await expect(browsingSecurity).toBeChecked();
}
});
test('should toggle parental control feature and verify DNS changes', async ({ page }) => {
await page.goto('/#settings');
const parentalControl = page.getByTestId('parental');
const parentalControlLabel = await parentalControl.locator('xpath=following-sibling::*[1]');
const initialState = await parentalControl.isChecked();
if (!initialState) {
await parentalControlLabel.click();
await expect(parentalControl).toBeChecked();
}
const resultEnabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
await parentalControlLabel.click();
await expect(parentalControl).not.toBeChecked();
const resultDisabled = execSync('nslookup pornhub.com 127.0.0.1').toString();
expect(resultEnabled).not.toEqual(resultDisabled);
if (initialState) {
await parentalControlLabel.click();
await expect(parentalControl).toBeChecked();
}
});
test('should toggle safe search feature', async ({ page }) => {
await page.goto('/#settings');
const safeSearch = page.getByTestId('safesearch');
const safeSearchLabel = await safeSearch.locator('xpath=following-sibling::*[1]');
const initialState = await safeSearch.isChecked();
await safeSearchLabel.click();
await expect(safeSearch).not.toBeChecked({ checked: initialState });
await safeSearchLabel.click();
await expect(safeSearch).toBeChecked({ checked: initialState });
});
});

View File

@@ -0,0 +1,124 @@
import { test, expect } from '@playwright/test';
import { ADMIN_USERNAME, ADMIN_PASSWORD } from '../constants';
test.describe('QueryLog', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login.html');
await page.getByTestId('username').click();
await page.getByTestId('username').fill(ADMIN_USERNAME);
await page.getByTestId('password').click();
await page.getByTestId('password').fill(ADMIN_PASSWORD);
await page.keyboard.press('Tab');
await page.getByTestId('sign_in').click();
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
});
test('Search of queryLog should work correctly', async ({ page }) => {
await page.route('/control/querylog', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(
{
"data": [
{
"answer": [
{
"type": "A",
"value": "77.88.44.242",
"ttl": 294
},
{
"type": "A",
"value": "5.255.255.242",
"ttl": 294
},
{
"type": "A",
"value": "77.88.55.242",
"ttl": 294
}
],
"answer_dnssec": false,
"cached": false,
"client": "127.0.0.1",
"client_info": {
"whois": {},
"name": "localhost",
"disallowed_rule": "127.0.0.1",
"disallowed": false
},
"client_proto": "",
"elapsedMs": "78.163167",
"question": {
"class": "IN",
"name": "ya.ru",
"type": "A"
},
"reason": "NotFilteredNotFound",
"rules": [],
"status": "NOERROR",
"time": "2024-07-17T16:02:37.500662+02:00",
"upstream": "https://dns10.quad9.net:443/dns-query"
},
{
"answer": [
{
"type": "A",
"value": "77.88.55.242",
"ttl": 351
},
{
"type": "A",
"value": "77.88.44.242",
"ttl": 351
},
{
"type": "A",
"value": "5.255.255.242",
"ttl": 351
}
],
"answer_dnssec": false,
"cached": false,
"client": "127.0.0.1",
"client_info": {
"whois": {},
"name": "localhost",
"disallowed_rule": "127.0.0.1",
"disallowed": false
},
"client_proto": "",
"elapsedMs": "5051.070708",
"question": {
"class": "IN",
"name": "ya.ru",
"type": "A"
},
"reason": "NotFilteredNotFound",
"rules": [],
"status": "NOERROR",
"time": "2024-07-17T16:02:37.4983+02:00",
"upstream": "https://dns10.quad9.net:443/dns-query"
}
],
"oldest": "2024-07-17T16:02:37.4983+02:00"
}
),
});
});
await page.goto('/#logs');
await page.getByTestId('querylog_search').fill('127.0.0.1');
const [request] = await Promise.all([
page.waitForRequest((req) => req.url().includes('/control/querylog')),
]);
if (request) {
expect(request.url()).toContain('search=127.0.0.1');
expect(await page.getByTestId('querylog_cell').first().isVisible()).toBe(true);
}
});
});

View File

@@ -1,12 +1,12 @@
# A docker file for scripts/make/build-docker.sh.
FROM alpine:3.18
FROM alpine:3.21
ARG BUILD_DATE
ARG VERSION
ARG VCS_REF
LABEL\
LABEL \
maintainer="AdGuard Team <devteam@adguard.com>" \
org.opencontainers.image.authors="AdGuard Team <devteam@adguard.com>" \
org.opencontainers.image.created=$BUILD_DATE \
@@ -30,8 +30,8 @@ ARG TARGETARCH
ARG TARGETOS
ARG TARGETVARIANT
COPY --chown=nobody:nogroup\
./${DIST_DIR}/docker/AdGuardHome_${TARGETOS}_${TARGETARCH}_${TARGETVARIANT}\
COPY --chown=nobody:nogroup \
./${DIST_DIR}/docker/AdGuardHome_${TARGETOS}_${TARGETARCH}_${TARGETVARIANT} \
/opt/adguardhome/AdGuardHome
RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
@@ -45,8 +45,15 @@ RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
# 3000 : TCP, UDP : HTTP(S) (alt, incl. HTTP/3)
# 5443 : TCP, UDP : DNSCrypt (alt)
# 6060 : TCP : HTTP (pprof)
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 443/udp 853/tcp\
853/udp 3000/tcp 3000/udp 5443/tcp 5443/udp 6060/tcp
EXPOSE 53/tcp 53/udp \
67/udp \
68/udp \
80/tcp \
443/tcp 443/udp \
853/tcp 853/udp \
3000/tcp 3000/udp \
5443/tcp 5443/udp \
6060/tcp
WORKDIR /opt/adguardhome/work

73
go.mod
View File

@@ -1,17 +1,17 @@
module github.com/AdguardTeam/AdGuardHome
go 1.24.1
go 1.24.2
require (
github.com/AdguardTeam/dnsproxy v0.75.1
github.com/AdguardTeam/golibs v0.32.5
github.com/AdguardTeam/dnsproxy v0.75.3
github.com/AdguardTeam/golibs v0.32.8
github.com/AdguardTeam/urlfilter v0.20.0
github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.3.0
github.com/ameshkov/dnscrypt/v2 v2.4.0
github.com/bluele/gcache v0.0.2
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/digineo/go-ipset/v2 v2.2.1
github.com/fsnotify/fsnotify v1.8.0
github.com/fsnotify/fsnotify v1.9.0
// TODO(e.burkov): This package is deprecated; find a new one or use our
// own code for that. Perhaps, use gopacket.
github.com/go-ping/ping v1.2.0
@@ -28,33 +28,31 @@ require (
// TODO(a.garipov): This package is deprecated; find a new one or use our
// own code for that. Perhaps, use gopacket.
github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.63
github.com/quic-go/quic-go v0.49.0
github.com/miekg/dns v1.1.65
github.com/quic-go/quic-go v0.50.1
github.com/stretchr/testify v1.10.0
github.com/ti-mo/netfilter v0.5.2
go.etcd.io/bbolt v1.4.0
golang.org/x/crypto v0.36.0
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
golang.org/x/net v0.37.0
golang.org/x/sys v0.31.0
golang.org/x/crypto v0.37.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/net v0.39.0
golang.org/x/sys v0.32.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.1
)
require (
cloud.google.com/go v0.119.0 // indirect
cloud.google.com/go/ai v0.10.1 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go v0.120.1 // indirect
cloud.google.com/go/ai v0.10.2 // indirect
cloud.google.com/go/auth v0.16.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/longrunning v0.6.6 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
@@ -63,7 +61,7 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golangci/misspell v0.6.0 // indirect
github.com/google/generative-ai-go v0.19.0 // indirect
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
@@ -72,7 +70,7 @@ require (
github.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -80,7 +78,7 @@ require (
github.com/quic-go/qpack v0.5.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/securego/gosec/v2 v2.22.2 // indirect
github.com/securego/gosec/v2 v2.22.3 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
@@ -90,26 +88,27 @@ require (
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/tools v0.32.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
gonum.org/v1/gonum v0.15.1 // indirect
google.golang.org/api v0.227.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/api v0.229.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
honnef.co/go/tools v0.6.1 // indirect
mvdan.cc/editorconfig v0.3.0 // indirect
mvdan.cc/gofumpt v0.7.0 // indirect
mvdan.cc/gofumpt v0.8.0 // indirect
mvdan.cc/sh/v3 v3.11.0 // indirect
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect
)

148
go.sum
View File

@@ -1,31 +1,27 @@
cloud.google.com/go v0.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE=
cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08=
cloud.google.com/go/ai v0.10.1 h1:EU93KqYmMeOKgaBXAz2DshH2C/BzAT1P+iJORksLIic=
cloud.google.com/go/ai v0.10.1/go.mod h1:sWWHZvmJ83BjuxAQtYEiA0SFTpijtbH+SXWFO14ri5A=
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go v0.120.1 h1:Z+5V7yd383+9617XDCyszmK5E4wJRJL+tquMfDj9hLM=
cloud.google.com/go v0.120.1/go.mod h1:56Vs7sf/i2jYM6ZL9NYlC82r04PThNcPS5YgFmb0rp8=
cloud.google.com/go/ai v0.10.2 h1:5NHzmZlRs+3kvlsVdjT0cTnLrjQdROJ/8VOljVfs+8o=
cloud.google.com/go/ai v0.10.2/go.mod h1:xZuZuE9d3RgsR132meCnPadiU9XV0qXjpLr+P4J46eE=
cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
github.com/AdguardTeam/dnsproxy v0.75.1 h1:ux2sQfF/9+WRo6a32g9NtfaAPU19gJhqkEu2OZflxJg=
github.com/AdguardTeam/dnsproxy v0.75.1/go.mod h1:HKBI/IO2/ACOjfTV6qIzB5ZDDxfjgHHvQ3hIbGg9wvc=
github.com/AdguardTeam/golibs v0.32.5 h1:4Rkv2xBnyJe6l/EM2MFgoY1S4pweYwDgLTYg2MDArEA=
github.com/AdguardTeam/golibs v0.32.5/go.mod h1:agsvz8Iyv0uV9NU56hpCoFLAtSPkiBf9nPVhDvdUIb0=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
github.com/AdguardTeam/dnsproxy v0.75.3 h1:pxlMNO+cP1A3px40PY/old6SAE82pkdLPUA2P3KY8u0=
github.com/AdguardTeam/dnsproxy v0.75.3/go.mod h1:50OyTHao+uQzUJiXay08hgfvWQ3o2Q2WV99W8u8ypDE=
github.com/AdguardTeam/golibs v0.32.8 h1:O3mc3kYcPkW3kbmd+gqzFNgUka13a+iBgFLThwOYSQE=
github.com/AdguardTeam/golibs v0.32.8/go.mod h1:McV1QFFlKLElKa306V4OL/T2kr7564PhsayfvTWYBVs=
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=
github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP2zNLs=
github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE=
github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=
github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
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=
@@ -34,8 +30,8 @@ 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/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -43,8 +39,8 @@ github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1M
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -76,8 +72,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
@@ -128,12 +124,12 @@ github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5
github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
@@ -145,16 +141,18 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94=
github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g=
github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE=
github.com/securego/gosec/v2 v2.22.3 h1:mRrCNmRF2NgZp4RJ8oJ6yPJ7G4x6OCiAXHd8x4trLRc=
github.com/securego/gosec/v2 v2.22.3/go.mod h1:42M9Xs0v1WseinaB/BmNGO8AVqG8vRfhC2686ACY48k=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@@ -199,16 +197,18 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 h1:VI4qDpTkfFaCXEPrbojidLgVQhj2x4nzTccG0hjaLlU=
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0 h1:oMe07YcizemJ09rs2kRkFYAp0pt4e1lYLwPWiEGMpXE=
golang.org/x/exp/typeparams v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -221,14 +221,14 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -241,43 +241,43 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314 h1:UY+gQAskx5vohcvUlJDKkJPt9lALCgtZs3rs8msRatU=
golang.org/x/telemetry v0.0.0-20250310203348-fdfaad844314/go.mod h1:16eI1RtbPZAEm3u7hpIh7JM/w5AbmlDtnrdKYaREic8=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3 h1:RXY2+rSHXvxO2Y+gKrPjYVaEoGOqh3VEXFhnWAt1Irg=
golang.org/x/telemetry v0.0.0-20250417124945-06ef541f3fa3/go.mod h1:RoaXAWDwS90j6FxVKwJdBV+0HCU+llrKUGgJaxiKl6M=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc=
google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -292,8 +292,8 @@ howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=
mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
mvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k=
mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8=

View File

@@ -0,0 +1,58 @@
package aghuser
import (
"context"
"github.com/AdguardTeam/golibs/errors"
"golang.org/x/crypto/bcrypt"
)
// Login is the type for web user logins.
type Login string
// NewLogin returns a web user login.
//
// TODO(s.chzhen): Add more constraints as needed.
func NewLogin(s string) (l Login, err error) {
if s == "" {
return "", errors.ErrEmptyValue
}
return Login(s), nil
}
// Password is an interface that defines methods for handling web user
// passwords.
type Password interface {
// Authenticate returns true if the provided password is allowed.
Authenticate(ctx context.Context, password string) (ok bool)
// Hash returns a hashed representation of the web user password.
Hash() (b []byte)
}
// DefaultPassword is the default bcrypt implementation of the [Password]
// interface.
type DefaultPassword struct {
hash []byte
}
// NewDefaultPassword returns the new properly initialized *DefaultPassword.
func NewDefaultPassword(hash string) (p *DefaultPassword) {
return &DefaultPassword{
hash: []byte(hash),
}
}
// type check
var _ Password = (*DefaultPassword)(nil)
// Authenticate implements the [Password] interface for *DefaultPassword.
func (p *DefaultPassword) Authenticate(ctx context.Context, passwd string) (ok bool) {
return bcrypt.CompareHashAndPassword([]byte(p.hash), []byte(passwd)) == nil
}
// Hash implements the [Password] interface for *DefaultPassword.
func (p *DefaultPassword) Hash() (b []byte) {
return p.hash
}

View File

@@ -0,0 +1,6 @@
package aghuser_test
import "time"
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second

149
internal/aghuser/db.go Normal file
View File

@@ -0,0 +1,149 @@
package aghuser
import (
"cmp"
"context"
"fmt"
"maps"
"slices"
"sync"
"github.com/AdguardTeam/golibs/errors"
)
// DB is an interface that defines methods for interacting with user
// information. All methods must be safe for concurrent use.
//
// TODO(s.chzhen): Use this.
//
// TODO(s.chzhen): Consider updating methods to return a clone.
type DB interface {
// All retrieves all users from the database, sorted by login.
//
// TODO(s.chzhen): Consider function signature change to reflect the
// in-memory implementation, as it currently always returns nil for error.
All(ctx context.Context) (users []*User, err error)
// ByLogin retrieves a user by their login. u must not be modified.
//
// TODO(s.chzhen): Remove this once user sessions support [UserID].
ByLogin(ctx context.Context, login Login) (u *User, err error)
// ByUUID retrieves a user by their unique identifier. u must not be
// modified.
//
// TODO(s.chzhen): Use this.
ByUUID(ctx context.Context, id UserID) (u *User, err error)
// Create adds a new user to the database. If the credentials already
// exist, it returns the [errors.ErrDuplicated] error. It also can return
// an error from the cryptographic randomness reader. u must not be
// modified.
Create(ctx context.Context, u *User) (err error)
}
// DefaultDB is the default in-memory implementation of the [DB] interface.
type DefaultDB struct {
// mu protects all properties below.
mu *sync.Mutex
// loginToUserID maps a web user login to their UserID. The values must not
// be empty.
//
// TODO(s.chzhen): Remove this once user sessions support [UserID].
loginToUserID map[Login]UserID
// userIDToUser maps a UserID to a web user. The values must not be nil.
// It must be synchronized with loginToUserID, meaning all UserIDs stored in
// loginToUserID must also be stored in this map.
userIDToUser map[UserID]*User
}
// NewDefaultDB returns the new properly initialized *DefaultDB.
func NewDefaultDB() (db *DefaultDB) {
return &DefaultDB{
mu: &sync.Mutex{},
loginToUserID: map[Login]UserID{},
userIDToUser: map[UserID]*User{},
}
}
// type check
var _ DB = (*DefaultDB)(nil)
// All implements the [DB] interface for *DefaultDB.
func (db *DefaultDB) All(ctx context.Context) (users []*User, err error) {
db.mu.Lock()
defer db.mu.Unlock()
if len(db.userIDToUser) == 0 {
return nil, nil
}
users = slices.SortedStableFunc(
maps.Values(db.userIDToUser),
func(a, b *User) (res int) {
// TODO(s.chzhen): Consider adding a custom comparer.
return cmp.Compare(a.Login, b.Login)
},
)
return users, nil
}
// ByLogin implements the [DB] interface for *DefaultDB.
func (db *DefaultDB) ByLogin(ctx context.Context, login Login) (u *User, err error) {
db.mu.Lock()
defer db.mu.Unlock()
id, ok := db.loginToUserID[login]
if !ok {
return nil, nil
}
u, ok = db.userIDToUser[id]
if !ok {
// Should not happen.
panic(fmt.Errorf("no web user present with login %q", login))
}
return u, nil
}
// ByUUID implements the [DB] interface for *DefaultDB.
func (db *DefaultDB) ByUUID(ctx context.Context, id UserID) (u *User, err error) {
db.mu.Lock()
defer db.mu.Unlock()
u, ok := db.userIDToUser[id]
if !ok {
return nil, nil
}
return u, nil
}
// Create implements the [DB] interface for *DefaultDB.
func (db *DefaultDB) Create(ctx context.Context, u *User) (err error) {
db.mu.Lock()
defer db.mu.Unlock()
if u.ID == (UserID{}) {
return fmt.Errorf("userid: %w", errors.ErrEmptyValue)
}
_, ok := db.userIDToUser[u.ID]
if ok {
return fmt.Errorf("userid: %w", errors.ErrDuplicated)
}
_, ok = db.loginToUserID[u.Login]
if ok {
return fmt.Errorf("login: %w", errors.ErrDuplicated)
}
db.userIDToUser[u.ID] = u
db.loginToUserID[u.Login] = u.ID
return nil
}

View File

@@ -0,0 +1,83 @@
package aghuser_test
import (
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghuser"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
)
func TestDB(t *testing.T) {
db := aghuser.NewDefaultDB()
const (
userWithIDPassRaw = "user_with_id_password"
userSecondPassRaw = "user_second_password"
)
userWithIDPassHash, err := bcrypt.GenerateFromPassword(
[]byte(userWithIDPassRaw),
bcrypt.DefaultCost,
)
require.NoError(t, err)
userSecondPassHash, err := bcrypt.GenerateFromPassword(
[]byte(userSecondPassRaw),
bcrypt.DefaultCost,
)
require.NoError(t, err)
userWithIDPass := aghuser.NewDefaultPassword(string(userWithIDPassHash))
userSecondPass := aghuser.NewDefaultPassword(string(userSecondPassHash))
var (
userWithID = &aghuser.User{
ID: aghuser.MustNewUserID(),
Login: "user_with_id",
Password: userWithIDPass,
}
userSecond = &aghuser.User{
ID: aghuser.MustNewUserID(),
Login: "user_second",
Password: userSecondPass,
}
userDuplicateLogin = &aghuser.User{
ID: aghuser.MustNewUserID(),
Login: userWithID.Login,
Password: userWithIDPass,
}
)
ctx := testutil.ContextWithTimeout(t, testTimeout)
err = db.Create(ctx, userWithID)
require.NoError(t, err)
err = db.Create(ctx, userSecond)
require.NoError(t, err)
err = db.Create(ctx, userDuplicateLogin)
assert.ErrorIs(t, err, errors.ErrDuplicated)
got, err := db.ByUUID(ctx, userWithID.ID)
require.NoError(t, err)
assert.Equal(t, userWithID, got)
assert.True(t, got.Password.Authenticate(ctx, userWithIDPassRaw))
got, err = db.ByLogin(ctx, userSecond.Login)
require.NoError(t, err)
assert.Equal(t, userSecond, got)
assert.True(t, got.Password.Authenticate(ctx, userSecondPassRaw))
users, err := db.All(ctx)
require.NoError(t, err)
assert.Len(t, users, 2)
assert.Equal(t, []*aghuser.User{userSecond, userWithID}, users)
}

44
internal/aghuser/user.go Normal file
View File

@@ -0,0 +1,44 @@
// Package aghuser contains types and logic for dealing with AdGuard Home's web
// users.
package aghuser
import (
"fmt"
"github.com/google/uuid"
)
// UserID is the type for the unique IDs of web users.
type UserID uuid.UUID
// NewUserID returns a new web user unique identifier. Any error returned is an
// error from the cryptographic randomness reader.
func NewUserID() (uid UserID, err error) {
uuidv7, err := uuid.NewV7()
return UserID(uuidv7), err
}
// MustNewUserID is a wrapper around [NewUserID] that panics if there is an
// error. It is currently only used in tests.
func MustNewUserID() (uid UserID) {
uid, err := NewUserID()
if err != nil {
panic(fmt.Errorf("unexpected uuidv7 error: %w", err))
}
return uid
}
// User represents a web user.
type User struct {
// ID is the unique identifier for the web user. It must not be empty.
ID UserID
// Login is the login name of the web user. It must not be empty.
Login Login
// Password stores the password information for the web user. It must not
// be nil.
Password Password
}

View File

@@ -496,6 +496,11 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
return p.ShallowClone(), ok
}
foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil {
return s.FindByMAC(foundMAC)
}
p = s.index.findByIPWithoutZone(ip)
if p != nil {
return p.ShallowClone(), true
@@ -682,6 +687,13 @@ func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filter
c, ok = s.index.findByIP(addr)
}
if !ok {
foundMAC := s.dhcp.MACByIP(addr)
if foundMAC != nil {
c, ok = s.FindByMAC(foundMAC)
}
}
if !ok {
s.logger.Debug("no client filtering settings found", "clientid", id, "addr", addr)

View File

@@ -281,6 +281,10 @@ type ServerConfig struct {
// ServePlainDNS defines if plain DNS is allowed for incoming requests.
ServePlainDNS bool
// PendingRequestsEnabled defines if duplicate requests should be forwarded
// to upstreams along with the original one.
PendingRequestsEnabled bool
}
// UpstreamMode is a enumeration of upstream mode representations. See
@@ -324,6 +328,9 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
UsePrivateRDNS: srvConf.UsePrivateRDNS,
PrivateSubnets: s.privateNets,
MessageConstructor: s,
PendingRequests: &proxy.PendingRequestsConfig{
Enabled: srvConf.PendingRequestsEnabled,
},
}
if srvConf.EDNSClientSubnet.UseCustom {

View File

@@ -78,11 +78,11 @@ func TestHostnameToHashes(t *testing.T) {
wantLen: 2,
}, {
name: "private_domain_v2",
host: "foo.blogspot.co.uk",
wantLen: 4,
host: "foo.dyndns.org",
wantLen: 3,
}, {
name: "sub_private_domain_v2",
host: "bar.foo.blogspot.co.uk",
host: "bar.foo.dyndns.org",
wantLen: 4,
}}

View File

@@ -261,6 +261,16 @@ type dnsConfig struct {
// HostsFileEnabled defines whether to use information from the system hosts
// file to resolve queries.
HostsFileEnabled bool `yaml:"hostsfile_enabled"`
// PendingRequests configures duplicate requests policy.
PendingRequests *pendingRequests `yaml:"pending_requests"`
}
// pendingRequests is a block with pending requests configuration.
type pendingRequests struct {
// Enabled controls if duplicate requests should be sent to the upstreams
// along with the original one.
Enabled bool `yaml:"enabled"`
}
type tlsConfigSettings struct {
@@ -380,6 +390,9 @@ var config = &configuration{
UsePrivateRDNS: true,
ServePlainDNS: true,
HostsFileEnabled: true,
PendingRequests: &pendingRequests{
Enabled: true,
},
},
TLS: tlsConfigSettings{
PortHTTPS: defaultPortHTTPS,
@@ -568,7 +581,7 @@ func parseConfig() (err error) {
}
// Do not wrap the error because it's informative enough as is.
return setContextTLSCipherIDs()
return validateTLSCipherIDs(config.TLS.OverrideTLSCiphers)
}
// validateConfig returns error if the configuration is invalid.
@@ -721,21 +734,15 @@ func (c *configuration) write(tlsMgr *tlsManager) (err error) {
return nil
}
// setContextTLSCipherIDs sets the TLS cipher suite IDs to use.
func setContextTLSCipherIDs() (err error) {
if len(config.TLS.OverrideTLSCiphers) == 0 {
log.Info("tls: using default ciphers")
globalContext.tlsCipherIDs = aghtls.SaferCipherSuites()
// validateTLSCipherIDs validates the custom TLS cipher suite IDs.
func validateTLSCipherIDs(cipherIDs []string) (err error) {
if len(cipherIDs) == 0 {
return nil
}
log.Info("tls: overriding ciphers: %s", config.TLS.OverrideTLSCiphers)
globalContext.tlsCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
_, err = aghtls.ParseCiphers(cipherIDs)
if err != nil {
return fmt.Errorf("parsing override ciphers: %w", err)
return fmt.Errorf("override_tls_ciphers: %w", err)
}
return nil

View File

@@ -38,6 +38,8 @@ const (
)
// Called by other modules when configuration is changed
//
// TODO(s.chzhen): Remove this after refactoring.
func onConfigModified() {
err := config.write(globalContext.tls)
if err != nil {
@@ -120,14 +122,15 @@ func initDNS(
anonymizer,
httpRegister,
tlsConf,
tlsMgr,
baseLogger,
)
}
// initDNSServer initializes the [context.dnsServer]. To only use the internal
// proxy, none of the arguments are required, but tlsConf and l still must not
// be nil, in other cases all the arguments also must not be nil. It also must
// not be called unless [config] and [globalContext] are initialized.
// proxy, none of the arguments are required, but tlsConf, tlsMgr and l still
// must not be nil, in other cases all the arguments also must not be nil. It
// also must not be called unless [config] and [globalContext] are initialized.
//
// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.
func initDNSServer(
@@ -138,6 +141,7 @@ func initDNSServer(
anonymizer *aghnet.IPMut,
httpReg aghhttp.RegisterFunc,
tlsConf *tlsConfigSettings,
tlsMgr *tlsManager,
l *slog.Logger,
) (err error) {
globalContext.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
@@ -166,6 +170,7 @@ func initDNSServer(
&config.DNS,
config.Clients.Sources,
tlsConf,
tlsMgr,
httpReg,
globalContext.clients.storage,
)
@@ -236,11 +241,12 @@ func ipsToUDPAddrs(ips []netip.Addr, port uint16) (udpAddrs []*net.UDPAddr) {
}
// newServerConfig converts values from the configuration file into the internal
// DNS server configuration. All arguments must not be nil.
// DNS server configuration. All arguments must not be nil, except for httpReg.
func newServerConfig(
dnsConf *dnsConfig,
clientSrcConf *clientSourcesConfig,
tlsConf *tlsConfigSettings,
tlsMgr *tlsManager,
httpReg aghhttp.RegisterFunc,
clientsContainer dnsforward.ClientsContainer,
) (newConf *dnsforward.ServerConfig, err error) {
@@ -256,7 +262,7 @@ func newServerConfig(
TLSConfig: newDNSTLSConfig(tlsConf, hosts),
TLSAllowUnencryptedDoH: tlsConf.AllowUnencryptedDoH,
UpstreamTimeout: time.Duration(dnsConf.UpstreamTimeout),
TLSv12Roots: globalContext.tlsRoots,
TLSv12Roots: tlsMgr.rootCerts,
ConfigModified: onConfigModified,
HTTPRegister: httpReg,
LocalPTRResolvers: dnsConf.PrivateRDNSResolvers,
@@ -266,6 +272,7 @@ func newServerConfig(
ServeHTTP3: dnsConf.ServeHTTP3,
UseHTTP3Upstreams: dnsConf.UseHTTP3Upstreams,
ServePlainDNS: dnsConf.ServePlainDNS,
PendingRequestsEnabled: dnsConf.PendingRequests.Enabled,
}
var initialAddresses []netip.Addr

View File

@@ -3,7 +3,6 @@ package home
import (
"context"
"crypto/x509"
"fmt"
"io/fs"
"log/slog"
@@ -22,7 +21,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
@@ -81,10 +79,6 @@ type homeContext struct {
workDir string // Location of our directory, used to protect against CWD being somewhere else
pidFileName string // PID file name. Empty if no PID file was created.
controlLock sync.Mutex
tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
// tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use.
tlsCipherIDs []uint16
// firstRun, if true, tells AdGuard Home to only start the web interface
// service, and only serve the first-run APIs.
@@ -142,7 +136,6 @@ func Main(clientBuildFS fs.FS) {
func setupContext(opts options) (err error) {
globalContext.firstRun = detectFirstRun()
globalContext.tlsRoots = aghtls.SystemRootCAs()
globalContext.mux = http.NewServeMux()
if !opts.noEtcHosts {
@@ -274,18 +267,13 @@ func setupOpts(opts options) (err error) {
return nil
}
// initContextClients initializes Context clients and related fields.
// initContextClients initializes Context clients and related fields. All
// arguments must not be nil.
func initContextClients(
ctx context.Context,
logger *slog.Logger,
sigHdlr *signalHandler,
) (err error) {
err = setupDNSFilteringConf(ctx, logger, config.Filtering)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
//lint:ignore SA1019 Migration is not over.
config.DHCP.WorkDir = globalContext.workDir
config.DHCP.DataDir = globalContext.getDataDir()
@@ -358,11 +346,13 @@ func setupBindOpts(opts options) (err error) {
return nil
}
// setupDNSFilteringConf sets up DNS filtering configuration settings.
// setupDNSFilteringConf sets up DNS filtering configuration settings. All
// arguments must not be nil.
func setupDNSFilteringConf(
ctx context.Context,
baseLogger *slog.Logger,
conf *filtering.Config,
tlsMgr *tlsManager,
) (err error) {
const (
dnsTimeout = 3 * time.Second
@@ -388,7 +378,7 @@ func setupDNSFilteringConf(
conf.Filters = slices.Clone(config.Filters)
conf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
conf.UserRules = slices.Clone(config.UserRules)
conf.HTTPClient = httpClient()
conf.HTTPClient = httpClient(tlsMgr)
cacheTime := time.Duration(conf.CacheTime) * time.Minute
@@ -630,6 +620,23 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
err = initContextClients(ctx, slogLogger, sigHdlr)
fatalOnError(err)
tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager")
tlsMgr, err := newTLSManager(ctx, &tlsManagerConfig{
logger: tlsMgrLogger,
configModified: onConfigModified,
tlsSettings: config.TLS,
servePlainDNS: config.DNS.ServePlainDNS,
})
if err != nil {
tlsMgrLogger.ErrorContext(ctx, "initializing", slogutil.KeyError, err)
onConfigModified()
}
globalContext.tls = tlsMgr
err = setupDNSFilteringConf(ctx, slogLogger, config.Filtering, tlsMgr)
fatalOnError(err)
err = setupOpts(opts)
fatalOnError(err)
@@ -642,7 +649,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
cmdlineUpdate(ctx, slogLogger, opts, upd)
cmdlineUpdate(ctx, slogLogger, opts, upd, tlsMgr)
if !globalContext.firstRun {
// Save the updated config.
@@ -664,19 +671,14 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
globalContext.auth, err = initUsers()
fatalOnError(err)
tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager")
tlsMgr, err := newTLSManager(ctx, tlsMgrLogger, config.TLS, config.DNS.ServePlainDNS)
if err != nil {
log.Error("initializing tls: %s", err)
onConfigModified()
}
globalContext.tls = tlsMgr
sigHdlr.addTLSManager(tlsMgr)
globalContext.web, err = initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
web, err := initWeb(ctx, opts, clientBuildFS, upd, slogLogger, tlsMgr, customURL)
fatalOnError(err)
globalContext.web = web
tlsMgr.setWebAPI(web)
sigHdlr.addTLSManager(tlsMgr)
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&globalContext, config)
fatalOnError(err)
@@ -706,7 +708,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}, sigHdlr *signalH
checkPermissions(ctx, slogLogger, globalContext.workDir, confPath, dataDir, statsDir, querylogDir)
}
globalContext.web.start(ctx)
web.start(ctx)
// Wait for other goroutines to complete their job.
<-done
@@ -1058,8 +1060,15 @@ type jsonError struct {
Message string `json:"message"`
}
// cmdlineUpdate updates current application and exits. l must not be nil.
func cmdlineUpdate(ctx context.Context, l *slog.Logger, opts options, upd *updater.Updater) {
// cmdlineUpdate updates current application and exits. l and tlsMgr must not
// be nil.
func cmdlineUpdate(
ctx context.Context,
l *slog.Logger,
opts options,
upd *updater.Updater,
tlsMgr *tlsManager,
) {
if !opts.performUpdate {
return
}
@@ -1069,7 +1078,7 @@ func cmdlineUpdate(ctx context.Context, l *slog.Logger, opts options, upd *updat
//
// TODO(e.burkov): We could probably initialize the internal resolver
// separately.
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, l)
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, tlsMgr, l)
fatalOnError(err)
l.InfoContext(ctx, "performing update via cli")

View File

@@ -10,10 +10,10 @@ import (
// httpClient returns a new HTTP client that uses the AdGuard Home's own DNS
// server for resolving hostnames. The resulting client should not be used
// until [Context.dnsServer] is initialized.
// until [Context.dnsServer] is initialized. tlsMgr must not be nil.
//
// TODO(a.garipov, e.burkov): This is rather messy. Refactor.
func httpClient() (c *http.Client) {
func httpClient(tlsMgr *tlsManager) (c *http.Client) {
// Do not use Context.dnsServer.DialContext directly in the struct literal
// below, since Context.dnsServer may be nil when this function is called.
dialContext := func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
@@ -27,8 +27,8 @@ func httpClient() (c *http.Client) {
DialContext: dialContext,
Proxy: httpProxy,
TLSClientConfig: &tls.Config{
RootCAs: globalContext.tlsRoots,
CipherSuites: globalContext.tlsCipherIDs,
RootCAs: tlsMgr.rootCerts,
CipherSuites: tlsMgr.customCipherIDs,
MinVersion: tls.VersionTLS12,
},
},

View File

@@ -14,6 +14,7 @@ import (
"fmt"
"log/slog"
"net/http"
"net/netip"
"os"
"strings"
"sync"
@@ -21,6 +22,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/errors"
@@ -41,6 +43,22 @@ type tlsManager struct {
// certLastMod is the last modification time of the certificate file.
certLastMod time.Time
// rootCerts is a pool of root CAs for TLSv1.2.
rootCerts *x509.CertPool
// web is the web UI and API server. It must not be nil.
//
// TODO(s.chzhen): Temporary cyclic dependency due to ongoing refactoring.
// Resolve it.
web *webAPI
// configModified is called when the TLS configuration is changed via an
// HTTP request.
configModified func()
// customCipherIDs are the ID of the cipher suites that AdGuard Home must use.
customCipherIDs []uint16
confLock sync.Mutex
conf tlsConfigSettings
@@ -48,21 +66,50 @@ type tlsManager struct {
servePlainDNS bool
}
// tlsManagerConfig contains the settings for initializing the TLS manager.
type tlsManagerConfig struct {
// logger is used for logging the operation of the TLS Manager. It must not
// be nil.
logger *slog.Logger
// configModified is called when the TLS configuration is changed via an
// HTTP request. It must not be nil.
configModified func()
// tlsSettings contains the TLS configuration settings.
tlsSettings tlsConfigSettings
// servePlainDNS defines if plain DNS is allowed for incoming requests.
servePlainDNS bool
}
// newTLSManager initializes the manager of TLS configuration. m is always
// non-nil while any returned error indicates that the TLS configuration isn't
// valid. Thus TLS may be initialized later, e.g. via the web UI. logger must
// not be nil.
func newTLSManager(
ctx context.Context,
logger *slog.Logger,
conf tlsConfigSettings,
servePlainDNS bool,
) (m *tlsManager, err error) {
// valid. Thus TLS may be initialized later, e.g. via the web UI. conf must
// not be nil. Note that [tlsManager.web] must be initialized later on by using
// [tlsManager.setWebAPI].
func newTLSManager(ctx context.Context, conf *tlsManagerConfig) (m *tlsManager, err error) {
m = &tlsManager{
logger: logger,
status: &tlsConfigStatus{},
conf: conf,
servePlainDNS: servePlainDNS,
logger: conf.logger,
configModified: conf.configModified,
status: &tlsConfigStatus{},
conf: conf.tlsSettings,
servePlainDNS: conf.servePlainDNS,
}
m.rootCerts = aghtls.SystemRootCAs()
if len(conf.tlsSettings.OverrideTLSCiphers) > 0 {
m.customCipherIDs, err = aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
if err != nil {
// Should not happen because upstreams are already validated. See
// [validateTLSCipherIDs].
panic(err)
}
m.logger.InfoContext(ctx, "overriding ciphers", "ciphers", config.TLS.OverrideTLSCiphers)
} else {
m.logger.InfoContext(ctx, "using default ciphers")
}
if m.conf.Enabled {
@@ -79,6 +126,15 @@ func newTLSManager(
return m, nil
}
// setWebAPI stores the provided web API. It must be called before
// [tlsManager.start], [tlsManager.reload], [tlsManager.handleTLSConfigure], or
// [tlsManager.validateTLSSettings].
//
// TODO(s.chzhen): Remove it once cyclic dependency is resolved.
func (m *tlsManager) setWebAPI(webAPI *webAPI) {
m.web = webAPI
}
// load reloads the TLS configuration from files or data from the config file.
func (m *tlsManager) load(ctx context.Context) (err error) {
err = m.loadTLSConf(ctx, &m.conf, m.status)
@@ -126,7 +182,7 @@ func (m *tlsManager) start(_ context.Context) {
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
m.web.tlsConfigChanged(context.Background(), tlsConf)
}
// reload updates the configuration and restarts the TLS manager.
@@ -178,7 +234,7 @@ func (m *tlsManager) reload(ctx context.Context) {
// The background context is used because the TLSConfigChanged wraps context
// with timeout on its own and shuts down the server, which handles current
// request.
globalContext.web.tlsConfigChanged(context.Background(), tlsConf)
m.web.tlsConfigChanged(context.Background(), tlsConf)
}
// reconfigureDNSServer updates the DNS server configuration using the stored
@@ -191,6 +247,7 @@ func (m *tlsManager) reconfigureDNSServer() (err error) {
&config.DNS,
config.Clients.Sources,
tlsConf,
m,
httpRegister,
globalContext.clients.storage,
)
@@ -368,6 +425,8 @@ func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
// handleTLSValidate is the handler for the POST /control/tls/validate HTTP API.
func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
setts, err := unmarshalTLS(r)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
@@ -379,7 +438,9 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
setts.PrivateKey = m.conf.PrivateKey
}
if err = validateTLSSettings(setts); err != nil {
if err = m.validateTLSSettings(setts); err != nil {
m.logger.InfoContext(ctx, "validating tls settings", slogutil.KeyError, err)
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
@@ -388,7 +449,7 @@ func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
// Skip the error check, since we are only interested in the value of
// status.WarningValidation.
status := &tlsConfigStatus{}
_ = m.loadTLSConf(r.Context(), &setts.tlsConfigSettings, status)
_ = m.loadTLSConf(ctx, &setts.tlsConfigSettings, status)
resp := tlsConfig{
tlsConfigSettingsExt: setts,
tlsConfigStatus: status,
@@ -458,7 +519,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
req.PrivateKey = m.conf.PrivateKey
}
if err = validateTLSSettings(req); err != nil {
if err = m.validateTLSSettings(req); err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return
@@ -489,7 +550,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
}()
}
onConfigModified()
m.configModified()
err = m.reconfigureDNSServer()
if err != nil {
@@ -516,36 +577,54 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
// same reason.
if restartHTTPS {
go func() {
globalContext.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
m.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
}()
}
}
// validateTLSSettings returns error if the setts are not valid.
func validateTLSSettings(setts tlsConfigSettingsExt) (err error) {
if setts.Enabled {
err = validatePorts(
tcpPort(config.HTTPConfig.Address.Port()),
tcpPort(setts.PortHTTPS),
tcpPort(setts.PortDNSOverTLS),
tcpPort(setts.PortDNSCrypt),
udpPort(config.DNS.Port),
udpPort(setts.PortDNSOverQUIC),
)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
func (m *tlsManager) validateTLSSettings(setts tlsConfigSettingsExt) (err error) {
if !setts.Enabled {
if setts.ServePlainDNS == aghalg.NBFalse {
// TODO(a.garipov): Support full disabling of all DNS.
return errors.Error("plain DNS is required in case encryption protocols are disabled")
}
} else if setts.ServePlainDNS == aghalg.NBFalse {
// TODO(a.garipov): Support full disabling of all DNS.
return errors.Error("plain DNS is required in case encryption protocols are disabled")
return nil
}
if !webCheckPortAvailable(setts.PortHTTPS) {
return fmt.Errorf("port %d is not available, cannot enable HTTPS on it", setts.PortHTTPS)
var (
tlsConf tlsConfigSettings
webAPIAddr netip.Addr
webAPIPort uint16
plainDNSPort uint16
)
func() {
config.Lock()
defer config.Unlock()
tlsConf = config.TLS
webAPIAddr = config.HTTPConfig.Address.Addr()
webAPIPort = config.HTTPConfig.Address.Port()
plainDNSPort = config.DNS.Port
}()
err = validatePorts(
tcpPort(webAPIPort),
tcpPort(setts.PortHTTPS),
tcpPort(setts.PortDNSOverTLS),
tcpPort(setts.PortDNSCrypt),
udpPort(plainDNSPort),
udpPort(setts.PortDNSOverQUIC),
)
if err != nil {
// Don't wrap the error because it's informative enough as is.
return err
}
return nil
// Don't wrap the error because it's informative enough as is.
return m.checkPortAvailability(tlsConf, setts.tlsConfigSettings, webAPIAddr)
}
// validatePorts validates the uniqueness of TCP and UDP ports for AdGuard Home
@@ -557,10 +636,11 @@ func validatePorts(
tcpPorts := aghalg.UniqChecker[tcpPort]{}
addPorts(
tcpPorts,
tcpPort(bindPort),
tcpPort(dohPort),
tcpPort(dotPort),
tcpPort(dnscryptTCPPort),
bindPort,
dohPort,
dotPort,
dnscryptTCPPort,
tcpPort(dnsPort),
)
err = tcpPorts.Validate()
@@ -569,7 +649,7 @@ func validatePorts(
}
udpPorts := aghalg.UniqChecker[udpPort]{}
addPorts(udpPorts, udpPort(dnsPort), udpPort(doqPort))
addPorts(udpPorts, dnsPort, doqPort)
err = udpPorts.Validate()
if err != nil {
@@ -604,7 +684,7 @@ func (m *tlsManager) validateCertChain(
opts := x509.VerifyOptions{
DNSName: srvName,
Roots: globalContext.tlsRoots,
Roots: m.rootCerts,
Intermediates: pool,
}
_, err = main.Verify(opts)
@@ -615,6 +695,67 @@ func (m *tlsManager) validateCertChain(
return nil
}
// checkPortAvailability checks [tlsConfigSettings.PortHTTPS],
// [tlsConfigSettings.PortDNSOverTLS], and [tlsConfigSettings.PortDNSOverQUIC]
// are available for use. It checks the current configuration and, if needed,
// attempts to bind to the port. The function returns human-readable error
// messages for the frontend. This is best-effort check to prevent an "address
// already in use" error.
//
// TODO(a.garipov): Adapt for HTTP/3.
func (m *tlsManager) checkPortAvailability(
currConf tlsConfigSettings,
newConf tlsConfigSettings,
addr netip.Addr,
) (err error) {
const (
networkTCP = "tcp"
networkUDP = "udp"
protoHTTPS = "HTTPS"
protoDoT = "DNS-over-TLS"
protoDoQ = "DNS-over-QUIC"
)
needBindingCheck := []struct {
network string
proto string
currPort uint16
newPort uint16
}{{
network: networkTCP,
proto: protoHTTPS,
currPort: currConf.PortHTTPS,
newPort: newConf.PortHTTPS,
}, {
network: networkTCP,
proto: protoDoT,
currPort: currConf.PortDNSOverTLS,
newPort: newConf.PortDNSOverTLS,
}, {
network: networkUDP,
proto: protoDoQ,
currPort: currConf.PortDNSOverQUIC,
newPort: newConf.PortDNSOverQUIC,
}}
var errs []error
for _, v := range needBindingCheck {
port := v.newPort
if v.currPort == port {
continue
}
addrPort := netip.AddrPortFrom(addr, port)
err = aghnet.CheckPort(v.network, addrPort)
if err != nil {
errs = append(errs, fmt.Errorf("port %d for %s is not available", port, v.proto))
}
}
return errors.Join(errs...)
}
// errNoIPInCert is the error that is returned from [tlsManager.parseCertChain]
// if the leaf certificate doesn't contain IPs.
const errNoIPInCert errors.Error = `certificates has no IP addresses; ` +
@@ -718,27 +859,12 @@ func (m *tlsManager) validateCertificates(
) (err error) {
// Check only the public certificate separately from the key.
if len(certChain) > 0 {
var certs []*x509.Certificate
certs, status.ValidCert, err = m.parseCertChain(ctx, certChain)
if !status.ValidCert {
var ok bool
ok, err = m.validateCertificate(ctx, status, certChain, serverName)
if !ok {
// Don't wrap the error, since it's informative enough as is.
return err
}
mainCert := certs[0]
status.Subject = mainCert.Subject.String()
status.Issuer = mainCert.Issuer.String()
status.NotAfter = mainCert.NotAfter
status.NotBefore = mainCert.NotBefore
status.DNSNames = mainCert.DNSNames
if chainErr := m.validateCertChain(ctx, certs, serverName); chainErr != nil {
// Let self-signed certs through and don't return this error to set
// its message into the status.WarningValidation afterwards.
err = chainErr
} else {
status.ValidChain = true
}
}
// Validate the private key by parsing it.
@@ -766,6 +892,41 @@ func (m *tlsManager) validateCertificates(
return err
}
// validateCertificate processes certificate data. status must not be nil, as
// it is used to accumulate the validation results. Other parameters are
// optional. If ok is true, the returned error, if any, is not critical.
func (m *tlsManager) validateCertificate(
ctx context.Context,
status *tlsConfigStatus,
certChain []byte,
serverName string,
) (ok bool, err error) {
var certs []*x509.Certificate
certs, status.ValidCert, err = m.parseCertChain(ctx, certChain)
if !status.ValidCert {
// Don't wrap the error, since it's informative enough as is.
return false, err
}
mainCert := certs[0]
status.Subject = mainCert.Subject.String()
status.Issuer = mainCert.Issuer.String()
status.NotAfter = mainCert.NotAfter
status.NotBefore = mainCert.NotBefore
status.DNSNames = mainCert.DNSNames
err = m.validateCertChain(ctx, certs, serverName)
if err != nil {
// Let self-signed certs through and don't return this error to set
// its message into the status.WarningValidation afterwards.
return true, err
}
status.ValidChain = true
return true, nil
}
// Key types.
const (
keyTypeECDSA = "ECDSA"
@@ -828,17 +989,18 @@ func unmarshalTLS(r *http.Request) (tlsConfigSettingsExt, error) {
}
}
if data.PrivateKey != "" {
var key []byte
key, err = base64.StdEncoding.DecodeString(data.PrivateKey)
if err != nil {
return data, fmt.Errorf("failed to base64-decode private key: %w", err)
}
if data.PrivateKey == "" {
return data, nil
}
data.PrivateKey = string(key)
if data.PrivateKeyPath != "" {
return data, fmt.Errorf("private key data and file can't be set together")
}
key, err := base64.StdEncoding.DecodeString(data.PrivateKey)
if err != nil {
return data, fmt.Errorf("failed to base64-decode private key: %w", err)
}
data.PrivateKey = string(key)
if data.PrivateKeyPath != "" {
return data, fmt.Errorf("private key data and file can't be set together")
}
return data, nil

View File

@@ -30,6 +30,7 @@ import (
"github.com/stretchr/testify/require"
)
// TODO(s.chzhen): Consider moving to testdata.
var testCertChainData = []byte(`-----BEGIN CERTIFICATE-----
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
@@ -66,7 +67,11 @@ func TestValidateCertificates(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout)
logger := slogutil.NewDiscardLogger()
m, err := newTLSManager(ctx, logger, tlsConfigSettings{}, false)
m, err := newTLSManager(ctx, &tlsManagerConfig{
logger: logger,
configModified: func() {},
servePlainDNS: false,
})
require.NoError(t, err)
t.Run("bad_certificate", func(t *testing.T) {
@@ -112,7 +117,6 @@ func TestValidateCertificates(t *testing.T) {
// - [homeContext.clients.storage]
// - [homeContext.dnsServer]
// - [homeContext.mux]
// - [homeContext.web]
//
// TODO(s.chzhen): Remove this once the TLS manager no longer accesses global
// variables. Make tests that use this helper concurrent.
@@ -123,14 +127,12 @@ func storeGlobals(tb testing.TB) {
storage := globalContext.clients.storage
dnsServer := globalContext.dnsServer
mux := globalContext.mux
web := globalContext.web
tb.Cleanup(func() {
config = prevConfig
globalContext.clients.storage = storage
globalContext.dnsServer = dnsServer
globalContext.mux = mux
globalContext.web = web
})
}
@@ -221,9 +223,6 @@ func TestTLSManager_Reload(t *testing.T) {
globalContext.mux = http.NewServeMux()
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
const (
snBefore int64 = 1
snAfter int64 = 2
@@ -236,15 +235,25 @@ func TestTLSManager_Reload(t *testing.T) {
certDER, key := newCertAndKey(t, snBefore)
writeCertAndKey(t, certDER, certPath, key, keyPath)
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
m, err := newTLSManager(ctx, &tlsManagerConfig{
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
},
},
}, false)
servePlainDNS: false,
})
require.NoError(t, err)
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
m.setWebAPI(web)
conf := &tlsConfigSettings{}
m.WriteDiskConfig(conf)
assertCertSerialNumber(t, conf, snBefore)
@@ -265,13 +274,18 @@ func TestTLSManager_HandleTLSStatus(t *testing.T) {
err error
)
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
m, err := newTLSManager(ctx, &tlsManagerConfig{
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
},
},
}, false)
servePlainDNS: false,
})
require.NoError(t, err)
w := httptest.NewRecorder()
@@ -291,26 +305,42 @@ func TestTLSManager_HandleTLSStatus(t *testing.T) {
func TestValidateTLSSettings(t *testing.T) {
storeGlobals(t)
globalContext.mux = http.NewServeMux()
var (
logger = slogutil.NewDiscardLogger()
ctx = testutil.ContextWithTimeout(t, testTimeout)
err error
)
ln, err := net.Listen("tcp", ":0")
m, err := newTLSManager(ctx, &tlsManagerConfig{
logger: logger,
configModified: func() {},
servePlainDNS: false,
})
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, ln.Close)
addr := testutil.RequireTypeAssert[*net.TCPAddr](t, ln.Addr())
busyPort := addr.Port
globalContext.mux = http.NewServeMux()
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
m.setWebAPI(web)
tcpLn, err := net.Listen("tcp", ":0")
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, tcpLn.Close)
tcpAddr := testutil.RequireTypeAssert[*net.TCPAddr](t, tcpLn.Addr())
busyTCPPort := tcpAddr.Port
udpLn, err := net.ListenPacket("udp", ":0")
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, udpLn.Close)
udpAddr := testutil.RequireTypeAssert[*net.UDPAddr](t, udpLn.LocalAddr())
busyUDPPort := udpAddr.Port
testCases := []struct {
setts tlsConfigSettingsExt
name string
@@ -329,11 +359,29 @@ func TestValidateTLSSettings(t *testing.T) {
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortHTTPS: uint16(busyPort),
PortHTTPS: uint16(busyTCPPort),
},
},
name: "busy_port",
wantErr: fmt.Sprintf("port %d is not available, cannot enable HTTPS on it", busyPort),
name: "busy_https_port",
wantErr: fmt.Sprintf("port %d for HTTPS is not available", busyTCPPort),
}, {
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortDNSOverTLS: uint16(busyTCPPort),
},
},
name: "busy_dot_port",
wantErr: fmt.Sprintf("port %d for DNS-over-TLS is not available", busyTCPPort),
}, {
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
PortDNSOverQUIC: uint16(busyUDPPort),
},
},
name: "busy_doq_port",
wantErr: fmt.Sprintf("port %d for DNS-over-QUIC is not available", busyUDPPort),
}, {
setts: tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
@@ -348,7 +396,7 @@ func TestValidateTLSSettings(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err = validateTLSSettings(tc.setts)
err = m.validateTLSSettings(tc.setts)
testutil.AssertErrorMsg(t, tc.wantErr, err)
})
}
@@ -357,26 +405,33 @@ func TestValidateTLSSettings(t *testing.T) {
func TestTLSManager_HandleTLSValidate(t *testing.T) {
storeGlobals(t)
globalContext.mux = http.NewServeMux()
var (
logger = slogutil.NewDiscardLogger()
ctx = testutil.ContextWithTimeout(t, testTimeout)
err error
)
globalContext.mux = http.NewServeMux()
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
m, err := newTLSManager(ctx, &tlsManagerConfig{
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificateChain: string(testCertChainData),
PrivateKey: string(testPrivateKeyData),
},
},
}, false)
servePlainDNS: false,
})
require.NoError(t, err)
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
m.setWebAPI(web)
setts := &tlsConfigSettingsExt{
tlsConfigSettings: tlsConfigSettings{
Enabled: true,
@@ -438,9 +493,6 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
globalContext.mux = http.NewServeMux()
globalContext.web, err = initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
config.DNS.BindHosts = []netip.Addr{netip.MustParseAddr("127.0.0.1")}
config.DNS.Port = 0
@@ -455,15 +507,25 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
writeCertAndKey(t, certDER, certPath, key, keyPath)
// Initialize the TLS manager and assert its configuration.
m, err := newTLSManager(ctx, logger, tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
m, err := newTLSManager(ctx, &tlsManagerConfig{
logger: logger,
configModified: func() {},
tlsSettings: tlsConfigSettings{
Enabled: true,
TLSConfig: dnsforward.TLSConfig{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
},
},
}, true)
servePlainDNS: true,
})
require.NoError(t, err)
web, err := initWeb(ctx, options{}, nil, nil, logger, nil, false)
require.NoError(t, err)
m.setWebAPI(web)
conf := &tlsConfigSettings{}
m.WriteDiskConfig(conf)
assertCertSerialNumber(t, conf, wantSerialNumber)
@@ -509,10 +571,10 @@ func TestTLSManager_HandleTLSConfigure(t *testing.T) {
//
// TODO(s.chzhen): Remove when [httpsServer.cond] is removed.
assert.Eventually(t, func() bool {
globalContext.web.httpsServer.condLock.Lock()
defer globalContext.web.httpsServer.condLock.Unlock()
web.httpsServer.condLock.Lock()
defer web.httpsServer.condLock.Unlock()
cert = globalContext.web.httpsServer.cert
cert = web.httpsServer.cert
if cert.Leaf == nil {
return false
}

View File

@@ -12,10 +12,8 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/updater"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/netutil/httputil"
@@ -158,27 +156,6 @@ func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) {
return w
}
// webCheckPortAvailable checks if port, which is considered an HTTPS port, is
// available, unless the HTTPS server isn't active.
//
// TODO(a.garipov): Adapt for HTTP/3.
func webCheckPortAvailable(port uint16) (ok bool) {
if globalContext.web.httpsServer.server != nil {
return true
}
addrPort := netip.AddrPortFrom(config.HTTPConfig.Address.Addr(), port)
err := aghnet.CheckPort("tcp", addrPort)
if err != nil {
log.Info("web: warning: checking https port: %s", err)
return false
}
return true
}
// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server
// if necessary.
func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
@@ -329,8 +306,8 @@ func (web *webAPI) tlsServerLoop(ctx context.Context) {
Handler: hdlr,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: globalContext.tlsRoots,
CipherSuites: globalContext.tlsCipherIDs,
RootCAs: web.tlsManager.rootCerts,
CipherSuites: web.tlsManager.customCipherIDs,
MinVersion: tls.VersionTLS12,
},
ReadTimeout: web.conf.ReadTimeout,
@@ -363,8 +340,8 @@ func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) {
Addr: address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{web.httpsServer.cert},
RootCAs: globalContext.tlsRoots,
CipherSuites: globalContext.tlsCipherIDs,
RootCAs: web.tlsManager.rootCerts,
CipherSuites: web.tlsManager.customCipherIDs,
MinVersion: tls.VersionTLS12,
},
Handler: withMiddlewares(globalContext.mux, limitRequestBody),

View File

@@ -64,7 +64,7 @@ type Entry struct {
Domain string
// UpstreamStats contains the DNS query statistics for both the upstream and
// fallback DNS servers.
// fallback DNS servers. Don't modify items in the slice.
UpstreamStats []*proxy.UpstreamStatistics
// Result is the result of processing the request.

View File

@@ -6,12 +6,6 @@ We are using [OpenAPI specification](https://swagger.io/docs/specification/about
The easiest way would be to use [Swagger Editor](http://editor.swagger.io/) and just copy/paste the YAML file there.
## How to read the API doc
1. `yarn install`
2. `yarn start`
3. open `http://localhost:4000/`
## Changelog
[Here](CHANGELOG.md) we keep track of all non-compatible changes that are being made.

View File

@@ -1,12 +0,0 @@
{
"name": "adguard-home-api",
"version": "0.2.0",
"private": true,
"scripts": {
"start": "node_modules/.bin/speccy serve -p 4000 openapi.yaml",
"test": "node_modules/.bin/speccy lint openapi.yaml"
},
"dependencies": {
"speccy": "^0.11.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -97,6 +97,7 @@ if [ "$(git diff --cached --name-only -- '*.go' '*.mod' 'Makefile' || :)" != ''
make VERBOSE="$verbose" go-os-check go-lint go-test
fi
if [ "$(git diff --cached --name-only -- './openapi/openapi.yaml' || :)" != '' ]; then
make VERBOSE="$verbose" openapi-lint
fi
# TODO(a.gairpov): Re-enable after finding a better linter.
# if [ "$(git diff --cached --name-only -- './openapi/openapi.yaml' || :)" != '' ]; then
# make VERBOSE="$verbose" openapi-lint
# fi

View File

@@ -119,4 +119,5 @@ $sudo_cmd docker "$debug_flags" \
--build-arg VERSION="$version" \
--output "$docker_output" \
--platform "$docker_platforms" \
--progress 'plain' \
$docker_version_tag $docker_channel_tag -f ./docker/Dockerfile .

View File

@@ -34,6 +34,18 @@ trailing_newlines() (
readonly nl
find . \
'(' \
-type 'd' \
'(' \
-name 'node_modules' \
-o -path './.git' \
-o -path './bin' \
-o -path './build' \
-o -path './client/playwright-report' \
')' \
-prune \
')' \
-o \
-type 'f' \
'!' '(' \
-name '*.db' \
@@ -46,11 +58,8 @@ trailing_newlines() (
-o -name '*.zip' \
-o -name 'AdGuardHome' \
-o -name 'adguard-home' \
-o -path '*/node_modules/*' \
-o -path './.git/*' \
-o -path './bin/*' \
-o -path './build/*' \
')' \
-print \
| while read -r f; do
final_byte="$(tail -c -1 "$f")"
if [ "$final_byte" != "$nl" ]; then